# Repeated measures ANOVA vs mixed model: which to use for measurements over time

> Repeated measures ANOVA and a mixed model agree on complete data; when a value is missing for a reason unrelated to the value itself, use the mixed model. R code for both, and a Python per-animal summary route.

Canonical: https://www.licklider.ai/guides/repeated-measures-anova-vs-mixed-model/
Section: Guides › Repeated measurements over time
Author: Licklider · Published 2026-11-11 · Last reviewed 2026-11-11 (UTC)

> **Quick answer.** When every animal has every time point, repeated measures ANOVA and a linear mixed model with a random intercept for each animal give the same group × time F test; that test is valid when sphericity holds, and otherwise the Greenhouse–Geisser correction to the ANOVA keeps it honest. **When a value is missing for a reason unrelated to the value itself (missing at random: a failed measurement, a lost sample), use the mixed model**: repeated measures ANOVA drops that animal entirely, while the mixed model keeps its other measurements. If animals dropped out because they were sick or died, neither method alone is reliable. A random intercept assumes every pair of time points is equally correlated; with many or widely spaced time points, where neighbouring measurements are expected to be more alike, plan a covariance structure in which correlation falls with distance (AR(1) or unstructured) instead. Take the mixed model's p-values from Satterthwaite or Kenward–Roger degrees of freedom: they matter with few animals, such as this example's 12, and change little with hundreds of animals. A third valid route is one summary per animal — such as its slope over time — compared between groups with Welch's t-test, provided you chose that summary before seeing the data.

> **Terms used below.** *Repeated measures*: the same animal measured more than once (here, weekly). *Repeated measures ANOVA*: an ANOVA that separates animal-to-animal differences from week-to-week change. *Sphericity*: the assumption that the differences between any two time points are equally variable; *Greenhouse–Geisser correction*: reduces the degrees of freedom when sphericity fails. *Linear mixed model*: a regression that knows which measurements came from which animal; *random intercept*: lets each animal have its own baseline weight. *Group × time interaction*: the test of whether the groups change differently over time. *Degrees of freedom (df)*: the number that sets how wide the t or F distribution is; *Satterthwaite* and *Kenward–Roger* are two ways of estimating it for mixed models with few animals. *Missing at random*: whether a value is missing may depend on what you did observe (such as the animal's earlier weights) but not on the missing value itself. *Fixed effects*: the parts of the model you test (group, week and their interaction). *Contrast* (planned contrast): one specific comparison chosen before the analysis, such as the week 0 to week 3 change in drug mice minus that in vehicle mice. *SE* (standard error): how uncertain an estimate is; a 95% CI spans roughly 2 SE either side of it. *ε (epsilon)*: the Greenhouse–Geisser measure of sphericity, 1 when it holds exactly; the df are multiplied by it. *Covariance structure*: the assumed pattern of correlation between one animal's measurements; *compound symmetry*: every pair of time points equally correlated (what a random intercept assumes); *AR(1)* (first-order autoregressive): correlation falls the further apart two time points are; *unstructured*: every pair of time points gets its own correlation. *AIC*: a measure of model fit, lower is better. *Residual df*: observations minus estimated fixed effects (47 − 8 = 39 here: intercept, group, three week terms and three interaction terms). *gls* (generalised least squares, `nlme::gls`): the same fixed effects as the mixed model, with the correlation between one animal's measurements specified directly instead of through a random intercept. *Forking path*: choosing the analysis after seeing the data, which raises the false-positive rate. *Multiplicity*: testing many comparisons, each at p < 0.05, raises the chance that at least one is a false positive. *Least squares*: fitting the straight line that makes the sum of squared vertical distances to the points smallest. *Liberal*: a test that gives p < 0.05 more often than 5% of the time when there is no effect; *conservative*: less often.

## Which analysis fits your data

| Your data | Use | Why |
| --- | --- | --- |
| Every animal measured at every time point, same times for all | Repeated measures ANOVA with the Greenhouse–Geisser correction, or the mixed model with a random intercept — they agree when sphericity holds | On complete data the two give the same F test (shown below); with many or widely spaced time points, see the third row |
| Any animal missing any value, for a reason unrelated to the missing value itself (failed measurement, lost sample) | Linear mixed model, random intercept per animal, Satterthwaite or Kenward–Roger df | Repeated measures ANOVA removes the whole animal; the mixed model keeps its other values |
| Many or widely spaced time points, where measurements close in time are expected to be more alike than distant ones | Pre-specify a covariance structure that lets correlation fall with distance — AR(1) or unstructured, for example with `nlme::gls` — and report your planned contrast from that model; or use the summary-measure route | A random intercept assumes the same correlation between every pair of time points; when that is wrong, its interaction test gives p < 0.05 too often (7.3% instead of 5% in the simulation below) |
| Values missing because the animal was sick, died, reached a humane endpoint, or the value was outside the assay range | Neither model alone is reliable: report how many values are missing and why, and analyse the time window every animal completed as a check | Both methods assume that whether a value is missing does not depend on the missing value itself; here it probably does |
| Times differ between animals (day 3 for some, day 4 for others), or you want a smooth time trend | Mixed model with time as a number (`weight ~ group * week + (1 \| mouse)`) | Repeated measures ANOVA needs the same time points for everyone |
| One clear question, such as "do the groups gain weight at different rates?" | A summary per animal (slope, area under the curve, last value), chosen before seeing the data, then Welch's t-test | Each animal contributes one independent number (Matthews et al. 1990) |
| Scores, counts or proportions over time | See the [test chooser](/tools/statistical-test-chooser/) and the [count data guide](/guides/statistical-tests-for-count-data/) | Both models here assume roughly normal residuals |

Matthews and colleagues set out the summary-measure route in the *BMJ*: "In the first stage a suitable summary of the response in an individual, such as a rate of change or an area under a curve, is identified and calculated for each subject. In the second stage these summary measures are analysed by simple statistical techniques as though they were raw data." They add that "the method is statistically valid and likely to be more relevant to the study questions."

On missing values, Laird's (1988) review of longitudinal studies notes that unbalanced patterns make standard analyses "more difficult or inappropriate to implement", and that missing data mean a "loss of efficiency, and may introduce bias into the results as well". GraphPad's guide states the condition for the mixed model: "The results of fitting a mixed model with missing values will be meaningful, of course, only if the values are missing for random reasons." For the small-sample degrees of freedom, Kenward and Roger (1997) describe an F approximation "shown to perform well in a range of small sample settings"; Greenhouse and Geisser (1959) introduced the degrees-of-freedom adjustment "resulting in conservative F tests".

**What to do:** decide the route in your analysis plan, from the design and the expected missing values, before you see the results.

## One dataset, three routes

Twelve mice (6 vehicle, 6 drug) were weighed weekly for four weeks (weeks 0 to 3). The data are constructed: mice differ in baseline weight (SD 1.5 g), week-to-week noise is smaller (SD 0.6 g), and vehicle mice gain about 1.0 g per week and drug mice about 0.4 g. The random seed was fixed before the data were generated. One value is then missing: mouse D4's week-3 weight (a scale error). Download [weights_missing.csv](/guides/repeated-measures-anova-vs-mixed-model/weights_missing.csv) and the complete [weights.csv](/guides/repeated-measures-anova-vs-mixed-model/weights.csv).

| Analysis | Mice used | Group × time test |
| --- | --- | --- |
| Complete data: repeated measures ANOVA, sphericity assumed | 12 | F(3, 30) = 4.54, p = 0.0097 |
| Complete data: mixed model, random intercept per mouse | 12 | F(3, 30) = 4.54, p = 0.0097 — identical on complete data |
| Complete data: repeated measures ANOVA, Greenhouse–Geisser (ε = 0.85) | 12 | F(2.56, 25.65) = 4.54, p = 0.014 |
| One value missing: repeated measures ANOVA, Greenhouse–Geisser | 11 (D4 dropped) | F(2.54, 22.83) = 3.64, p = 0.034 |
| One value missing: mixed model, Satterthwaite df | 12 (47 of 48 values) | F(3, 29.0) = 3.77, p = 0.021 |
| One value missing: mixed model, Kenward–Roger df | 12 | F(3, 29.0) = 3.77, p = 0.021 (df 29.01 vs 28.99: the two methods agree to the printed digits) |

On complete data the two methods agree to every printed digit. With one missing value, repeated measures ANOVA throws away D4's three good weighings; the mixed model keeps them.

**The interaction says the groups differ; a planned contrast says by how much.** For "did drug mice gain less weight from week 0 to week 3?", the contrast is the change in drug mice minus the change in vehicle mice. In this constructed example it is **−1.69 g (95% CI −2.79 to −0.60), t(29) = −3.16, p = 0.0037** (mixed model, Satterthwaite). Comparing the groups at week 3 alone gives −1.34 g (95% CI −4.02 to 1.34, p = 0.30), because that comparison includes the large baseline differences between mice. The change contrast compares each mouse with its own baseline, so the differences between mice cancel, and the model also uses D4's three weighings; that is why it is so much more precise.

**The summary-measure route** on the same data: each mouse's slope (g per week), then Welch's t-test. Drug mice gained 0.53 g/week less (95% CI 0.13 to 0.94), Welch t(7.51) = −3.08, p = 0.016. R's `t.test` on the same slopes gives the same numbers.

**What to do:** report the group × time test and the contrast you planned, with its confidence interval in grams — not a p-value for every week.

## Code in R and Python

The R code uses the mixed model as the primary analysis, with repeated measures ANOVA shown for comparison. Both code blocks expect one row per weighing (columns mouse, group, week, weight); if your sheet has one row per mouse with a column per week, reshape it first with `tidyr::pivot_longer()` in R or `pandas.melt()` in Python. Save the CSV in R's working directory (`getwd()` shows where that is):

```r
# Install once: install.packages(c("lmerTest", "emmeans", "afex", "pbkrtest", "nlme"))
library(lmerTest)   # lme4 plus Satterthwaite p-values
library(emmeans)
library(afex)       # repeated-measures ANOVA with sphericity corrections
library(nlme)       # gls() for a pre-specified covariance structure

d <- read.csv("weights_missing.csv")   # columns: mouse, group, week, weight
d$group  <- factor(d$group, levels = c("vehicle", "drug"))
d$week_f <- factor(d$week)

# Mixed model: keeps every mouse, including D4 with its missing week-3 value
fit <- lmer(weight ~ group * week_f + (1 | mouse), data = d)
summary(fit)$ngrps                   # number of mice used: 12
anova(fit)                           # group:week_f row is the interaction (Satterthwaite df)
anova(fit, ddf = "Kenward-Roger")    # the same test with Kenward-Roger df

# Planned contrast: change from week 0 to week 3, drug minus vehicle
emm <- emmeans(fit, ~ group * week_f, lmer.df = "satterthwaite")
emm                                  # check the cell order before writing the weights
summary(contrast(emm, list(change = c(1, -1, 0, 0, 0, 0, -1, 1))), infer = TRUE)

# Covariance falling with distance (AR(1)), if that is what your plan specified
g <- gls(weight ~ group * week_f, data = d, correlation = corAR1(form = ~ week | mouse))
anova(g, type = "marginal")["group:week_f", ]

# Repeated-measures ANOVA for comparison: drops D4, reports the Greenhouse-Geisser correction
rm <- aov_ez("mouse", "weight", d, between = "group", within = "week_f")
nice(rm, correction = "GG")
summary(rm)$pval.adjustments         # "GG eps" column is the Greenhouse-Geisser epsilon
```

`summary(fit)$ngrps` prints `mouse 12`, the number of mice the model used. The Kenward–Roger line prints the same interaction, F(3, 29.0) = 3.77, p = 0.021. Printing `emm` lists the cells in the order the contrast weights follow (vehicle and drug at week 0, then at week 1, and so on): +1 for vehicle week 0, −1 for drug week 0, −1 for vehicle week 3, +1 for drug week 3; if your cells print in another order, reorder the weights to match. The contrast prints `change -1.69`, SE 0.536, t(29) = −3.156, p = 0.0037. The AR(1) model prints F = 1.59 on 3 and 39 df, p = 0.21 (next section). The last line prints the Greenhouse–Geisser ε in the `GG eps` column (0.85 here with D4 dropped; it is also 0.85 on the complete file).

Python's statsmodels has no small-sample degrees of freedom for mixed models (see the traps below), so the Python code uses the summary-measure route:

```python
# Install once: pip install numpy pandas scipy   (pandas 2.2 or later)
import numpy as np
import pandas as pd
from scipy import stats

d = pd.read_csv("weights_missing.csv")   # columns: mouse, group, week, weight

# Summary measure chosen in advance: each mouse's slope (g per week), then Welch's t-test
slopes = d.groupby(["mouse", "group"]).apply(
    lambda s: np.polyfit(s["week"], s["weight"], 1)[0], include_groups=False).reset_index(name="slope")
drug = slopes.loc[slopes.group == "drug", "slope"]
veh = slopes.loc[slopes.group == "vehicle", "slope"]
res = stats.ttest_ind(drug, veh, equal_var=False)
ci = res.confidence_interval()
print(f"slope difference {drug.mean() - veh.mean():.2f} g/week, 95% CI {ci.low:.2f} to {ci.high:.2f}, "
      f"Welch t({res.df:.2f}) = {res.statistic:.2f}, p = {res.pvalue:.3f}")
```

It prints `slope difference -0.53 g/week, 95% CI -0.94 to -0.13, Welch t(7.51) = -3.08, p = 0.016`.

**Software traps, checked on statsmodels 0.15.0, emmeans 1.10.0, afex 1.3-1, lmerTest 3.1-3 and pbkrtest 0.5.2:**

- **statsmodels `AnovaRM`** cannot fit a design with a missing value or a between-animal group: with the missing value it raises "Data is unbalanced"; with a between-animal group it raises "Between subject effect not yet supported!". Its documentation says sphericity corrections "are not yet implemented".
- **statsmodels `MixedLM`** gives the same estimate and standard error as R's `lmer` (the week-3 interaction coefficient −1.691, SE 0.536), but tests it with a z statistic: p = 0.0016 instead of 0.0037. With 12 animals, use R's lmerTest, or the summary-measure route in Python ([nested data](/guides/nested-data-mixed-model/) explains why).
- **emmeans** uses Kenward–Roger degrees of freedom for `lmer` models by default when the pbkrtest package is installed and the model has no more than 3,000 observations; without pbkrtest it uses large-sample (infinite) degrees of freedom. lmerTest's `anova()` uses Satterthwaite. Set `lmer.df` explicitly, as in the code above, and name the method you report.
- **afex** removes an animal with any missing value and prints a warning naming it (here "D4"), and applies the Greenhouse–Geisser correction by default.

**What to do:** run the code on your file, check that `summary(fit)$ngrps` and the `Number of obs` line of `summary(fit)` match your file, and look for afex's warning naming any dropped animal, then report.

## Choose the covariance structure before you analyse

A random intercept assumes that any two weeks of the same mouse are equally correlated (compound symmetry, which implies sphericity). Other structures let neighbouring weeks be more alike than distant ones (AR(1), first-order autoregressive). On the example data (the file with one value missing), `nlme::gls` with compound symmetry reproduces the random-intercept model's F = 3.77; it reports p = 0.018 rather than 0.021 only because `gls` uses the residual df (39) instead of Satterthwaite's 29, so report the mixed model's 0.021. Switching the same `gls` model to AR(1) changes the interaction test to F = 1.59, p = 0.21. The AIC values are close (146.4 vs 147.5), so the data do not settle which structure is right, and choosing the structure by AIC after seeing the results is as much a forking path as choosing it by p-value. The structure can change the conclusion, so choose it in your plan from what you expect of the measurements, and report it.

**What to do:** choose the structure from what you expect before you analyse — a random intercept when the correlation between any two time points should be similar (a few closely spaced measurements, such as four weekly weighings), AR(1) or unstructured when it should fall with distance (many time points, or gaps of very different length) — and name it in Methods.

## What the simulation shows

Our simulation used the example's design (2 groups × 6 mice × 4 weeks; 2,000 datasets per row, seeds fixed in advance) and counted how often each analysis gave p < 0.05. In the main table the data were generated with a random intercept per mouse (mouse SD 1.5 g, week-to-week SD 0.6 g) — the structure the mixed model assumes, which also satisfies sphericity.

| Analysis and test | No true effect, complete data | No true effect, one value missing at random | True interaction (1.0 vs 0.4 g/week), complete | True interaction, one value missing at random |
| --- | --- | --- | --- | --- |
| Two-way ANOVA ignoring the mice: group effect | 30.8% | 27.9% | — | — |
| Two-way ANOVA ignoring the mice: interaction | 0.0% | 0.0% | 2.1% | 1.9% |
| Mixed model, Satterthwaite: group effect | 4.6% | 4.2% | — | — |
| Mixed model, Satterthwaite: interaction | 4.2% | 5.4% | 88.6% | 86.9% |
| Repeated measures ANOVA, Greenhouse–Geisser, complete animals only: interaction | 3.0% | 3.5% | 84.8% | 77.8% |

Ignoring the animals fails in both directions: the group effect, which compares animals, came out "significant" in about 3 of every 10 experiments with no real difference, while the interaction, which compares changes within animals, reached p < 0.05 in only about 2 of every 100 experiments even when the effect was real. With these random-intercept data the mixed model keeps both near 5% when there is no effect. With one value missing at random, the mixed model detected the true interaction in 86.9% of experiments and repeated measures ANOVA, which drops that animal, in 77.8%. The simulation code is [simulation.R](/guides/repeated-measures-anova-vs-mixed-model/simulation.R).

When the correlation pattern differs from the model's, the picture changes. We repeated the no-effect scenario with correlation that falls with distance (0.9 between neighbouring weeks, 0.81 two weeks apart, 0.73 three weeks apart; total SD 1.6 g; [simulation_ar1.R](/guides/repeated-measures-anova-vs-mixed-model/simulation_ar1.R)):

| Analysis (no true effect, correlation falling with distance) | Share with p < 0.05 |
| --- | --- |
| Mixed model with a random intercept, Satterthwaite: interaction | 7.3% |
| Repeated measures ANOVA, Greenhouse–Geisser: interaction | 5.4% |
| `nlme::gls` with AR(1) correlation: interaction | 5.8% |
| Slope per mouse, Welch's t-test | 5.2% |

The random-intercept model is then too liberal, because it assumes every pair of weeks is equally correlated. If you expect measurements close in time to be more alike — many or widely spaced time points — plan an AR(1) or unstructured covariance, the Greenhouse–Geisser-corrected ANOVA for complete data, or the summary-measure route, and say so in Methods.

**What to do:** never analyse repeated measurements as if every weighing were a separate animal; use one of the three routes on this page.

## Check what an AI assistant ran

Check these five points in generated code for repeated measurements:

1. **Is the animal in the model?** A plain two-way ANOVA, `anova(lm(weight ~ group * week_f))`, treats 47 weighings as 47 animals. On the example it gave an interaction p of 0.58, hiding the effect the correct analyses find.
2. **Did an animal disappear?** Look for afex's "Missing values … removed" warning, or compare `summary(fit)$ngrps` with the number of animals in your file.
3. **Which test statistic?** A z-test from statsmodels `MixedLM` is too optimistic with few animals; ask for Satterthwaite or Kenward–Roger df, or a summary-measure analysis.
4. **Which contrast?** Separate p-values for every week raise a multiplicity problem and are less precise than the planned change contrast; see [which correction to use](/guides/which-multiple-comparison-correction/) if you do need several.
5. **Random slopes?** Code such as `(1 + week | mouse)` with 4–6 animals per group can fail to estimate its variances ("singular fit"); with few animals, keep the random intercept and say so.

**What to do:** paste the model formula and the first lines of the output into your notes, and check each point above before you report.

## Report it

> Body weight was measured weekly (weeks 0–3) in 12 mice (6 per group; the unit was the mouse). One week-3 value was missing because of a scale error. We analysed weight with a linear mixed model with group, week (as a factor) and their interaction as fixed effects and a random intercept for each mouse (R 4.3.3, lme4/lmerTest; Satterthwaite degrees of freedom). Weight change differed between groups (group × week, F(3, 29.0) = 3.77, p = 0.021); from week 0 to week 3, drug-treated mice gained 1.69 g less than vehicle mice (95% CI 0.60 to 2.79 g, t(29) = −3.16, p = 0.0037; planned contrast).

If your data are complete and you used repeated measures ANOVA, replace the model sentence with the ANOVA, report the Greenhouse–Geisser ε (the `GG eps` column; here 0.85) and corrected df (here F(2.56, 25.65) = 4.54, p = 0.014), and keep the contrast in grams. If you used the summary-measure route, report it as planned: "Each mouse's weight slope over weeks 0–3 was estimated by least squares and compared between groups with Welch's t-test."

**What to do:** state how many values were missing, why, and how the analysis handled them; reviewers ask for this first.

## References

- Matthews JNS, Altman DG, Campbell MJ, Royston P (1990). Analysis of serial measurements in medical research. *BMJ* 300:230–235. [doi:10.1136/bmj.300.6719.230](https://doi.org/10.1136/bmj.300.6719.230)
- Greenhouse SW, Geisser S (1959). On methods in the analysis of profile data. *Psychometrika* 24:95–112. [doi:10.1007/BF02289823](https://doi.org/10.1007/BF02289823)
- Laird NM (1988). Missing data in longitudinal studies. *Statistics in Medicine* 7:305–315. [doi:10.1002/sim.4780070131](https://doi.org/10.1002/sim.4780070131)
- Kenward MG, Roger JH (1997). Small sample inference for fixed effects from restricted maximum likelihood. *Biometrics* 53:983–997. [doi:10.2307/2533558](https://doi.org/10.2307/2533558)
- GraphPad Prism 11 Statistics Guide: [Missing values in repeated measures ANOVA](https://www.graphpad.com/guides/prism/latest/statistics/stat_missing-values-in-repeated-mea.htm) and [The mixed effects model approach](https://www.graphpad.com/guides/prism/latest/statistics/stat_anova-approach-vs_-mixed-model.htm) (accessed 10 November 2026).
- statsmodels 0.15.0, [`AnovaRM`](https://www.statsmodels.org/stable/generated/statsmodels.stats.anova.AnovaRM.html) documentation.

## Related

- [Nested data: average per animal or fit a mixed model?](https://www.licklider.ai/guides/nested-data-mixed-model/)
- [Two-way ANOVA vs separate t-tests](https://www.licklider.ai/guides/two-way-anova-vs-multiple-t-tests/)
- [How to report ANOVA results](https://www.licklider.ai/guides/how-to-report-anova-results/)
- [Glucose tolerance test statistics](https://www.licklider.ai/guides/glucose-tolerance-test-statistics/)
- [Change from baseline vs ANCOVA](https://www.licklider.ai/guides/change-from-baseline-vs-ancova/)
- [Tumour growth curves](https://www.licklider.ai/guides/tumour-growth-curves/)
- [Circadian rhythms (cosinor)](https://www.licklider.ai/guides/circadian-rhythm-cosinor/)
- [Fear conditioning freezing](https://www.licklider.ai/guides/fear-conditioning-freezing/)

## How this page was made

- Written and published by Licklider. Recommendations cite peer-reviewed methods papers and official software documentation listed under References; quoted passages were checked against the source text.
- The worked example uses constructed data. Its numbers and the simulation were produced by stored code (R 4.3.3 with afex 1.3-1, lme4 1.1-35.1, lmerTest 3.1-3, emmeans 1.10.0, pbkrtest 0.5.2 and nlme 3.1-164; Python 3.13 with pandas 3.0.5, SciPy 1.18.1 and statsmodels 0.15.0); where both languages compute a value, they agree.
- Separate review passes checked the statistics, the citations and readability before publication.
