Chapter 11 · Relationships
Analysis of Variance (ANOVA)
To compare the means of three or more groups at once, compare two kinds of variation: how far the group means sit from each other versus how much the data scatter inside each group. If the first is large relative to the second, the groups differ.
In 60 seconds
- One-way ANOVA tests H₀: μ₁ = μ₂ = … = μk with a single F-test instead of many t-tests.
- F = MSTR / MSE = (variation between group means) ÷ (variation within groups).
- A significant F says "not all equal"; Tukey's method then says which pairs differ.
- When normality or equal spreads fail, use the rank-based Kruskal–Wallis test.
Why not just run several t-tests?
With three groups you would need 3 pairwise t-tests; with 5 groups, 10. Each at α = 0.05 means the chance of at least one false alarm grows quickly — about 1 − 0.95¹⁰ ≈ 40 % for ten independent tests. ANOVA asks one question with one α.
The idea in one picture
Water depth across Norway's three seas
Does typical water depth at wildcat locations differ between the North Sea, Norwegian Sea and Barents Sea (wells spudded since 2000)?
Water depth at Norwegian wildcats since 2000
Metres · dots = group mean
Sums of squares and the ANOVA table
Treatment (between) SS
df = k - 1
Error (within) SS
df = n - k
F-statistic
One-way ANOVA identity: SST = SSTR + SSE.
| Source | df | SS | MS = SS/df | F | p |
|---|
F = 147.0, p < 0.0001: mean water depth differs between the areas. The share of total variation explained by area, η² = SSTR/SST = 28.4%, is ANOVA's equivalent of R².
Checking the assumptions
- Independent simple random samples from each population.
- Normal populations — check with normal probability plots of the residuals (value minus its group mean).
- Equal standard deviations — rule of 2: OK if the largest sample SD is at most twice the smallest.
Assumption check failed
The Norwegian Sea mixes shelf locations with a few ultra-deep slope wells (over 1,000 m), giving it an SD three times the others and a strongly skewed distribution. Classic ANOVA is fairly robust to unequal SDs when group sizes are similar, but here they are not (435 vs 107). The robust alternative below is the safer evidence.
Which groups differ? Tukey multiple comparisons
A significant F does not say which means differ. Tukey's method builds confidence intervals for every pair of means while holding the family confidence level (all intervals correct simultaneously) at 95 %, using the studentised range (q) distribution. Each individual interval is therefore slightly wider than a stand-alone t-interval.
| Comparison | Difference in mean depth (m) | 95 % family CI | Differ? |
|---|
All three pairs differ: the North Sea is shallowest, the Barents Sea in between, the Norwegian Sea deepest on average.
Kruskal–Wallis: the rank-based alternative
The Kruskal–Wallis test ranks all observations together and checks whether the average rank differs between groups. It needs only that the distributions have the same shape — no normality. Here H = 258.2 (compared with χ² on 2 df), p < 0.0001: the conclusion survives without the normality assumption.
Kruskal–Wallis statistic
R_j = sum of the ranks in group j. Under H₀, H \approx \chi^2_{k-1} (each n_j \ge 5).
A second example: refinery regions
Mean weekly utilisation in 2023 by PADD region: . One-way ANOVA: F = 12.1, p < 0.0001. But the five regions are measured in the same 52 weeks — a hurricane week hits several regions at once — so the samples are not independent. A design that pairs regions by week (a "randomized block" analysis, the multi-group cousin of the paired t-test) is the correct tool; one-way ANOVA here is only descriptive.
Do it in code
w = pd.read_sql("select main_area, water_depth_m from v_wildcats where entry_year >= 2000 and water_depth_m is not null", con)
groups = [g.water_depth_m for _, g in w.groupby("main_area")]
stats.f_oneway(*groups) # F = 147.0, p ≈ 0
stats.kruskal(*groups) # H = 258.2
model = smf.ols("water_depth_m ~ C(main_area)", data=w).fit()
sm.stats.anova_lm(model) # the ANOVA table
from statsmodels.stats.multicomp import pairwise_tukeyhsd
pairwise_tukeyhsd(w.water_depth_m, w.main_area)
with g as (select main_area, count(*) n, avg(water_depth_m) m, var_samp(water_depth_m) v
from v_wildcats where entry_year >= 2000 and water_depth_m is not null
group by main_area),
o as (select sum(n * m) / sum(n) as grand from g)
select sum(n * (m - grand)^2) as sstr,
sum((n - 1) * v) as sse,
(sum(n * (m - grand)^2) / (count(*) - 1))
/ (sum((n - 1) * v) / (sum(n) - count(*))) as f
from g, o;
Key takeaways
- ANOVA compares between-group to within-group variation with one F-test.
- SST = SSTR + SSE; F = MSTR/MSE with (k − 1, n − k) df; η² measures effect size.
- Check independence, normality and the rule of 2; switch to Kruskal–Wallis when they fail.
- Follow a significant F with Tukey's family-wise intervals.
Self-check
1 · Degrees of freedom
Four groups with 12, 15, 9 and 14 observations. df for the F-test?
k − 1 = 3 and n − k = 50 − 4 = 46: F(3, 46).
2 · Read the table
If SSTR = 120, SSE = 480, k = 3 and n = 63, what is F?
MSTR = 120/2 = 60; MSE = 480/60 = 8; F = 7.5.
3 · Rule of 2
Group SDs 116, 117 and 353. Can you use classic one-way ANOVA?
353/116 ≈ 3.0 > 2, so the equal-SD assumption is doubtful. Use Kruskal–Wallis (or Welch's ANOVA), and look at why one group is so variable.