Guides / anova in r
How to run ANOVA in R
Base R will give you an ANOVA in one line. The complications are that aov() uses Type I sums of squares, which are the wrong ones for an unbalanced two-way design, and that repeated measures needs a different function entirely.
For a one-way ANOVA use aov(outcome ~ group, data = df) and wrap it in summary() to see the F statistic and p-value. Check assumptions with plot() on the model object, run post-hoc comparisons with TukeyHSD(), and use oneway.test() if variances are unequal.
The basic case
One-way ANOVA
Your grouping variable must be a factor. This is the commonest cause of confusing output — if the group is stored as a number, R will treat it as continuous and silently fit a regression instead.
df$group <- factor(df$group)
m <- aov(score ~ group, data = df)
summary(m)
The summary gives degrees of freedom, sums of squares, mean squares, the F value and Pr(>F), which is your p-value.
str(df$group) will tell you. If it says num or int rather than Factor, convert it before fitting or the model answers a different question entirely.
Diagnostics
Checking assumptions
par(mfrow = c(2, 2)) then plot(m) gives four diagnostic plots in one window.
| Plot | Checks | Healthy looks like |
|---|---|---|
| Residuals vs Fitted | Homoscedasticity | Formless band, no funnel |
| Q-Q Residuals | Normality of residuals | Points along the diagonal |
| Scale-Location | Homoscedasticity again | Flat trend line |
| Residuals vs Leverage | Influential cases | No points beyond Cook's distance contours |
For a formal test of equal variances: car::leveneTest(score ~ group, data = df). A significant result means the variances differ.
Not to the raw outcome. shapiro.test(residuals(m)) tests the right thing. Running it on the outcome variable when groups genuinely differ will flag a violation that is not there.
Welch
When variances are unequal
If Levene's test is significant, or group sizes differ markedly, use Welch's ANOVA. Base R has it built in:
oneway.test(score ~ group, data = df, var.equal = FALSE)
It reports fractional denominator degrees of freedom — that is correct, not a rounding error. Welch's costs very little power when variances are in fact equal, which is why many statisticians now recommend it as the default rather than the fallback.
Follow-up
Post-hoc tests
A significant F says at least one group differs. It does not say which.
TukeyHSD(m) gives all pairwise comparisons with adjusted p-values and confidence intervals.
| Situation | Use |
|---|---|
| Equal variances, all pairwise comparisons | TukeyHSD(m) |
| Unequal variances | rstatix::games_howell_test(df, score ~ group) |
| Comparing everything against one control | DescTools::DunnettTest(score ~ group, data = df) |
| A few planned comparisons | pairwise.t.test(df$score, df$group, p.adjust.method = "holm") |
Two factors
Two-way ANOVA and Type III
m2 <- aov(score ~ methodA * methodB, data = df) fits both main effects and the interaction. The asterisk expands to both plus their interaction; a colon would give the interaction alone.
The catch: summary() on an aov object gives Type I (sequential) sums of squares, where the order predictors appear changes the result. That is fine for a perfectly balanced design and wrong for almost every real one.
For Type III, which is what SPSS reports by default and what most journals expect:
options(contrasts = c("contr.sum", "contr.poly"))
car::Anova(m2, type = 3)
Running car::Anova(type = 3) without first setting sum-to-zero contrasts produces results that look plausible and are not interpretable. The two lines go together.
Interpret the interaction before the main effects. A significant interaction means the effect of one factor depends on the level of the other, and the main effects can be misleading on their own.
Within-subjects
Repeated measures
If the same participants appear in every condition, aov() needs an error term identifying the subject:
aov(score ~ condition + Error(id/condition), data = df_long)
Your data must be in long format — one row per observation, with an id column. Reshape with tidyr::pivot_longer() if it is currently wide.
In practice most people use afex, which handles the sphericity correction automatically:
afex::aov_ez(id = "id", dv = "score", within = "condition", data = df_long)
Repeated measures ANOVA assumes the variances of the differences between all condition pairs are equal. When that fails, degrees of freedom need a Greenhouse-Geisser or Huynh-Feldt correction. afex applies one by default; base aov() does not warn you at all.
Send your data structure and design. A named statistician confirms the model, fits it, checks the assumptions and returns annotated output with the code.
Get a fixed quoteEta squared
Effect size
effectsize::eta_squared(m) gives eta squared with a confidence interval. Use partial = FALSE for eta squared and TRUE for partial eta squared.
| η² | Conventional label |
|---|---|
| 0.01 | Small |
| 0.06 | Medium |
| 0.14 | Large |
Omega squared is less biased in small samples and is available as effectsize::omega_squared(m). Report it where you can.
APA style
How to report it
| Situation | How to write it |
|---|---|
| One-way, significant | F(2, 87) = 12.44, p < .001, η² = .22 |
| Welch's | Welch's F(2, 54.3) = 8.91, p = .001 |
| Interaction | F(1, 84) = 9.44, p = .003, partial η² = .10 |
| Post-hoc | Tukey HSD indicated C > A (p = .002) |
Send your dataset and script. A named statistician checks the model specification, the assumptions and the interpretation, and returns commented code you can cite.
See statistical consultancyAnswers
Frequently asked questions
How do I run a one-way ANOVA in R?
Make sure the grouping variable is a factor, then fit the model with aov(score ~ group, data = df) and view it with summary(). The Pr(>F) column is your p-value. Check assumptions by calling plot() on the model object.
Why does my ANOVA in R give strange results?
The most common cause is a grouping variable stored as a number rather than a factor, so R fits a regression instead of an ANOVA. Check with str() and convert using factor() before fitting.
What is the difference between aov() and Anova() in R?
summary() on an aov object gives Type I sequential sums of squares, where the order predictors are entered changes the result. car::Anova(model, type = 3) gives Type III, which is what SPSS reports and what most journals expect for unbalanced designs. Type III also requires setting sum-to-zero contrasts first.
How do I run a two-way ANOVA in R?
aov(score ~ factorA * factorB, data = df) fits both main effects and their interaction. For an unbalanced design, follow it with options(contrasts = c('contr.sum','contr.poly')) and car::Anova(model, type = 3). Interpret the interaction before the main effects.
How do I run a repeated measures ANOVA in R?
Use aov(score ~ condition + Error(id/condition), data = df_long) with data in long format, or afex::aov_ez(), which handles the sphericity correction automatically. Base aov() does not warn you when sphericity is violated.
How do I get post-hoc tests after ANOVA in R?
TukeyHSD(model) gives all pairwise comparisons with adjusted p-values. For unequal variances use rstatix::games_howell_test(), and for comparing every group against a single control use DescTools::DunnettTest().
Keep reading