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.

NoteAssignment vs this activity

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.

NoteANOVA in one sentence

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

NoteHint

Load tidyverse (ggplot2, dplyr) and palmerpenguins (penguins dataset).

TipSolution
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.

NoteHint

Filter penguins to rows with non-missing bill_depth_mm.

Example:

penguins |>
  filter(!is.na(bill_length_mm))
TipSolution
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.

NoteHint

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()
TipSolution
box_species <- ggplot(penguin_data, aes(x = species, y = bill_depth_mm)) +
  geom_boxplot()
box_species

Gentoo 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.

NoteHint

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)
TipSolution
hist_species <- ggplot(penguin_data, aes(x = bill_depth_mm)) +
  geom_histogram() +
  facet_wrap(~ species)
hist_species

Each 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).

NoteHint

Fit one-way ANOVA with aov(response ~ factor, data = ...), then summary().

Example:

fit <- aov(mpg ~ as.factor(cyl), data = mtcars)
summary(fit)
TipSolution
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.

NoteHint

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.

TipSolution
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.

NoteHint

Test equal variances with fligner.test() using the same formula as your ANOVA.

Example:

fligner.test(mpg ~ as.factor(cyl), data = mtcars)
TipSolution
fligner_anova <- fligner.test(bill_depth_mm ~ species, data = penguin_data)
fligner_anova

p ≈ 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.

NoteHint

Use the Fligner p-value and your residual plot. Assign TRUE if variance homogeneity looks acceptable, otherwise FALSE.

TipSolution
anova_assumptions_ok <- TRUE

9. Tukey’s HSD

Run TukeyHSD(anova_model). Save as tukey_result and print it.

NoteHint

Run TukeyHSD() on the fitted ANOVA model object.

Example:

TukeyHSD(aov(mpg ~ as.factor(cyl), data = mtcars))
TipSolution
tukey_result <- TukeyHSD(anova_model)
tukey_result

Adelie–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.

NoteHint

Read tukey_result — check each pairwise p adj. Which species pair is not significant? Is Gentoo shallower than both others?

TipSolution
adelie_chinstrap_same <- TRUE
gentoo_shallower <- TRUE

Overall 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.