Mixed ANOVA in R: One Between- and One Within-Subjects Factor

Run a two-way mixed ANOVA the modern way — combine a between-subjects factor and a within-subjects (repeated) factor, check sphericity, and decompose the interaction

Biostatistics

Learn to run a mixed ANOVA in R with rstatix — a design with one between-subjects factor and one within-subjects (repeated-measures) factor. Reshape the data to long format, check the assumptions including homogeneity of covariances (Box’s M) and sphericity (Mauchly’s test with automatic Greenhouse-Geisser correction), compute the group, time and interaction effects, and decompose a significant interaction with simple main effects and pairwise comparisons. Edit and run live.

Published

June 23, 2026

Modified

July 7, 2026

TipKey takeaways
  • A mixed ANOVA combines at least one between-subjects factor (different participants per group) and at least one within-subjects factor (the same participants measured repeatedly).
  • Fit it with rstatix anova_test(dv = score, wid = id, between = group, within = time), then extract the table with get_anova_table() — which applies the Greenhouse-Geisser correction automatically when sphericity is violated.
  • It needs the repeated-measures assumptions: homogeneity of covariances (Box’s M) and sphericity (Mauchly’s test, checked inside anova_test()), on top of the usual outliers/normality/variance checks.
  • A significant interaction is decomposed two ways: the effect of the between factor at each level of the within factor (a one-way between test), and the within factor at each level of the between factor (a paired test).
  • The data must be in long format — one row per measurement (id, group, time, score).

Introduction

A mixed ANOVA (also split-plot ANOVA) is used when a design has both a between-subjects factor — different participants in each group — and a within-subjects factor — the same participants measured under several conditions or over time. It is the meeting point of the two-way ANOVA (all between) and the repeated-measures ANOVA (all within).

The classic scenario: three exercise groups (between-subjects) each have their anxiety measured at three time points (within-subjects) — and the question is whether the groups change differently over time, i.e. whether group and time interact. This lesson runs the full workflow with rstatix using the anxiety dataset.

Note

Mixed ANOVA hypotheses. Three nulls: no main effect of the between factor (group means equal), no main effect of the within factor (time means equal), and no interaction (the groups follow the same time profile). The interaction is usually the question of interest.

The data: groups measured over time (long format)

We use the anxiety dataset from datarium: anxiety score for participants in three exercise groups (grp1/grp2/grp3, between-subjects), each measured at three times t1/t2/t3 (within-subjects). A mixed ANOVA needs long format — one row per measurement — which we build from the wide columns with base R:

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id    = factor(rep(anxiety$id, 3)),
  group = rep(anxiety$group, 3),
  time  = factor(rep(c("t1", "t2", "t3"), each = n)),
  score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

set.seed(123)
long %>% sample_n_by(group, time, size = 1)
# A tibble: 9 × 4
  id    group time  score
  <fct> <fct> <fct> <dbl>
1 15    grp1  t1     19.8
2 15    grp1  t2     20  
3 3     grp1  t3     14.9
4 29    grp2  t1     18.4
5 18    grp2  t2     14.4
6 25    grp2  t3     15.9
7 32    grp3  t1     15  
8 36    grp3  t2     14.5
9 41    grp3  t3     14.3

Summarize score by the group × time cells:

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

long %>%
  group_by(group, time) %>%
  get_summary_stats(score, type = "mean_sd")
# A tibble: 9 × 6
  group time  variable     n  mean    sd
  <fct> <fct> <fct>    <dbl> <dbl> <dbl>
1 grp1  t1    score       15  17.1  1.63
2 grp1  t2    score       15  16.9  1.70
3 grp1  t3    score       15  16.5  1.56
4 grp2  t1    score       15  16.6  1.57
5 grp2  t2    score       15  16.5  1.70
6 grp2  t3    score       15  15.5  1.70
7 grp3  t1    score       15  17.0  1.32
8 grp3  t2    score       15  15.0  1.39
9 grp3  t3    score       15  13.6  1.42

Look at the data first

A boxplot of score by time, coloured by group, shows whether the groups follow different time profiles — non-parallel patterns hint at an interaction:

library(rstatix)
library(ggpubr)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

ggboxplot(long, x = "time", y = "score", color = "group", palette = "jco")

A boxplot of anxiety score at three time points, coloured by exercise group; all groups start similar at t1 but separate by t3, where grp3 is lowest — a visual sign of a group-by-time interaction.

Check the assumptions

Beyond independent groups and no extreme outliers, a mixed ANOVA needs normality of the score in each cell, homogeneity of variance at each time (Levene), homogeneity of covariances across the between groups (Box’s M), and sphericity of the within factor (Mauchly — checked automatically by anova_test()).

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

# normality per cell (p > 0.05 = normal)
long %>% group_by(group, time) %>% shapiro_test(score)
# A tibble: 9 × 5
  group time  variable statistic     p
  <fct> <fct> <chr>        <dbl> <dbl>
1 grp1  t1    score        0.964 0.769
2 grp1  t2    score        0.956 0.624
3 grp1  t3    score        0.949 0.506
4 grp2  t1    score        0.977 0.949
5 grp2  t2    score        0.935 0.328
6 grp2  t3    score        0.909 0.131
7 grp3  t1    score        0.954 0.588
8 grp3  t2    score        0.952 0.558
9 grp3  t3    score        0.925 0.232
# equal variance at each time point
long %>% group_by(time) %>% levene_test(score ~ group)
# A tibble: 3 × 5
  time    df1   df2 statistic     p
  <fct> <int> <int>     <dbl> <dbl>
1 t1        2    42     0.176 0.839
2 t2        2    42     0.249 0.781
3 t3        2    42     0.335 0.717
# homogeneity of covariances across groups (Box's M; judge at alpha = 0.001)
box_m(long[, "score", drop = FALSE], long$group)
# A tibble: 1 × 4
  statistic p.value parameter method                                            
      <dbl>   <dbl>     <dbl> <chr>                                             
1      1.93   0.381         2 Box's M-test for Homogeneity of Covariance Matric…

The score is normal in each cell, Levene’s test is non-significant at each time, and Box’s M is non-significant — the assumptions hold. Sphericity (Mauchly’s test) is reported as part of the ANOVA below.

Run the mixed ANOVA

Pass the long data with dv (outcome), wid (subject id), between and within. get_anova_table() extracts the table and auto-applies the Greenhouse-Geisser correction if Mauchly’s sphericity test is significant:

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

res.aov <- anova_test(data = long, dv = score, wid = id, between = group, within = time)
get_anova_table(res.aov)
ANOVA Table (type II tests)

      Effect DFn DFd       F        p p<.05   ges
1      group   2  42   4.352 1.90e-02     * 0.168
2       time   2  84 394.909 1.91e-43     * 0.179
3 group:time   4  84 110.188 1.38e-32     * 0.108

There is a statistically significant group × time interaction, F(4, 84) = 110.19, p < 0.0001 — the exercise groups change differently over time. Both main effects are also significant (group F(2, 42) = 4.35, p = 0.02; time F(2, 84) = 394.91, p < 0.0001), but with a significant interaction the interaction is what we decompose.

Tip

Sphericity is handled for you. anova_test() runs Mauchly’s test internally; here it is non-significant (p = 0.08), so sphericity holds and no correction is needed. When Mauchly is significant, get_anova_table() returns the Greenhouse-Geisser-corrected table by default — see the repeated-measures ANOVA lesson for the sphericity details.

Post-hoc: decompose the interaction

A significant interaction is decomposed in two directions.

Effect of group at each time — a one-way between-subjects test within each time point, followed by pairwise comparisons:

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

long %>%
  group_by(time) %>%
  anova_test(dv = score, wid = id, between = group) %>%
  get_anova_table()
# A tibble: 3 × 8
  time  Effect   DFn   DFd      F         p `p<.05`   ges
* <fct> <chr>  <dbl> <dbl>  <dbl>     <dbl> <chr>   <dbl>
1 t1    group      2    42  0.365 0.696     ""      0.017
2 t2    group      2    42  5.84  0.006     "*"     0.218
3 t3    group      2    42 13.8   0.0000248 "*"     0.396

The groups do not differ at t1 (p = 0.7) but differ significantly at t2 (p = 0.006) and t3 (p < 0.0001) — the group effect emerges over time, which is the interaction in action.

Effect of time at each group — a within-subjects (paired) test inside each group:

library(rstatix)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

long %>%
  group_by(group) %>%
  anova_test(dv = score, wid = id, within = time) %>%
  get_anova_table()
# A tibble: 3 × 8
  group Effect   DFn   DFd     F        p `p<.05`   ges
  <fct> <chr>  <dbl> <dbl> <dbl>    <dbl> <chr>   <dbl>
1 grp1  time       2    28  14.8 4.05e- 5 *       0.024
2 grp2  time       2    28  77.5 3.88e-12 *       0.086
3 grp3  time       2    28 490.  1.64e-22 *       0.531

Anxiety changes significantly over time within every group, but most steeply in grp3 — consistent with the diverging profiles in the plot.

Plot it with the p-values

The reporting figure shows the group-at-each-time comparisons on the boxplot, with the mixed-ANOVA result in the subtitle:

library(rstatix)
library(ggpubr)

data("anxiety", package = "datarium")
n <- nrow(anxiety)
long <- data.frame(
  id = factor(rep(anxiety$id, 3)), group = rep(anxiety$group, 3),
  time = factor(rep(c("t1", "t2", "t3"), each = n)), score = c(anxiety$t1, anxiety$t2, anxiety$t3)
)

res.aov <- anova_test(data = long, dv = score, wid = id, between = group, within = time)
pwc <- long %>%
  group_by(time) %>%
  tukey_hsd(score ~ group) %>%
  add_xy_position(x = "time")

ggboxplot(long, x = "time", y = "score", color = "group", palette = "jco") +
  stat_pvalue_manual(pwc, hide.ns = TRUE, tip.length = 0) +
  labs(
    subtitle = get_test_label(res.aov, detailed = TRUE),
    caption = get_pwc_label(pwc)
  )

A boxplot of anxiety score across three time points coloured by exercise group, with the mixed-ANOVA interaction F(4,84)=110.19, p<0.0001 in the subtitle and the significant between-group pairwise brackets drawn at t2 and t3.

Report

A mixed ANOVA examined the effect of exercise group (between) and time (within) on anxiety score. There was a statistically significant interaction between group and time, F(4, 84) = 110.19, p < 0.0001. Simple main-effect analyses showed no group difference at t1 (p = 0.7) but significant differences at t2 (p = 0.006) and t3 (p < 0.0001); anxiety decreased significantly over time within every group.

Three-way mixed ANOVA: two between + one within

A mixed design can carry more factors. The first three-way layout has two between-subjects factors and one within-subjects factor (a 2-between, 1-within design). We use the performance dataset: performance score measured at two times (time, within), with gender and stress as the two between-subjects factors. Build the long format with base R:

library(rstatix)

data("performance", package = "datarium")
np <- nrow(performance)
perf <- data.frame(
  id     = factor(rep(performance$id, 2)),
  gender = rep(performance$gender, 2),
  stress = rep(performance$stress, 2),
  time   = factor(rep(c("t1", "t2"), each = np)),
  score  = c(performance$t1, performance$t2)
)

Pass both between factors with between = c(gender, stress) and the within factor with within = time:

library(rstatix)

data("performance", package = "datarium")
np <- nrow(performance)
perf <- data.frame(
  id = factor(rep(performance$id, 2)), gender = rep(performance$gender, 2),
  stress = rep(performance$stress, 2), time = factor(rep(c("t1", "t2"), each = np)),
  score = c(performance$t1, performance$t2)
)

res.aov <- anova_test(data = perf, dv = score, wid = id, between = c(gender, stress), within = time)
get_anova_table(res.aov)
ANOVA Table (type II tests)

              Effect DFn DFd      F        p p<.05      ges
1             gender   1  54  2.406 1.27e-01       0.023000
2             stress   2  54 21.166 1.63e-07     * 0.288000
3               time   1  54  0.063 8.03e-01       0.000564
4      gender:stress   2  54  1.554 2.21e-01       0.029000
5        gender:time   1  54  4.730 3.40e-02     * 0.041000
6        stress:time   2  54  1.821 1.72e-01       0.032000
7 gender:stress:time   2  54  6.101 4.00e-03     * 0.098000

There is a significant three-way interaction between gender, stress and time, F(2, 54) = 6.10, p = 0.004 — the gender × stress pattern itself changes over time. Decompose it exactly as a three-way ANOVA: the simple two-way interaction of gender × stress at each time point (grouping by time):

library(rstatix)

data("performance", package = "datarium")
np <- nrow(performance)
perf <- data.frame(
  id = factor(rep(performance$id, 2)), gender = rep(performance$gender, 2),
  stress = rep(performance$stress, 2), time = factor(rep(c("t1", "t2"), each = np)),
  score = c(performance$t1, performance$t2)
)

perf %>%
  group_by(time) %>%
  anova_test(dv = score, wid = id, between = c(gender, stress))
# A tibble: 6 × 8
  time  Effect          DFn   DFd      F          p `p<.05`   ges
* <fct> <chr>         <dbl> <dbl>  <dbl>      <dbl> <chr>   <dbl>
1 t1    gender            1    54  0.186 0.668      ""      0.003
2 t1    stress            2    54 14.9   0.00000723 "*"     0.355
3 t1    gender:stress     2    54  2.12  0.131      ""      0.073
4 t2    gender            1    54  5.97  0.018      "*"     0.1  
5 t2    stress            2    54  9.60  0.000271   "*"     0.262
6 t2    gender:stress     2    54  4.95  0.011      "*"     0.155

The gender × stress interaction is significant at t2 (F(2, 54) = 4.95, p = 0.011) but not at t1 — from there you would run simple simple main effects (e.g. the effect of stress for females at t2, F(2, 27) = 10.5, p = 0.0004) and pairwise comparisons, just as in the three-way ANOVA.

Three-way mixed ANOVA: one between + two within

The second layout has one between-subjects factor and two within-subjects factors (1-between, 2-within). We use the weightloss dataset, treating exercises as the between factor and both diet and time as within factors. The data needs one tweak — each of the 24 individuals appears in two diet trials, so the id is repeated across them:

library(rstatix)

data("weightloss", package = "datarium")
weightloss$id <- factor(rep(1:24, 2))   # same 24 people across the two diet trials
nw <- nrow(weightloss)
wl <- data.frame(
  id        = factor(rep(weightloss$id, 3)),
  diet      = rep(weightloss$diet, 3),
  exercises = rep(weightloss$exercises, 3),
  time      = factor(rep(c("t1", "t2", "t3"), each = nw)),
  score     = c(weightloss$t1, weightloss$t2, weightloss$t3)
)

Pass the single between factor and both within factors with within = c(diet, time):

library(rstatix)

data("weightloss", package = "datarium")
weightloss$id <- factor(rep(1:24, 2))
nw <- nrow(weightloss)
wl <- data.frame(
  id = factor(rep(weightloss$id, 3)), diet = rep(weightloss$diet, 3),
  exercises = rep(weightloss$exercises, 3), time = factor(rep(c("t1", "t2", "t3"), each = nw)),
  score = c(weightloss$t1, weightloss$t2, weightloss$t3)
)

res.aov <- anova_test(data = wl, dv = score, wid = id, between = exercises, within = c(diet, time))
get_anova_table(res.aov)
ANOVA Table (type II tests)

               Effect DFn DFd      F        p p<.05   ges
1           exercises   1  22 38.771 2.88e-06     * 0.284
2                diet   1  22  7.912 1.00e-02     * 0.028
3                time   2  44 82.199 1.38e-15     * 0.541
4      exercises:diet   1  22 51.698 3.31e-07     * 0.157
5      exercises:time   2  44 26.222 3.18e-08     * 0.274
6           diet:time   2  44  0.784 4.63e-01       0.013
7 exercises:diet:time   2  44  9.966 2.69e-04     * 0.147

There is a significant three-way interaction between exercises, diet and time, F(2, 44) = 9.97, p < 0.001 — the diet × time pattern differs between exercisers and non-exercisers. The decomposition follows the same recipe: the simple two-way interaction within each level of the between factor, then simple simple main effects and pairwise comparisons, with the within-subjects factors handled by the repeated-measures machinery (sphericity correction, paired post-hoc).

A mixed ANOVA splits the total variation into a between-subjects stratum and a within-subjects stratum, each with its own error term: between effects are tested against the subject-to-subject error, while each within effect (and any interaction involving it) is tested against its own subject × effect error. The within-subjects F-tests assume sphericity — equal variances of the pairwise differences across the repeated levels — which is why Mauchly’s test and the Greenhouse-Geisser correction apply only to the within and interaction rows, never to the purely between-subjects factor.

Try it live

Run the mixed ANOVA yourself and edit the post-hoc step. The within-subjects computation is heavy in the browser, so the cell raises R’s expression limit first (otherwise webR errors with “evaluation nested too deeply”). The sandbox boots on first Run.

🟢 With an AI agent

Ask Prova “I have a between-subjects group and a within-subjects time factor — run a mixed ANOVA on my long-format data, check sphericity, and decompose the interaction” — it answers with rstatix code you can run on your own data, then helps you read the interaction and the simple-main-effect follow-ups. The runtime is the judge. Ask Prova →

Common issues

Your data is in wide format. A mixed ANOVA needs long format — one row per measurement with id, the between factor, the within factor and the score. Reshape the wide columns first (here, stack t1/t2/t3 into a single score column with a time label).

You forgot wid. anova_test() needs the subject identifier (wid = id) to know which rows are repeated measures of the same participant; without it the within factor is treated as between-subjects and the result is wrong. Make id a factor.

“evaluation nested too deeply” in the browser. The within-subjects model overflows webR’s default expression limit. Set options(expressions = 5e5) as the first line of the live cell (this is a webR-only quirk; the static render is unaffected).

Frequently asked questions

A mixed ANOVA tests a design with both a between-subjects factor (different participants per group) and a within-subjects factor (the same participants measured repeatedly). It reports a main effect for each factor plus their interaction — typically the question is whether the groups change differently across the repeated condition.

A repeated-measures ANOVA has only within-subjects factors (every participant experiences every condition). A mixed ANOVA adds at least one between-subjects factor, so participants are split into independent groups and measured repeatedly. Mixed = between + within in one model.

Yes — for the within-subjects factor. anova_test() runs Mauchly’s test automatically, and get_anova_table() applies the Greenhouse-Geisser correction when sphericity is violated (Mauchly p ≤ 0.05). The between-subjects factor does not need a sphericity check.

In two directions. (1) The between factor at each level of the within factor — a one-way between-subjects test within each time point. (2) The within factor at each level of the between factor — a repeated-measures (paired) test inside each group. Follow each significant simple main effect with Bonferroni-adjusted pairwise comparisons.

Test your understanding

  1. Run it. In the live cell, fit a mixed ANOVA of score by group (between) and time (within), subject id. Fill the blanks. Is the group × time interaction significant?
  2. Conceptual. Your interaction is significant. Name the two complementary simple-main-effect analyses you would run to decompose it.

Fill dv = score, wid = id, between = group, within = time. Read the group:time row. For question 2, recall that a mixed interaction is decomposed both by holding the within factor fixed and by holding the between factor fixed.

anova_test(data = long, dv = score, wid = id, between = group, within = time) |>
  get_anova_table()
#> group:time interaction is significant: F(4, 84) = 110.19, p < 0.0001.

The interaction is significant. To decompose it: (1) test the effect of group at each time point (between-subjects, then pairwise), and (2) test the effect of time within each group (within-subjects, paired). Together they describe where and how the groups diverge.

TipWhich test when?
  • All factors between-subjectstwo-way ANOVA.
  • All factors within-subjectsrepeated-measures ANOVA.
  • One between + one withinmixed ANOVA (this lesson).
  • A continuous covariate to adjust forANCOVA.

Conclusion

You can now run a mixed ANOVA in R: reshape to long format, fit anova_test(dv, wid, between, within), and read the table from get_anova_table() — which corrects for sphericity automatically. Check the repeated-measures assumptions (Box’s M, Mauchly) alongside the usual ones, and when the interaction is significant, decompose it in both directions — the between factor within each repeated level, and the within factor inside each group. Mixed designs are everywhere in longitudinal and intervention studies, and this is the test that fits them.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Mixed {ANOVA} in {R:} {One} {Between-} and {One}
    {Within-Subjects} {Factor}},
  date = {2026-06-23},
  url = {https://www.datanovia.com/learn/biostatistics/anova/mixed-anova-in-r},
  langid = {en}
}
For attribution, please cite this work as:
“Mixed ANOVA in R: One Between- and One Within-Subjects Factor.” 2026. June 23. https://www.datanovia.com/learn/biostatistics/anova/mixed-anova-in-r.