Partitioning variance, checking assumptions, and post-hoc tests with emmeans
2026-07-05
fct_reorder() / fct_relevel() — set the order and the reference groupNote
✅ Transition
The t-test (Lecture 04) compared two groups. But biology is full of three-or-more group questions — three species, four sites, five treatments. Now that you can control factor levels, today we learn the test for many groups: one-way ANOVA.
aov() and car::Anova()emmeans post-hoc testsTools today:
palmerpenguins, tidyversecar — Levene’s test, Anova()emmeans, multcomp — post-hocTextbook:
Naming: models → _model, plots → _plot
This lecture runs in four short chunks. After each chunk you switch to the activity and type the code yourself.
For every code block, do three things:
Note
✅ Why bother? (the evidence)
We will cover: why not many t-tests, what ANOVA asks, how F partitions variance, and a first look at the penguin data.
Tip
🖐 After this chunk: Activity Parts 1–3 (load the penguins, explore, state hypotheses).
With 3 groups you’d need 3 t-tests (A–B, A–C, B–C). With 4 groups, 6 tests.
Every test at α = 0.05 has a 5% chance of a false positive. Run several and those risks stack up:
ANOVA asks the question once, holding the overall error rate at 0.05.
Important
The multiple-comparisons problem
More tests = more chances to be fooled by noise. ANOVA is the single, honest test for “do any of these groups differ?”
📖 W&S §15.1 — why a single test
One numeric response (Y) across the levels of one categorical predictor (X).
| Hypothesis | |
|---|---|
| H₀ — null | all group means are equal (\(\mu_1 = \mu_2 = \mu_3\)) |
| Hₐ — alternate | at least one group mean differs |
Note what Hₐ does not say: it does not tell you which group differs — that comes later, from the post-hoc test.
Our question today:
Do the three penguin species differ in body mass?
body_mass_gspecies (Adelie, Chinstrap, Gentoo)\[F = \frac{\text{variance BETWEEN groups}}{\text{variance WITHIN groups}}\]
Big F → the groups are far apart relative to their internal noise → small p-value.
F ≈ 1 → the spread between groups is no bigger than the spread within them → no real difference.
Note
✅ Key idea
ANOVA is a signal-to-noise ratio. The “signal” is the gap between group means; the “noise” is the within-group scatter.
📖 W&S §15.1–15.2
Three species, one lab: body mass (g) of penguins at Palmer Station, Antarctica.
drop_na() removes rows with missing masscount(species) shows the sample size per groupImportant
The groups are unbalanced (different n per species) — that matters when we pick our ANOVA type later.
Note
🔮 Predict first: Before the plot renders — do you expect all three species to differ, or just one? Which species do you think is heaviest?
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
Always plot before you test.
📖 R4DS §9 — Layers
Load the penguins, plot body mass by species, and write your hypotheses. Predict which species differ before you run anything.
We will cover: fitting the model, then checking normality of the residuals and equal variance across groups.
Tip
🖐 After this chunk: Activity Parts 4–6 (fit, QQ + Shapiro on residuals, Levene’s test).
lm(Y ~ X) — the same syntax as regression (Lecture 06)X is categorical, that linear model is a one-way ANOVANote
✅ Key idea
ANOVA and regression are the same machinery (lm). ANOVA just has a grouping variable instead of a numeric one.
📖 W&S §15.2

Important
Large-sample caution: with ~340 birds, Shapiro-Wilk flags tiny departures as “significant.” Trust the QQ plot — if points track the line, you’re fine.
Note
🔮 Predict first: Look back at the boxplot spreads. Predict — will Levene’s test call the variances equal (p > 0.05) or unequal?
Fit lm(body_mass_g ~ species), check the residual QQ plot + Shapiro, and run Levene’s test. Predict each result before you run it.
We will cover: the ANOVA table two ways — base aov() and car::Anova() — and why the difference matters for unbalanced data.
Tip
🖐 After this chunk: Activity Parts 7–8 (run both, read F / df / p).
aov() and summary()Note
🔮 Predict first: The boxplot looked very separated. Predict the p-value — closer to 0.5, 0.05, or far below 0.001?
Df Sum Sq Mean Sq F value Pr(>F)
species 2 146864214 73432107 343.6 <2e-16 ***
Residuals 339 72443483 213698
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Reading the table:
| Column | Meaning |
|---|---|
Df |
groups − 1, and residual df |
F value |
between ÷ within variance |
Pr(>F) |
the p-value for H₀ |
p < 0.05 → reject H₀ → at least one species differs. But which? Chunk 4.
For unbalanced data we prefer car::Anova(). I’ll reach for it but type lowercase, and forget the package:
R stops:
Error in Anova(mass_model) :
could not find function "Anova"
The fix — load car, and mind the capital A:
Tip
✅ Why show a broken run?
anova() (base) and Anova() (car) are different functions — lowercase compares models with Type I sums of squares; capital-A gives the Type II/III table you want. “could not find function” almost always means a missing library().
car::Anova() — Type II for Unbalanced DataWhy Type II?
Note
anova() = Type I (order-dependent). Anova(type = "II") = order-independent.
Run aov() + summary(), then car::Anova(type = "II"). Predict the F and p before you read them, and confirm both tables agree.
We will cover: ANOVA says “some differ” — emmeans says which, with the comparisons properly adjusted.
Tip
🖐 After this chunk: Activity Parts 9–11 (pairwise tests, letters, plot, report).
Note
🔮 Predict first: From the boxplot, predict — will all three species differ from each other, or will two be statistically tied?
contrast estimate SE df t.ratio p.value
Adelie - Chinstrap -32.4 67.5 339 -0.480 0.8807
Adelie - Gentoo -1375.4 56.1 339 -24.495 <0.0001
Chinstrap - Gentoo -1342.9 69.9 339 -19.224 <0.0001
P value adjustment: tukey method for comparing a family of 3 estimates
Reading it:
emmeans(..., pairwise ~ species) gives each group’s mean and every pairwise comparison📖 W&S §15.4 — planned vs. unplanned comparisons
species emmean SE df lower.CL upper.CL .group
Adelie 3701 37.6 339 3627 3775 a
Chinstrap 3733 56.1 339 3623 3843 a
Gentoo 5076 41.7 339 4994 5158 b
Confidence level used: 0.95
P value adjustment: tukey method for comparing a family of 3 estimates
significance level used: alpha = 0.05
NOTE: If two or more means share the same grouping symbol,
then we cannot show them to be different.
But we also did not show them to be the same.
How to read letters:
This is the compact summary you see in published figures — one letter above each bar.
Tip
Install once: install.packages("multcomp")
mass_emm_plot <- as.data.frame(mass_emm$emmeans) %>%
ggplot(aes(x = species, y = emmean, color = species)) +
geom_point(size = 3) +
geom_errorbar(
aes(ymin = lower.CL, ymax = upper.CL),
width = 0.15, linewidth = 0.9
) +
labs(x = "Species", y = "Estimated mean mass (g)") +
theme_minimal() +
theme(legend.position = "none")
mass_emm_plot
Estimated marginal means with 95% confidence intervals.
The emmeans plot is the ANOVA cousin of Lecture 03’s mean ± SE plot.
Anova Table (Type II tests)
Response: body_mass_g
Sum Sq Df F value Pr(>F)
species 146864214 2 343.63 < 2.2e-16 ***
Residuals 72443483 339
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
“Body mass differed significantly among the three penguin species (one-way ANOVA: F(2, 339) = 343.6, p < 0.001). Tukey-adjusted comparisons showed all three species differed, with Gentoo heaviest and Adelie lightest.”
Always include:
Never write “p = 0.000” — use “p < 0.001”.
Run the emmeans pairwise test, get the letters, plot the means ± CI, and write your results sentence. Predict which species differ before you run the comparisons.
Concepts:
R skills:
lm(Y ~ group) and aov() + summary()car::Anova(model, type = "II")leveneTest(), shapiro.test(residuals())emmeans(model, pairwise ~ group) + multcomp::cld()References:
Up next — Lecture 12: