Worksheet — One-Way ANOVA I: Setting Up the Test (with Factors)
Exploring the penguins, ordering groups with factors, fitting the model, and checking assumptions
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.
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
- Load and explore the Palmer penguins data
- Order the groups with a factor (
fct_reorder) for a readable boxplot - State the ANOVA null and alternate hypotheses
- 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.
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 forgotlibrary(palmerpenguins). Install it once withinstall.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:
speciesis a factor. Runlevels(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 needfct_reorder()here andfct_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
could not find function "penguins"→library(palmerpenguins).could not find function "fct_reorder"→library(tidyverse)(forcats is inside it).- Reordering “did nothing” → you must reorder inside
mutate()and plot the result; reordering a copy doesn’t change the original. - Levene says unequal variance → use
oneway.test(y ~ group, var.equal = FALSE)(Welch’s ANOVA). - Cheat sheets — https://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.