Worksheet — One-Way ANOVA I: Setting Up the Test (with Factors)

Exploring the penguins, ordering groups with factors, fitting the model, and checking assumptions

anova
statistics
project-setup

Hands-on companion to One-Way ANOVA I. Students explore the Palmer penguins data, fit a one-way ANOVA, and check its assumptions — normality of residuals and equal variance.

Author

Bill Perry

Published

September 10, 2026

One-Way ANOVA I — Do Penguin Species Differ in Body Mass?

Recap from Worksheets 09–11

  • Compared two groups with Welch’s t-test
  • Fit lm(Y ~ X) with a numeric predictor in the regression worksheet
  • Checked assumptions with plot(model) and Shapiro-Wilk

Today’s Objectives

  1. Load and explore the Palmer penguins data
  2. Order the groups with a factor (fct_reorder) for a readable boxplot
  3. State the ANOVA null and alternate hypotheses
  4. Fit a one-way ANOVA, set its reference level (fct_relevel), and check its assumptions

How to use this worksheet

  • Work through the parts in order. Type the code into a new R script in Positron and run it line by line.
  • Blocks marked ▶ Run this should be executed as written.
  • Blocks marked ✏️ Your turn ask you to write, modify, or interpret.
  • There’s no separate homework — the out-of-class Extension is on the ANOVA II worksheet.
Note🔮 Predict before you run — and type, don’t paste

Before each ▶ Run this block, cover the output and predict what R will print — the F, the p-value, which species differ. Then type the code yourself. Predicting first is what turns a test from a black box into something you understand; typing trains your eye for the small errors (anova vs Anova, a missing library()) that eat real time.


🧩 Chunk 1 — Explore, order, hypothesize (after lecture Chunk 1)

Parts 1–4: load the penguins, plot body mass by species, reorder the boxplot with a factor, and write your hypotheses.

Part 1 · Load libraries and data

▶ Run this at the top of your script:

# Load packages at the top — always ----------------------
library(tidyverse)      # wrangling + ggplot2
library(palmerpenguins) # the penguins data
library(car)            # Levene's test, Anova()
library(emmeans)        # post-hoc pairwise tests

▶ Run this:

# Drop rows missing mass or species ----------------------
penguins_df <- penguins %>%
  drop_na(body_mass_g, species)

penguins_df %>% count(species)

✏️ Your turn: Record the sample sizes.

n Adelie:
n Chinstrap:
n Gentoo:
Are the groups balanced (equal n)?  Y / N

⚠️ Watch out! If R says could not find function "penguins", you forgot library(palmerpenguins). Install it once with install.packages("palmerpenguins").


Part 2 · Explore with a boxplot

🔮 Predict first: Before you plot — which species do you think is heaviest? Do you expect all three to differ, or just one?

▶ Run this:

# Body mass by species -----------------------------------
mass_box_plot <- penguins_df %>%
  ggplot(aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot(alpha = 0.5, outlier.shape = NA) +
  geom_point(position = position_jitter(width = 0.15, seed = 42),
             alpha = 0.3, size = 1.5) +
  labs(x = "Species", y = "Body Mass (g)") +
  theme_minimal() +
  theme(legend.position = "none")

mass_box_plot

✏️ Your turn: Describe what you see.

Heaviest species:
Lightest species:
Do the spreads (box heights) look similar across species?  Y / N
What order are the species in on the x-axis, and why?

Part 3 · Order the boxplot with a factor

🔮 Predict first: species is a factor. Run levels(penguins_df$species) — what order are the levels in? Is that the order you want on the plot?

▶ Run this:

# What order does R store the species levels in? --------
class(penguins_df$species)
levels(penguins_df$species)

# Reorder species by MEDIAN body mass, then plot --------
penguins_df %>%
  mutate(species = fct_reorder(species, body_mass_g, .fun = median)) %>%
  ggplot(aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot(alpha = 0.5, outlier.shape = NA) +
  labs(x = "Species (ordered by median mass)", y = "Body Mass (g)") +
  theme_minimal() +
  theme(legend.position = "none")

✏️ Your turn: Answer.

Stored level order:
Order after fct_reorder():
Does the reordered plot read more clearly?  Y / N

✏️ Your turn: fct_reorder() has to go inside mutate(). What happens if you just call fct_reorder() on its own and then plot penguins_df?

Your answer:

💡 Key idea: the whole fct_* family (rename, merge, lump levels) lives in Common Code 15 · Factors. Today you only need fct_reorder() here and fct_relevel() in Part 6.


Part 4 · State your hypotheses

✏️ Your turn: Write the ANOVA hypotheses in words and symbols.

H₀ (null) in words:

Hₐ (alternate) in words:

In symbols:  H₀: μ_Adelie = μ_Chinstrap = μ_Gentoo
             Hₐ:

Significance level α:

💡 Key idea: Hₐ only says “at least one differs” — it does not say which. Finding which is the job of the post-hoc test in ANOVA II.


🧩 Chunk 2 — Fit & check assumptions (after lecture Chunk 2)

Parts 5–8: fit the model, set the reference level, check residual normality, check equal variance.

Part 5 · Fit the model

▶ Run this:

# ANOVA is a linear model with a categorical X ----------
mass_model <- lm(body_mass_g ~ species, data = penguins_df)

✏️ Your turn: This is the same lm() syntax as the regression worksheet. What is different about the X variable this time?

Your answer:

Part 6 · Set the reference level with fct_relevel()

🔮 Predict first: lm() uses the first factor level as the baseline. If you move Gentoo to the front, will the overall F-test / p-value change, or only the coefficients?

▶ Run this:

# Coefficients are differences FROM the first level -----
coef(mass_model)

# Make Gentoo the reference, refit, compare ------------
penguins_relevel <- penguins_df %>%
  mutate(species = fct_relevel(species, "Gentoo"))

mass_model_gentoo <- lm(body_mass_g ~ species, data = penguins_relevel)
coef(mass_model_gentoo)

✏️ Your turn: Compare the two coefficient sets.

What (Intercept) represents in mass_model:
What (Intercept) represents in mass_model_gentoo:
Did the coefficient values change?  Y / N
Would the overall F-statistic / p-value change?  Y / N

💡 Key idea: releveling changes which comparison the coefficients show, not whether any means differ. Use it to make the baseline a group your reader cares about (a control, a reference site).


Part 7 · Assumption 1 — normality of residuals

🔮 Predict first: plot(mass_model) gives the same four panels as the regression worksheet. Which panel checks normality?

▶ Run this:

# plot() on an lm object gives all four diagnostics -------
par(mfrow = c(2, 2))
plot(mass_model)
# Formal test on the residuals ---------------------------
shapiro.test(residuals(mass_model))

✏️ Your turn: Record and interpret.

Do the Normal Q-Q points (panel 2) track the dashed line?  Y / mostly / no
Shapiro-Wilk p-value =

✏️ Your turn: With ~340 penguins, Shapiro-Wilk is very sensitive and may flag tiny departures. Which should you trust more here — the Q-Q panel or the Shapiro p-value? Why?

Your answer:

Part 8 · Assumption 2 — equal variance

🔮 Predict first: From the box heights in Part 2, predict — will Levene’s test say the variances are equal (p > 0.05) or unequal?

▶ Run this:

# Levene's test — H0: all groups have equal variance ----
leveneTest(body_mass_g ~ species, data = penguins_df)

✏️ Your turn: Record and decide.

Levene's F =            p =
Variances equal?  Y / N
If NOT equal, which test would you use instead? (hint: Welch)

Review and checkpoint

At this point you should be able to:

✏️ Your turn — before you move on: Run your whole script top to bottom with Ctrl/Cmd + Shift + Enter. Does it run cleanly?

Ran cleanly?  Y / N
If not, what error appeared:

Getting unstuck

  1. could not find function "penguins"library(palmerpenguins).
  2. could not find function "fct_reorder"library(tidyverse) (forcats is inside it).
  3. Reordering “did nothing” → you must reorder inside mutate() and plot the result; reordering a copy doesn’t change the original.
  4. Levene says unequal variance → use oneway.test(y ~ group, var.equal = FALSE) (Welch’s ANOVA).
  5. Cheat sheetshttps://posit.co/resources/cheatsheets/ · Factors: https://rstudio.github.io/cheatsheets/factors.pdf

End of the ANOVA I worksheet. Next: One-Way ANOVA II — running the test, post-hoc comparisons, and the out-of-class Extension.