Compare Several Means with ANOVA
Fit the model, check residual spread, then use Tukey’s HSD to see which groups differ
Everything on this page runs real R in your browser. You will practice Coding Assignment 4, Part C: one-way ANOVA with assumption checks and Tukey’s HSD post-hoc comparisons.
On DataHub, Assignment 4 uses BrittlebushNitrogen.csv with four nitrogen levels and biomass_g. Here we ask whether bill depth differs across the three penguin species. The workflow — aov(), residual plot, fligner.test(), TukeyHSD() — is the same.
One-way ANOVA tests whether any group mean differs when you have one categorical factor with more than two levels and a continuous response. If the overall F-test is significant, use Tukey’s HSD (not a pile of uncorrected t-tests) to see which pairs differ.
With several groups, fit the model first, then check residuals — faceted histograms show each level; plot(model, which = 1) shows residual spread.
Research question: Does mean bill depth differ across Adelie, Chinstrap, and Gentoo penguins?
1. Load packages
Load tidyverse (ggplot2, dplyr) and palmerpenguins (penguins dataset).
library(tidyverse)
library(palmerpenguins)2. Build the analysis table
Keep every penguin with non-missing bill_depth_mm — use filter(!is.na(bill_depth_mm)). Save as penguin_data.
Filter penguins to rows with non-missing bill_depth_mm.
Example:
penguins |>
filter(!is.na(bill_length_mm))penguin_data <- penguins |>
filter(!is.na(bill_depth_mm))342 rows — all three species, no missing bill depths.
3. Boxplot by species
Make a boxplot of bill_depth_mm by species. Save as box_species and display it.
Use ggplot(), aes(), and geom_boxplot() on penguin_data. Map species to x and bill_depth_mm to y.
Example:
ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
geom_boxplot()box_species <- ggplot(penguin_data, aes(x = species, y = bill_depth_mm)) +
geom_boxplot()
box_speciesGentoo bills look shallower than Adelie and Chinstrap — but is that difference significant?
4. Faceted histograms
Build a histogram of bill_depth_mm faceted by species so you can judge shape within each level (as in lecture A009’s facet_wrap(~ feed)). Save as hist_species and display it.
Add geom_histogram(), then facet_wrap(~ species) so each species gets its own panel.
Example:
ggplot(penguins, aes(x = bill_length_mm)) +
geom_histogram() +
facet_wrap(~ species)hist_species <- ggplot(penguin_data, aes(x = bill_depth_mm)) +
geom_histogram() +
facet_wrap(~ species)
hist_speciesEach species gets its own panel — easier to spot skew or outliers per group.
5. Fit one-way ANOVA
Fit aov(bill_depth_mm ~ species, data = penguin_data). Save as anova_model and print summary(anova_model).
Fit one-way ANOVA with aov(response ~ factor, data = ...), then summary().
Example:
fit <- aov(mpg ~ as.factor(cyl), data = mtcars)
summary(fit)anova_model <- aov(bill_depth_mm ~ species, data = penguin_data)
summary(anova_model)F is large and p ≪ 0.001 — at least one species mean differs.
6. Residuals vs fitted
Use plot(anova_model, which = 1) to display the residuals-versus-fitted plot. Use only which = 1 — do not interpret the normal Q–Q panel.
After fitting anova_model, check residual spread with plot(model, which = 1).
Look for a random cloud with no funnel shape — the exercise code is already written for you; run it and inspect the plot.
plot(anova_model, which = 1)Residuals scatter roughly evenly around zero — no strong funnel.
7. Fligner test for equal variance
Test homogeneity of variance across species with fligner.test(). Save as fligner_anova and print it.
Test equal variances with fligner.test() using the same formula as your ANOVA.
Example:
fligner.test(mpg ~ as.factor(cyl), data = mtcars)fligner_anova <- fligner.test(bill_depth_mm ~ species, data = penguin_data)
fligner_anovap ≈ 0.13 — no strong evidence against equal variances.
8. Assumptions reasonably met?
Assign anova_assumptions_ok <- TRUE if the residual plot and Fligner test look acceptable.
Use the Fligner p-value and your residual plot. Assign TRUE if variance homogeneity looks acceptable, otherwise FALSE.
anova_assumptions_ok <- TRUE9. Tukey’s HSD
Run TukeyHSD(anova_model). Save as tukey_result and print it.
Run TukeyHSD() on the fitted ANOVA model object.
Example:
TukeyHSD(aov(mpg ~ as.factor(cyl), data = mtcars))tukey_result <- TukeyHSD(anova_model)
tukey_resultAdelie–Chinstrap: not significant. Gentoo differs from both Adelie and Chinstrap.
10. Interpret Tukey contrasts
Which pair is not significantly different? Assign adelie_chinstrap_same <- TRUE if Adelie and Chinstrap bill depths do not differ after Tukey correction.
Read tukey_result — check each pairwise p adj. Which species pair is not significant? Is Gentoo shallower than both others?
adelie_chinstrap_same <- TRUE
gentoo_shallower <- TRUEOverall ANOVA was significant, but only Gentoo stands apart in bill depth — Adelie and Chinstrap are statistically similar.
11. Recap — functions you practiced
| Function | Role |
|---|---|
ggplot() + geom_boxplot() |
Compare a continuous response across factor levels |
geom_histogram() + facet_wrap(~ species) |
One distribution panel per level |
aov(y ~ x, data = ...) |
Fit one-way ANOVA |
summary(anova_model) |
Overall F-test — do any means differ? |
plot(model, which = 1) |
Residuals vs fitted — check spread |
fligner.test(y ~ x, data = ...) |
Test homogeneity of variances |
TukeyHSD(model) |
Pairwise comparisons with family-wise error control |
If you can explain each row, you have covered Coding Assignment 4, Part C.
Keep playing
Try ANOVA on body_mass_g ~ species instead. Run fligner.test(body_mass_g ~ species, data = penguin_data) — does equal variance hold?
Fligner p ≈ 0.01 for body mass — variances differ across species. That is why this activity uses bill depth, where Fligner passes. Choosing a response where assumptions hold makes interpretation cleaner.