EZ Statistics

Two-Way ANOVA

Calculator

2. Select Columns & Options

Learn More

Two-Way ANOVA

Definition

Two-Way ANOVA examines the influence of two categorical independent variables on one continuous dependent variable. It tests for main effects of each factor and their interaction effect. It helps determine if there are significant differences between group means in a dataset.
  • Factors: The independent categorical variables.
  • Levels: The groups or categories within each factor.
  • Interaction: Determines whether the effect of one factor depends on the level of the other factor.

Formulas

Total Sum of Squares Decomposition:

SSTotal=SSFactorA+SSFactorB+SSInteraction+SSErrorSS_{Total} = SS_{FactorA} + SS_{FactorB} + SS_{Interaction} + SS_{Error}

SSTotal=i=1aj=1b(XijXˉ)2SS_{Total} = \sum_{i=1}^{a} \sum_{j=1}^{b} (X_{ij} - \bar{X})^2where Xˉ\bar{X} is the grand mean

SSFactorA=bi=1a(Xˉi.Xˉ)2SS_{FactorA} = b \sum_{i=1}^{a} (\bar{X}_{i.} - \bar{X})^2whereXˉi.\bar{X}_{i.} is the mean of level ii of Factor A, and bb is the number of levels in Factor B.

SSFactorB=aj=1b(Xˉ.jXˉ)2SS_{FactorB} = a \sum_{j=1}^{b} (\bar{X}_{.j} - \bar{X})^2where Xˉ.j\bar{X}_{.j} is the mean of level $j$ of Factor B, and $a$ is the number of levels in Factor A.

SSInteraction=i=1aj=1b(XˉijXˉi.Xˉ.j+Xˉ)2SS_{Interaction} = \sum_{i=1}^{a} \sum_{j=1}^{b} (\bar{X}_{ij} - \bar{X}_{i.} - \bar{X}_{.j} + \bar{X})^2

Where:

  • SSFactorASS_{FactorA} = Sum of Squares for Factor A, df=a1df = a - 1 where aa is the number of levels in Factor A
  • SSFactorBSS_{FactorB} = Sum of Squares for Factor B, df=b1df = b - 1 where bb is the number of levels in Factor B
  • SSInteractionSS_{Interaction} = Sum of Squares for interaction with df=(a1)(b1)df = (a - 1)(b - 1)
  • SSErrorSS_{Error} = Residual Sum of Squares, df=Nabdf = N - a*b

Mean Square:

MS=SSdfMS = \frac{SS}{df}

F-Statistic for each factor:

fFactor=MSFactorMSErrorf_{Factor} = \frac{MS_{Factor}}{MS_{Error}}

F-statistics are calculated separately for each factor and interaction effect

Key Assumptions

Independence: Observations must be independent
Normality: Residuals should be normally distributed
Homoscedasticity: Equal variances across groups

Practical Example

Step 1: State the Data

Weight loss study examining effects of diet and exercise:

Raw Data:
DietExerciseWeight Loss (pounds)
Low-fatYes8, 10, 9
Low-fatNo6, 7, 8
High-fatYes5, 7, 6
High-fatNo3, 4, 5
Summary Statistics:
DietExerciseMeanN
Low-fatYes9.003
Low-fatNo7.003
High-fatYes6.003
High-fatNo4.003
Step 2: State Hypotheses

Main Effects:

  • Diet: H0:α1=α2=0H_0: \alpha_1 = \alpha_2 = 0
  • Exercise: H0:β1=β2=0H_0: \beta_1 = \beta_2 = 0

Interaction:

  • H0:(αβ)ij=0H_0: (\alpha\beta)_{ij} = 0 for all i,ji,j
Step 3: Calculate Test Statistics
SourcedfSSMSFp-value
Diet127.027.027.00.000826
Exercise112.012.012.00.008516
Diet:Exercise10.00.00.01.0000
Residuals881
Step 4: Draw Conclusions
  • Significant main effect of Diet (p = 0.000826)
  • Significant main effect of Exercise (p = 0.008516)
  • No significant interaction effect (p = 1.0000)
  • Diet and Exercise appear to have a significant effect on weight loss at α=0.05\alpha = 0.05
  • The interaction between Diet and Exercise are not statistically significant

Effect Size

Partial Eta-squared:

ηp2=SSFactorSSFactor+SSError\eta^2_p = \frac{SS_{Factor}}{SS_{Factor} + SS_{Error}}

For the example above,

  • Diet: ηp2=2727+8=0.77\eta^2_p = \frac{27}{27+8} = 0.77 (large effect)
  • Exercise: ηp2=1212+8=0.60\eta^2_p = \frac{12}{12+8} = 0.60 (large effect)
  • Interaction: ηp2=0.00\eta^2_p = 0.00 (no effect)

Code Examples

R
1# Create the data
2library(tidyverse)
3
4data <- tibble(
5  Diet = factor(rep(c("Low-fat", "High-fat"), each = 6)),
6  Exercise = factor(rep(c("Yes", "No"), each = 3, times = 2)),
7  WeightLoss = c(8, 10, 9, 6, 7, 8, 5, 7, 6, 3, 4, 5)
8)
9
10# Perform two-way ANOVA
11model <- aov(WeightLoss ~ Diet * Exercise, data = data)
12
13# Get summary
14summary(model)
15
16
17#------ Manual calculations ------#
18# Compute the grand mean
19grand_mean <- data |>
20  summarize(grand_mean = mean(WeightLoss)) |>
21  pull(grand_mean)
22
23# Compute SS Total
24ss_total <- data |>
25  summarize(ss_total = sum((WeightLoss - grand_mean)^2)) |>
26  pull(ss_total)
27
28# Compute SS for Diet
29ss_diet <- data |>
30  group_by(Diet) |>
31  summarize(group_mean = mean(WeightLoss), n = n()) |>
32  ungroup() |>
33  summarize(ss_diet = sum((group_mean - grand_mean)^2 * n)) |>
34  pull(ss_diet)
35
36# Compute SS for Exercise
37ss_exercise <- data |>
38  group_by(Exercise) |>
39  summarize(group_mean = mean(WeightLoss), n = n()) |>
40  ungroup() |>
41  summarize(ss_exercise = sum((group_mean - grand_mean)^2 * n)) |>
42  pull(ss_exercise)
43
44# Compute SS Interaction
45ss_interaction <- data |>
46  group_by(Diet, Exercise) |>
47  mutate(group_mean = mean(WeightLoss)) |>
48  ungroup() |>
49  group_by(Diet) |>
50  mutate(diet_mean = mean(WeightLoss)) |>
51  ungroup() |>
52  group_by(Exercise) |>
53  mutate(exercise_mean = mean(WeightLoss)) |>
54  ungroup() |>
55  mutate(interaction_term = (group_mean - diet_mean - exercise_mean + grand_mean)^2) |>
56  summarize(ss_interaction = sum(interaction_term)) |>
57  pull(ss_interaction)
58
59ss_error <- ss_total - ss_diet - ss_exercise - ss_interaction
60
61print(str_glue("SS total: {ss_total}"))
62print(str_glue("SS diet: {ss_diet}"))
63print(str_glue("SS exercise: {ss_exercise}"))
64print(str_glue("SS interaction: {ss_interaction}"))
65print(str_glue("SS error: {ss_error}"))
66
67ms_diet = ss_diet / (2 - 1)
68ms_exercise = ss_exercise / (2 - 1)
69ms_error = ss_error / (2 * 2 * (3 - 1))
70
71f_diet = ms_diet / ms_error
72f_exercise = ms_exercise / ms_error
73print(str_glue("F Diet: {f_diet}"))
74print(str_glue("F Exercise: {f_exercise}"))
Python
1import pandas as pd
2import numpy as np
3from scipy import stats
4
5# Create the data
6data = pd.DataFrame({
7    'Diet': pd.Categorical(np.repeat(['Low-fat', 'High-fat'], 6)),
8    'Exercise': pd.Categorical(np.tile(np.repeat(['Yes', 'No'], 3), 2)),
9    'WeightLoss': [8, 10, 9, 6, 7, 8, 5, 7, 6, 3, 4, 5]
10})
11
12# Using statsmodels for ANOVA
13import statsmodels.api as sm
14from statsmodels.stats.anova import anova_lm
15
16# Fit the model using statsmodels
17model = sm.OLS.from_formula('WeightLoss ~ Diet + Exercise + Diet:Exercise', data=data)
18fit = model.fit()
19anova_table = anova_lm(fit, typ=2)
20print("ANOVA results from statsmodels:")
21print(anova_table)
22
23#------ Manual calculations ------#
24grand_mean = data['WeightLoss'].mean()
25ss_total = np.sum((data['WeightLoss'] - grand_mean) ** 2)
26
27# Compute SS for Diet
28diet_means = data.groupby('Diet', observed=True)['WeightLoss'].agg(['mean', 'size']).reset_index()
29ss_diet = np.sum((diet_means['mean'] - grand_mean) ** 2 * diet_means['size'])
30
31# Compute SS for Exercise
32exercise_means = data.groupby('Exercise', observed=True)['WeightLoss'].agg(['mean', 'size']).reset_index()
33ss_exercise = np.sum((exercise_means['mean'] - grand_mean) ** 2 * exercise_means['size'])
34
35# Compute SS Interaction
36cell_means = data.groupby(['Diet', 'Exercise'], observed=True)['WeightLoss'].mean().reset_index()
37cell_means = cell_means.merge(
38    data.groupby('Diet', observed=True)['WeightLoss'].mean().reset_index().rename(columns={'WeightLoss': 'diet_mean'}),
39    on='Diet'
40)
41cell_means = cell_means.merge(
42    data.groupby('Exercise', observed=True)['WeightLoss'].mean().reset_index().rename(columns={'WeightLoss': 'exercise_mean'}),
43    on='Exercise'
44)
45
46cell_means['interaction_term'] = (
47    (cell_means['WeightLoss'] - cell_means['diet_mean'] - 
48     cell_means['exercise_mean'] + grand_mean) ** 2
49)
50ss_interaction = cell_means['interaction_term'].sum()
51
52ss_error = ss_total - ss_diet - ss_exercise - ss_interaction
53
54# Calculate Mean Squares
55df_diet = len(data['Diet'].unique()) - 1
56df_exercise = len(data['Exercise'].unique()) - 1
57df_interaction = df_diet * df_exercise
58df_error = len(data) - (df_diet + 1) * (df_exercise + 1)
59
60ms_diet = ss_diet / df_diet
61ms_exercise = ss_exercise / df_exercise
62ms_error = ss_error / df_error
63
64# Calculate F statistics and p-values
65f_diet = ms_diet / ms_error
66f_exercise = ms_exercise / ms_error
67p_diet = 1 - stats.f.cdf(f_diet, df_diet, df_error)
68p_exercise = 1 - stats.f.cdf(f_exercise, df_exercise, df_error)
69
70# Create ANOVA table
71anova_manual = pd.DataFrame({
72    'df': [df_diet, df_exercise, df_interaction, df_error],
73    'sum_sq': [ss_diet, ss_exercise, ss_interaction, ss_error],
74    'mean_sq': [ms_diet, ms_exercise, ss_interaction/df_interaction, ms_error],
75    'F': [f_diet, f_exercise, (ss_interaction/df_interaction)/ms_error, np.nan],
76    'PR(>F)': [p_diet, p_exercise, 
77             1 - stats.f.cdf((ss_interaction/df_interaction)/ms_error, df_interaction, df_error), 
78             np.nan]
79}, index=['Diet', 'Exercise', 'Diet:Exercise', 'Residuals'])
80
81print("ANOVA Table:")
82print(anova_manual.round(4))

Alternative Tests

When assumptions are violated:

  • Aligned Rank Transform ANOVA: For non-normal data
  • Scheirer-Ray-Hare Test: Non-parametric alternative

Related Calculators

One-Way ANOVA Calculator

Independent T Test Calculator

Repeated Measures ANOVA Calculator

ANCOVA Calculator

Help us improve

Found an error or have a suggestion? Let us know!