Why ANOVA Instead of Multiple T-Tests
Why ANOVA Instead of Multiple T-Tests
When comparing means across three or more groups, the temptation is to run pairwise t-tests. This approach is wrong, and understanding why is essential to grasping what ANOVA actually does.
The Multiple Comparisons Problem
Suppose you have four groups (A, B, C, D) and you run all six pairwise t-tests. Each test at α = 0.05 has a 5% chance of a false positive. The probability of at least one false positive across all six tests is:
P(at least one false positive) = 1 - (1 - 0.05)^6 = 1 - 0.735 = 0.265
A 26.5% false positive rate is unacceptable. This is the multiple comparisons problem: the more tests you run, the more likely you are to find a "significant" result by chance alone.
You could apply a Bonferroni correction (dividing α by the number of tests), but this dramatically reduces power. With six tests, each would need p < 0.0083 to be significant. Real effects get missed.
What ANOVA Does Differently
ANOVA (Analysis of Variance) tests all groups simultaneously with a single test. It asks: "Is there any difference among the group means?" rather than "Is group A different from group B?"
H₀: μ_A = μ_B = μ_C = μ_D (all group means are equal)
H₁: At least one mean differs (not all means are equal)
This single test maintains the nominal α level. If the overall test is significant, you then use post-hoc tests (with appropriate corrections) to identify which specific groups differ.
The Variance Decomposition
ANOVA's name comes from its approach: it analyzes variance. The total variability in the data is split into two components:
Total variance = Between-group variance + Within-group variance
- Between-group variance measures how much the group means differ from the grand mean. Large between-group variance suggests the groups are genuinely different.
- Within-group variance measures how much individual observations vary within each group. This is the noise or error term.
The F-statistic is the ratio:
F = Between-group variance / Within-group variance
If the groups truly differ, between-group variance will be large relative to within-group variance, producing a large F-statistic. If the groups are identical, both variance components estimate the same underlying noise, and F will be close to 1.
A Concrete Example
You test three onboarding flows and measure completion rates:
Flow A: [28, 32, 30, 29, 31, 27, 33, 30, 28, 31] mean = 30.0
Flow B: [35, 38, 36, 34, 37, 35, 39, 36, 35, 37] mean = 36.2
Flow C: [31, 33, 32, 30, 34, 31, 35, 32, 31, 33] mean = 32.2
The grand mean is 32.8. Between-group variance captures how far each group mean (30.0, 36.2, 32.8) is from 32.8. Within-group variance captures the spread of individual observations around their group means.
from scipy import stats
import numpy as np
flow_a = np.array([28, 32, 30, 29, 31, 27, 33, 30, 28, 31])
flow_b = np.array([35, 38, 36, 34, 37, 35, 39, 36, 35, 37])
flow_c = np.array([31, 33, 32, 30, 34, 31, 35, 32, 31, 33])
f_stat, p_value = stats.f_oneway(flow_a, flow_b, flow_c)
print(f"F-statistic: {f_stat:.3f}")
print(f"p-value: {p_value:.6f}")
The large F-statistic and tiny p-value indicate that at least one flow differs from the others. Post-hoc tests will identify which ones.
The F-Statistic and ANOVA Table
The F-Statistic and ANOVA Table
The F-statistic follows an F-distribution under the null hypothesis. The F-distribution has two parameters: degrees of freedom for the numerator (between groups) and degrees of freedom for the denominator (within groups).
Degrees of Freedom
For one-way ANOVA with k groups and N total observations:
Between-group df = k - 1
Within-group df = N - k
Total df = N - 1
With 3 groups and 30 total observations: between-group df = 2, within-group df = 27.
The ANOVA Table
The results are typically presented in an ANOVA table:
Source | SS | df | MS | F | p
----------------|-------|----|-------|-------|------
Between groups | 192.4 | 2 | 96.2 | 45.8 | <0.001
Within groups | 56.7 | 27 | 2.1 | |
Total | 249.1 | 29 | | |
Where:
- SS (Sum of Squares) measures total variation
- df (degrees of freedom) counts independent pieces of information
- MS (Mean Square) = SS / df, the variance estimate
- F = MS_between / MS_within
Computing ANOVA Manually
import numpy as np
from scipy import stats
groups = [flow_a, flow_b, flow_c]
k = len(groups)
N = sum(len(g) for g in groups)
grand_mean = np.mean(np.concatenate(groups))
# Between-group sum of squares
ss_between = sum(len(g) * (g.mean() - grand_mean)**2 for g in groups)
# Within-group sum of squares
ss_within = sum(((g - g.mean())**2).sum() for g in groups)
# Mean squares
ms_between = ss_between / (k - 1)
ms_within = ss_within / (N - k)
# F-statistic
f_stat = ms_between / ms_within
p_value = 1 - stats.f.cdf(f_stat, k-1, N-k)
print(f"F({k-1}, {N-k}) = {f_stat:.3f}, p = {p_value:.6f}")
Interpreting the F-Statistic
The F-statistic is a ratio of variances. Under H₀, both variance components estimate the same population variance, so F ≈ 1. Under H₁, between-group variance includes the true effect plus noise, making F > 1.
The p-value comes from the F-distribution. Large F-values are in the right tail, which is the rejection region. The F-distribution is asymmetric (bounded at 0, long right tail), so there is only one rejection region.
Effect Size: Eta-Squared
ANOVA's effect size is eta-squared (η²), the proportion of total variance explained by group membership:
η² = SS_between / SS_total
eta_squared = ss_between / (ss_between + ss_within)
print(f"η² = {eta_squared:.3f}")
Interpretation:
- η² ≈ 0.01: small effect
- η² ≈ 0.06: medium effect
- η² ≈ 0.14: large effect
A significant F-test with a small η² means the groups differ statistically but the differences may not be practically important. Always report η² alongside the p-value.
One-Way vs. Two-Way ANOVA
One-way ANOVA has one independent variable (factor) with multiple levels. Two-way ANOVA has two factors and can test interactions between them.
For example, if you test both the button color (green, blue) and the page layout (compact, spacious), a two-way ANOVA can detect:
- Main effect of color
- Main effect of layout
- Interaction effect (color works differently depending on layout)
Interaction effects are often the most interesting findings in product experiments. A feature might help on desktop but hurt on mobile. One-way ANOVA would average these effects and miss the pattern.
Post-Hoc Tests
Post-Hoc Tests
A significant ANOVA result tells you that at least one group differs, but not which ones. Post-hoc tests (Latin for "after this") identify the specific pairs that differ while controlling the overall false positive rate.
Why Not Just Run T-Tests?
Running uncorrected pairwise t-tests after a significant ANOVA still inflates the false positive rate. Post-hoc tests adjust the p-values or critical values to account for the number of comparisons.
Tukey's HSD (Honestly Significant Difference)
Tukey's HSD is the most common post-hoc test for all pairwise comparisons. It controls the family-wise error rate (FWER) — the probability of making at least one false positive across all comparisons.
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
import numpy as np
# Combine data with group labels
all_data = np.concatenate([flow_a, flow_b, flow_c])
groups = ['A']*len(flow_a) + ['B']*len(flow_b) + ['C']*len(flow_c)
tukey = pairwise_tukeyhsd(all_data, groups, alpha=0.05)
print(tukey)
Output:
Multiple Comparison of Means - Tukey HSD
===================================================
group1 group2 meandiff p-adj lower upper reject
---------------------------------------------------
A B 6.20 0.001 3.42 8.98 True
A C 2.20 0.041 0.04 4.36 True
B C -4.00 0.008 -6.58 -1.42 True
---------------------------------------------------
The p-adj column shows adjusted p-values. The reject column indicates whether the difference is significant after correction.
Bonferroni Correction
The Bonferroni correction divides α by the number of comparisons:
α_adjusted = α / m
Where m is the number of pairwise comparisons. For 3 groups, m = 3, so α_adjusted = 0.05/3 ≈ 0.017.
from scipy import stats
import numpy as np
from itertools import combinations
group_dict = {'A': flow_a, 'B': flow_b, 'C': flow_c}
pairs = list(combinations(group_dict.keys(), 2))
for g1, g2 in pairs:
t_stat, p_value = stats.ttest_ind(group_dict[g1], group_dict[g2])
p_adjusted = min(p_value * len(pairs), 1.0) # Bonferroni
print(f"{g1} vs {g2}: p = {p_value:.4f}, p-adj = {p_adjusted:.4f}")
Bonferroni is simple but conservative. It controls FWER strictly, which means it can miss real effects (low power). Use it when false positives are very costly.
Dunnett's Test
When comparing multiple treatment groups against a single control, Dunnett's test is more powerful than Tukey's because it only makes k-1 comparisons instead of k(k-1)/2.
from statsmodels.stats.multicomp import MultiComparison
mc = MultiComparison(all_data, groups)
result = mc.allpairtest(stats.ttest_ind, method='bonferroni')
print(result[0])
Choosing a Post-Hoc Test
| Test | Comparisons | Control | Power | Use When |
|---|---|---|---|---|
| Tukey's HSD | All pairs | FWER | Moderate | General pairwise comparisons |
| Bonferroni | All pairs | FWER | Low | Few comparisons, strict control needed |
| Dunnett | vs. control only | FWER | High | One control, multiple treatments |
| Scheffé | All contrasts | FWER | Low | Complex contrasts, unplanned comparisons |
Interpreting Post-Hoc Results
Report the post-hoc results alongside the ANOVA:
ANOVA: F(2, 27) = 45.8, p < 0.001, η² = 0.77
Post-hoc (Tukey's HSD, α = 0.05):
- Flow B > Flow A: diff = 6.2, p-adj = 0.001
- Flow B > Flow C: diff = 4.0, p-adj = 0.008
- Flow C > Flow A: diff = 2.2, p-adj = 0.041
All three flows differ significantly. Flow B performs best. The effect size (η² = 0.77) indicates that flow type explains 77% of the variance in completion rates — a very large effect.
Assumptions and Violations
Assumptions and Violations
ANOVA relies on the same assumptions as the t-test, plus one additional requirement. Violations can distort p-values and lead to incorrect conclusions.
The Assumptions
Independence. Observations within and across groups are independent. Violated by repeated measures, clustered data, or temporal dependencies.
Normality. The residuals (observations minus group means) are approximately normally distributed. ANOVA is robust to mild violations with balanced designs and moderate sample sizes.
Homogeneity of variances (homoscedasticity). All groups have approximately equal population variances. This is the assumption most likely to be violated in practice.
Checking Assumptions
from scipy import stats
import numpy as np
# Normality of residuals
all_data = np.concatenate([flow_a, flow_b, flow_c])
group_means = np.repeat([g.mean() for g in [flow_a, flow_b, flow_c]], [len(flow_a), len(flow_b), len(flow_c)])
residuals = all_data - group_means
stat, p_norm = stats.shapiro(residuals)
print(f"Shapiro-Wilk (residuals): p = {p_norm:.4f}")
# Homogeneity of variances
stat, p_var = stats.levene(flow_a, flow_b, flow_c)
print(f"Levene's test: p = {p_var:.4f}")
Handling Violated Assumptions
Non-normality:
- With large samples (n > 30 per group), ANOVA is robust due to the CLT
- For small samples with skewness, use the Kruskal-Wallis test (non-parametric alternative)
- Transform the data (log, square root) to reduce skewness
Unequal variances:
- Use Welch's ANOVA, which does not assume equal variances
- Apply a variance-stabilizing transformation
- Use the Brown-Forsythe test
# Kruskal-Wallis (non-parametric ANOVA)
h_stat, p_value = stats.kruskal(flow_a, flow_b, flow_c)
print(f"Kruskal-Wallis: H = {h_stat:.3f}, p = {p_value:.6f}")
Welch's ANOVA
Standard ANOVA pools variances across groups. When group variances differ substantially, this pooling produces an inaccurate F-statistic. Welch's ANOVA uses weighted variances and adjusts degrees of freedom.
import pingouin as pg
import pandas as pd
df = pd.DataFrame({
'value': np.concatenate([flow_a, flow_b, flow_c]),
'group': ['A']*len(flow_a) + ['B']*len(flow_b) + ['C']*len(flow_c)
})
result = pg.welch_anova(dv='value', between='group', data=df)
print(result)
Balanced vs. Unbalanced Designs
A balanced design has equal sample sizes in all groups. ANOVA is most robust when designs are balanced. With unbalanced designs:
- Type I error rates can inflate if variances are unequal
- Type II error rates increase
- Consider weighted least squares or mixed-effects models
Outliers
Outliers have a disproportionate influence on ANOVA because they inflate within-group variance. This reduces the F-statistic and can mask real effects.
Check for outliers visually (boxplots) and statistically (Grubbs' test, IQR method). If you find outliers:
- Verify they are data entry errors
- Consider robust methods (trimmed means, bootstrap ANOVA)
- Report results with and without outliers
A Complete ANOVA Workflow
from scipy import stats
import numpy as np
# 1. Check assumptions
# 2. Run ANOVA
f_stat, p_value = stats.f_oneway(flow_a, flow_b, flow_c)
# 3. If significant, run post-hoc tests
if p_value < 0.05:
from statsmodels.stats.multicomp import pairwise_tukeyhsd
all_data = np.concatenate([flow_a, flow_b, flow_c])
groups = ['A']*len(flow_a) + ['B']*len(flow_b) + ['C']*len(flow_c)
tukey = pairwise_tukeyhsd(all_data, groups, alpha=0.05)
print(tukey)
# 4. Report effect size
# 5. Interpret results in context
Always follow this sequence. Running post-hoc tests when the overall ANOVA is not significant is a mistake — it inflates the false positive rate without the protection of the omnibus test.
Practice Problems
You have 5 groups and want to compare all pairwise means. If you run 10 t-tests at α = 0.05, what is the family-wise error rate? How does ANOVA solve this problem?
Solution
alpha = 0.05
m = 10
fwer = 1 - (1 - alpha)**m
fwer = 1 - (0.95)**10 = 1 - 0.599 = 0.401
The family-wise error rate is approximately 40%. This means there is a 40% chance of at least one false positive across the 10 tests, compared to the nominal 5%.
ANOVA solves this by testing all groups simultaneously with a single F-test at α = 0.05. The omnibus test controls the error rate at 5%. If the F-test is significant, post-hoc tests (Tukey's HSD, Bonferroni) identify which pairs differ while maintaining control over the family-wise error rate. Three groups have the following data: Group A [10, 12, 11, 13, 12], Group B [15, 17, 16, 18, 17], Group C [12, 14, 13, 15, 14]. Compute the F-statistic, p-value, and identify which groups differ using Tukey's HSD.
Solution
from scipy import stats
import numpy as np
from statsmodels.stats.multicomp import pairwise_tukeyhsd
group_a = np.array([10, 12, 11, 13, 12])
group_b = np.array([15, 17, 16, 18, 17])
group_c = np.array([12, 14, 13, 15, 14])
f_stat, p_value = stats.f_oneway(group_a, group_b, group_c)
print(f"F(2, 12) = {f_stat:.3f}, p = {p_value:.6f}")
# F(2, 12) = 45.0, p < 0.001
# Since p < 0.05, run Tukey's HSD
all_data = np.concatenate([group_a, group_b, group_c])
groups = ['A']*5 + ['B']*5 + ['C']*5
tukey = pairwise_tukeyhsd(all_data, groups, alpha=0.05)
print(tukey)
# Results:
# B vs A: diff = 5.0, p-adj < 0.001, reject = True
# C vs A: diff = 2.0, p-adj = 0.012, reject = True
# C vs B: diff = -3.0, p-adj = 0.001, reject = True
All three groups differ significantly. Group B has the highest mean (16.6), followed by Group C (13.6), then Group A (11.6). Group A [20, 22, 21, 23, 22] has variance 1.3. Group B [10, 30, 15, 35, 20] has variance 92.5. Why is standard ANOVA problematic here, and what alternative would you use?
Solution
Standard ANOVA assumes homogeneity of variances — all groups have approximately equal population variances. Here, Group A has variance 1.3 and Group B has variance 92.5, a ratio of about 71:1. This severe violation means the pooled variance estimate is dominated by Group B, making the F-statistic unreliable.
The appropriate alternative is Welch's ANOVA, which:
1. Uses separate variance estimates for each group instead of pooling
2. Adjusts the degrees of freedom using the Welch-Satterthwaite equation
3. Provides valid p-values regardless of variance equality
```python
import pingouin as pg
import pandas as pd
df = pd.DataFrame({
'value': [20, 22, 21, 23, 22, 10, 30, 15, 35, 20],
'group': ['A']*5 + ['B']*5
})
result = pg.welch_anova(dv='value', between='group', data=df)
print(result)
```
Welch's ANOVA is the safer default when group variances are unknown or suspected to differ. Quiz
1. Why is ANOVA preferred over multiple t-tests when comparing 3+ groups?
2. What does a significant ANOVA F-test tell you?
3. When should you run post-hoc tests after ANOVA?
Flashcards
Question
What is the multiple comparisons problem?
Click to reveal answer
Answer
When running multiple hypothesis tests, the probability of at least one false positive increases with the number of tests. With m tests at α = 0.05, FWER = 1 - (1-α)^m. For 10 tests, FWER ≈ 0.40, not 0.05.
Question
What does the F-statistic in ANOVA represent?
Click to reveal answer
Answer
F = between-group variance / within-group variance. Under H₀, both estimate the same noise, so F ≈ 1. Under H₁, between-group variance includes the true effect, making F > 1. Large F-values indicate group differences.
Question
What is the difference between Tukey's HSD and Bonferroni correction?
Click to reveal answer
Answer
Tukey's HSD is designed specifically for all pairwise comparisons after ANOVA and is more powerful than Bonferroni. Bonferroni divides α by the number of tests and is more conservative but works for any set of comparisons.
Question
What is eta-squared (η²) and how is it interpreted?
Click to reveal answer
Answer
η² = SS_between / SS_total, the proportion of total variance explained by group membership. Guidelines: 0.01 = small, 0.06 = medium, 0.14 = large. It complements the p-value by quantifying practical significance.
Revision Notes
Key Takeaways
- 1. ANOVA replaces multiple t-tests to control the family-wise error rate
- 2. The F-statistic is a ratio of between-group to within-group variance
- 3. A significant F-test means at least one group differs, not which ones
- 4. Post-hoc tests (Tukey's HSD, Bonferroni) identify specific differences
- 5. Eta-squared quantifies the proportion of variance explained by group membership
- 6. Check assumptions: independence, normality, homogeneity of variances
- 7. Use Welch's ANOVA when group variances are unequal
- 8. Only run post-hoc tests when the omnibus F-test is significant
Interview Tips
- • Explain why multiple t-tests are inferior to ANOVA using the FWER calculation
- • Describe the variance decomposition: total = between + within
- • Discuss when to use different post-hoc tests and their power tradeoffs
- • Mention non-parametric alternatives (Kruskal-Wallis) for violated assumptions
Cheat Sheet
Cheat Sheet: ANOVA for Multiple Groups
When to Use ANOVA
- Comparing means across 3+ groups
- Single categorical independent variable (one-way)
- Two categorical variables (two-way, with interactions)
The F-Statistic
F = MS_between / MS_within
MS = SS / df
ANOVA Table
| Source | SS | df | MS | F |
|---|---|---|---|---|
| Between | SSB | k-1 | SSB/(k-1) | MSB/MSW |
| Within | SSW | N-k | SSW/(N-k) | |
| Total | SST | N-1 |
Post-Hoc Tests
- Tukey's HSD: all pairwise, moderate power
- Bonferroni: all pairwise, conservative
- Dunnett: vs. control only, high power
- Scheffé: complex contrasts, most conservative
Assumptions
- Independence
- Normality of residuals
- Homogeneity of variances
Violations
- Non-normality → Kruskal-Wallis
- Unequal variances → Welch's ANOVA
- Outliers → robust methods, transformations