Two-Way Repeated Measures ANOVA
The engine for Dual-Factor Within-Subject Discovery. This model audits the synergistic interaction between two repeated measures factors (e.g., Time x Condition) within the same group.
What is it?
Two-Way Repeated Measures ANOVA assesses the impact of two independent factors on a continuous outcome, where all combinations of factors are measured on the same subjects.
When to use it
- 2 Within Factors: (e.g., Time [Pre/Post] and Drug [A/B]).
- Outcome: Continuous scale variable.
- Single Group: Every subject goes through all 4 test cells.
Core Idea
We map how subjects react to both factors simultaneously. For example, does active drug reduce scores over time, while placebo does not?
This design has extreme statistical power because it controls for both participant baseline and general temporal trends.
Hypotheses
How it works
Calculates distinct error terms for each test:
- Time tested against $Time \times Subjects$
- Drug tested against $Drug \times Subjects$
- Interaction tested against $Time \times Drug \times Subjects$
Assumptions
Important Note
Carryover effects (fatigue, learning) are the biggest threat. They must be controlled by counterbalancing or randomized scheduling of conditions.
Quick Example
| Subj / time | Active Pre | Active Post | Placebo Pre |
|---|---|---|---|
| Subj 1 | 52.3 | 81.0 | 50.1 |
| Subj 2 | 41.4 | 58.2 | 42.8 |
Two-Way RM ANOVA Live Laboratory
Adjust Time, Treatment, and Interaction values to observe how repeated measures handle multi-factor data.
| Source | SS | df | F | p-value |
|---|---|---|---|---|
| Factor A (Time) | 0.0 | 1 | 0.00 | 1.0000 |
| Factor B (Drug) | 0.0 | 1 | 0.00 | 1.0000 |
| Interaction (AB) | 0.0 | 1 | 0.00 | 1.0000 |
| Error (Residual) | 525.0 | 21 | - | - |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀ (Main effect A): All means across levels of Factor A are equal, averaging over Factor B. H₀ (Main effect B): All means across levels of Factor B are equal, averaging over Factor A. H₀ (Interaction): No A×B interaction (the effect of Factor A is constant across levels of Factor B, and vice versa).
Hₐ: At least one mean differs for Factor A OR Factor B, OR there is an A×B interaction (effects are not additive—the effect of one factor depends on the level of the other).
Two-way RM-ANOVA tests THREE null hypotheses simultaneously. CRITICAL: Sphericity must be tested separately for EACH within-subjects effect (main effect A, main effect B, and A×B interaction). If sphericity is violated for any effect, apply Greenhouse-Geisser (ε < .75) or Huynh-Feldt (ε > .75) correction to that specific effect's F-test. Interaction interpretation takes precedence—if significant, main effects may be misleading.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Mauchly's test of sphericity for EACH within-subjects effect (Factor A, Factor B, interaction)
- Greenhouse-Geisser and Huynh-Feldt epsilon values to quantify sphericity violations
- Q-Q plots of difference scores for normality assessment
- Boxplots for each Factor A × Factor B cell to identify outliers
- Interaction plot (profile plot) to visualize A×B interaction pattern
- Shapiro-Wilk test on difference scores (if n < 50)
- Variance-covariance matrix inspection for each within-subjects effect
- Residual vs fitted plot from model
- Within-subject trajectory plots (spaghetti plots) to visualize individual patterns
- Descriptive statistics (M, SD, n) for each cell
- Intraclass correlation (ICC) to quantify within-subject correlation
- Simple effects analysis if interaction significant
- Effect size estimates with confidence intervals
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Reaction Time Across Practice Sessions and Task Difficulty (3×2 Design)
Research question: How do practice and task difficulty interact to affect reaction time? Design: 3 Practice Sessions (Session 1, 2, 3) × 2 Task Difficulty levels (Easy, Hard), fully within-subjects (n=35 participants, 6 measurements per participant, total N=210 observations). Outcome: Reaction time in milliseconds (continuous). Hypothesis: Practice reduces RT more for hard tasks (interaction)—skill acquisition more beneficial when task is challenging.
# Two-way Repeated Measures ANOVA: Practice × Difficulty on RT
# 3×2 within-subjects design with sphericity corrections
library(tidyverse)
library(ez) # For ezANOVA
library(rstatix) # For anova_test
library(emmeans) # For simple effects
library(effectsize) # For effect sizes
library(ggpubr) # For plots
set.seed(2025)
# Simulate realistic RT data with practice × difficulty interaction
# Hard task benefits more from practice (steeper slope)
subjects <- 1:35
data_list <- list()
for (subj in subjects) {
# Subject-specific baseline RT (individual differences)
baseline <- rnorm(1, 500, 50)
data_list[[subj]] <- data.frame(
subject = subj,
session = rep(1:3, each=2),
difficulty = rep(c("Easy", "Hard"), 3),
RT = c(
# Session 1
baseline - 50 + rnorm(1, 0, 20), # Easy S1
baseline + 100 + rnorm(1, 0, 30), # Hard S1
# Session 2: practice improves both, more for hard
baseline - 70 + rnorm(1, 0, 20), # Easy S2 (small gain)
baseline + 60 + rnorm(1, 0, 30), # Hard S2 (large gain)
# Session 3: further practice
baseline - 80 + rnorm(1, 0, 20), # Easy S3 (ceiling)
baseline + 30 + rnorm(1, 0, 30) # Hard S3 (continued gain)
)
)
}
data <- bind_rows(data_list) %>%
mutate(
subject = factor(subject),
session = factor(session, levels=1:3, labels=c("Session1", "Session2", "Session3")),
difficulty = factor(difficulty, levels=c("Easy", "Hard"))
)
# === STEP 1: Descriptive Statistics by Cell ===
cat("=== Cell Means(ms) ===\n")
data %>%
group_by(session, difficulty) %>%
summarise(n=n(), M=mean(RT), SD=sd(RT), .groups='drop') %>%
pivot_wider(names_from=difficulty, values_from=c(M, SD)) %>%
print()
# === STEP 2: Visualize Data ===
# Profile plot (interaction plot)
interaction_summary <- data %>%
group_by(session, difficulty) %>%
summarise(M=mean(RT), SE=sd(RT)/sqrt(n()), .groups='drop')
ggplot(interaction_summary, aes(x=session, y=M, color=difficulty, group=difficulty)) +
geom_line(size=1.5) +
geom_point(size=4) +
geom_errorbar(aes(ymin=M-SE, ymax=M+SE), width=0.15) +
labs(title="Practice × Difficulty Interaction on Reaction Time",
subtitle="Hard task benefits more from practice(steeper slope = interaction)",
x="Practice Session", y="Mean RT(ms) ± SE",
color="Task Difficulty") +
scale_color_manual(values=c("Easy"="#00BA38", "Hard"="#F8766D")) +
theme_classic(base_size=14)
# Individual trajectories (spaghetti plot)
ggplot(data, aes(x=session, y=RT, group=interaction(subject, difficulty), color=difficulty)) +
geom_line(alpha=0.3) +
stat_summary(aes(group=difficulty), fun=mean, geom="line", size=2) +
labs(title="Individual RT Trajectories Across Practice",
x="Session", y="Reaction Time(ms)") +
facet_wrap(~difficulty) +
theme_minimal()
# === STEP 3: Check Assumptions ===
# Outliers (boxplots by cell)
ggboxplot(data, x="session", y="RT", color="difficulty",
palette="jco", add="jitter",
title="RT Distribution by Cell(Check Outliers)",
xlab="Session", ylab="RT(ms)")
# Normality of difference scores (key assumption)
# Calculate differences for Session factor
data_wide <- data %>%
pivot_wider(id_cols=c(subject, difficulty),
names_from=session,
values_from=RT)
for (diff_level in c("Easy", "Hard")) {
cat("\n=== Normality Tests: Session differences at", diff_level, "difficulty ===\n")
diff_data <- data_wide %>% filter(difficulty == diff_level)
# S2-S1 difference
diff1 <- diff_data$Session2 - diff_data$Session1
print(shapiro.test(diff1))
# S3-S1 difference
diff2 <- diff_data$Session3 - diff_data$Session1
print(shapiro.test(diff2))
}
# === STEP 4: Two-way RM-ANOVA ===
cat("\n=== Two-way Repeated Measures ANOVA ===\n")
# Using ez::ezANOVA (comprehensive output)
rm_anova <- ezANOVA(
data = data,
dv = RT,
wid = subject,
within = .(session, difficulty),
detailed = TRUE,
type = 3
)
print(rm_anova$ANOVA)
# === STEP 5: Sphericity Tests ===
cat("\n=== Mauchly's Test of Sphericity ===\n")
print(rm_anova$`Mauchly's Test for Sphericity`)
cat("\n=== Sphericity Corrections ===\n")
print(rm_anova$`Sphericity Corrections`)
cat("
=== Interpretation Guide ===
For EACH within-subjects effect(session, difficulty, session:difficulty):
- If Mauchly's p > .05: sphericity met, use uncorrected F-test
- If Mauchly's p < .05 AND GG epsilon < .75: use Greenhouse-Geisser correction
- If Mauchly's p < .05 AND GG epsilon > .75: use Huynh-Feldt correction
Note: Difficulty has only 2 levels, so sphericity is automatically met(no correction needed).
Session has 3 levels, so sphericity can be violated(check epsilon).
Interaction has 3×2=6 cells with(3-1)×(2-1)=2 df, so sphericity can be violated.
")
# === STEP 6: Effect Sizes ===
cat("\n=== Effect Sizes(Generalized η²) ===\n")
print(rm_anova$ANOVA[, c("Effect", "ges")])
# Using rstatix for partial eta squared
rm_anova2 <- anova_test(
data = data,
dv = RT,
wid = subject,
within = c(session, difficulty)
)
cat("\n=== Partial η² ===\n")
print(get_anova_table(rm_anova2))
# === STEP 7: Simple Effects Analysis (if interaction significant) ===
if (rm_anova$ANOVA$p[3] < 0.05) { # Check interaction p-value
cat("\n=== Interaction Significant: Simple Effects Analysis ===\n")
# Fit model for emmeans
model <- lm(RT ~ session * difficulty, data=data)
# Simple effects: Session at each Difficulty level
emm_session <- emmeans(model, ~ session | difficulty)
cat("\nEffect of Practice Session within each Difficulty level:\n")
pairs_session <- pairs(emm_session, adjust="bonferroni")
print(summary(pairs_session))
# Test linear trend for practice effect
cat("\n=== Linear Trend: Practice Effect ===\n")
contrast_linear <- contrast(
emm_session,
list(linear = c(-1, 0, 1)),
by = "difficulty"
)
print(summary(contrast_linear))
# Effect sizes for simple effects
cat("\n=== Effect Sizes: Practice Effect by Difficulty ===\n")
# Easy: Session 3 vs Session 1
easy_s1 <- data %>% filter(difficulty=="Easy", session=="Session1") %>% pull(RT)
easy_s3 <- data %>% filter(difficulty=="Easy", session=="Session3") %>% pull(RT)
d_easy <- effsize::cohen.d(easy_s1, easy_s3, paired=TRUE)$estimate
cat(sprintf("Easy task - Practice effect(S1 vs S3): d = %.2f\n", d_easy))
# Hard: Session 3 vs Session 1
hard_s1 <- data %>% filter(difficulty=="Hard", session=="Session1") %>% pull(RT)
hard_s3 <- data %>% filter(difficulty=="Hard", session=="Session3") %>% pull(RT)
d_hard <- effsize::cohen.d(hard_s1, hard_s3, paired=TRUE)$estimate
cat(sprintf("Hard task - Practice effect(S1 vs S3): d = %.2f\n", d_hard))
cat(sprintf("\n→ Practice effect %.2f times larger for hard task(d=%.2f) vs easy(d=%.2f)\n",
d_hard/d_easy, d_hard, d_easy))
}
# === STEP 8: Visualization (Publication Quality) ===
# Bar plot with error bars
data_summary <- data %>%
group_by(session, difficulty) %>%
summarise(M=mean(RT), SE=sd(RT)/sqrt(n()), .groups='drop')
ggplot(data_summary, aes(x=session, y=M, fill=difficulty)) +
geom_bar(stat="identity", position=position_dodge(0.9), width=0.8) +
geom_errorbar(aes(ymin=M-SE, ymax=M+SE),
position=position_dodge(0.9), width=0.2) +
labs(title="Skill Acquisition: Practice × Difficulty Interaction",
x="Practice Session", y="Mean Reaction Time(ms) ± SE",
fill="Task Difficulty") +
scale_fill_brewer(palette="Set1") +
theme_classic(base_size=14)
# === APA-Style Report ===
cat("
=== APA-STYLE RESULTS ===
A 3×2 within-subjects ANOVA examined the effects of practice session(1, 2, 3)
and task difficulty(Easy, Hard) on reaction time(N = 35 participants, 6
measurements each). Mauchly's test indicated sphericity was met for the session
main effect(p = .18) but violated for the interaction(p = .042, ε = .89).
Huynh-Feldt corrections were applied to the interaction term(ε > .75).
Results revealed significant main effects of session, F(2, 68) = 124.56,
p < .001, partial η² = .79 (very large), and difficulty, F(1, 34) = 89.23,
p < .001, partial η² = .72 (very large). CRITICALLY, there was a significant
Session × Difficulty interaction, F(1.78, 60.52) = 12.45, p < .001 (Huynh-Feldt
corrected), partial η² = .27 (large effect).
Simple effects analysis revealed that practice reduced RT for both difficulty
levels(both p < .001), but the effect was significantly larger for hard tasks.
From Session 1 to Session 3:
- Easy task: 450ms → 420ms (30ms reduction, d = 0.85)
- Hard task: 600ms → 530ms (70ms reduction, d = 1.82)
The practice effect was 2.1 times larger for hard tasks, supporting skill
acquisition theory: practice benefits are greatest when tasks are challenging
and require substantial learning. Findings have implications for training
protocols—allocate more practice time to difficult skills.
")Main effects: Session F(2, 68) = 124.56, p < .001, partial η² = .79; Difficulty F(1, 34) = 89.23, p < .001, partial η² = .72. Interaction: F(1.78, 60.52) = 12.45, p < .001 (Huynh-Feldt corrected due to sphericity violation), partial η² = .27 (large). CRITICAL: Interaction shows practice effect is difficulty-dependent. Hard tasks show 70ms improvement (d=1.82) vs easy tasks 30ms (d=0.85). Non-parallel slopes indicate differential learning rates—challenging tasks benefit more from practice, consistent with skill acquisition theory (Heathcote et al., 2000). Practical implication: training protocols should allocate proportionally more practice time to difficult skills.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Greenhouse-Geisser Shield — Mandatory DF adjustment for both temporal factors and their interaction.
- MANOVA Path — Treat the 4+ cells as a multivariate vector to bypass the sphericity mandate entirely.
- Bootstrap Interaction Strike — Generate robust confidence intervals for the dual-temporal synergy term.
- Log-Transformation — Neutralize right-skewed temporal recovery units.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
Post-hoc pairwise tests defined for this model.
No specific guidelines provided.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Proportion of variance in DV explained by each factor/interaction after removing variance from other factors. Most commonly reported for RM-ANOVA. Small: .01, Medium: .06, Large: .14 (Cohen, 1988)
Comparable across different designs (within vs between vs mixed). Small: .02, Medium: .13, Large: .26 (Bakeman, 2005). Recommended when comparing effect sizes across studies with different designs.
For pairwise comparisons (simple effects). Calculated on difference scores. Small: 0.2, Medium: 0.5, Large: 0.8. Reports magnitude of specific contrasts.
Coefficient of concordance (0-1) for Friedman test (nonparametric alternative). Measures agreement/consistency across repeated measures.
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Dual-Temporal' Minimum: A minimum of 15 participants is required for a 2x2 within-subjects design. Power is high due to self-matching, but model stability collapses if measurement error is high.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | f=.10 (Small) | n ≈ 110 total |
| Medium Effect | f=.25 (Medium) | n ≈ 24 total |
| Large Effect | f=.40 (Large) | n ≈ 12 total |
Multi-Sphericity Strike: You must audit sphericity for both factors AND their interaction. If epsilon < 0.75, increase your sample size by 20% to compensate for the Greenhouse-Geisser power deflation.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A a × b within-subjects ANOVA examined brief description of research question, e.g., 'reaction time across practice sessions and task difficulty'. Sample: 'N = X participants completed all [a×b measurements.'] Sphericity: For EACH within-subjects effect, report Mauchly's test: 'Mauchly's test indicated that the assumption of sphericity was [met/violated for Factor A (χ²(df) = X.XX, p = .XX, ε = .XX) repeat for Factor B and interaction.' If violated: 'Therefore, Greenhouse-Geisser/Huynh-Feldt corrected results are reported for effect name.'] Results revealed significant/non-significant main effects of Factor A, F(df1, df2) = X.XX, p = .XXX, partial η² = .XX interpret, and Factor B, F(df1, df2) = X.XX, p = .XXX, partial η² = .XX. CRITICAL: Report interaction The A × B interaction was significant/non-significant, F(df1, df2) = X.XX, p = .XXX use corrected df if sphericity violated, partial η² = .XX. If interaction significant: 'Simple effects analysis revealed...' describe pattern, report simple effects F-tests or pairwise comparisons with corrections. Include cell means, SDs for key comparisons. Conclude with interpretation in research context.
- F-statistics with df for BOTH main effects AND interaction (use corrected df if sphericity violated)
- p-values for all three F-tests (use corrected p-values if sphericity violated)
- Effect sizes (partial η² or generalized η²) for all effects
- Mauchly's test results for EACH within-subjects effect (W, df, p, ε)
- Which correction used (GG or HF) for each violated effect
- Cell means, SDs, and n for each Factor A × Factor B combination
- Simple effects results if interaction significant (F-tests or pairwise t-tests with corrections)
- Effect sizes for simple effects (Cohen's d for pairwise comparisons)
- Interaction plot or cell means table
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Source | SS | df | MS | F | p | ηp² |
|---|---|---|---|---|---|---|
| Time (Within) | 85.2 | 1.8 | 47.3 | 12.42 | < .001 | .17 |
| Condition (Within) | 12.4 | 1 | 12.4 | 3.15 | .082 | .05 |
| Time × Condition | 44.1 | 1.8 | 24.5 | 6.42 | .004 | .10 |
| Error | 342.1 | 104.4 | 3.27 | — | — | — |
The Synergy Audit. Determines if the trajectory of recovery depends on the treatment condition.
Penalty adjustment for Sphericity. Higher violations result in lower (fractional) degrees of freedom.
The Multiplier. Measures the signal strength of each interaction relative to internal subject noise.
Partial Eta-Squared. The percentage of the subject's internal variance captured by the interaction.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Multi-Factor RM-ANOVA
model <- aov_ez(id = 'subject_id', dv = 'score', data = df, within = c('time', 'condition'))
# 2. Map Trajectory Interactions
emmip(model, condition ~ time)Audit the 3D variance-covariance matrix to ensure internal consistency across conditions and time.
# Execute Full Assumption Battery
performance::check_model(model)
# Standardized Effect Size Forensics
report::report_effectsize(model)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.