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.

Written and reviewed by Hafiz Ahmad Tariq, Senior Biostatistician
Updated 2 September 20267 min read
How do you run an ANOVA in R?

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.

Check your factor first

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.

PlotChecksHealthy looks like
Residuals vs FittedHomoscedasticityFormless band, no funnel
Q-Q ResidualsNormality of residualsPoints along the diagonal
Scale-LocationHomoscedasticity againFlat trend line
Residuals vs LeverageInfluential casesNo 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.

Normality applies to residuals

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.

SituationUse
Equal variances, all pairwise comparisonsTukeyHSD(m)
Unequal variancesrstatix::games_howell_test(df, score ~ group)
Comparing everything against one controlDescTools::DunnettTest(score ~ group, data = df)
A few planned comparisonspairwise.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)

Set the contrasts or Type III is wrong

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)

Sphericity

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.

Not sure your design is specified correctly?

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 quote

Eta 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.01Small
0.06Medium
0.14Large

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

SituationHow to write it
One-way, significantF(2, 87) = 12.44, p < .001, η² = .22
Welch'sWelch's F(2, 54.3) = 8.91, p = .001
InteractionF(1, 84) = 9.44, p = .003, partial η² = .10
Post-hocTukey HSD indicated C > A (p = .002)
Both degrees of freedom, between then within
Fractional df for Welch's — report as given
An effect size for every F
Which post-hoc test and why
Whether a sphericity correction was applied, for repeated measures
Have your analysis and code reviewed

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 consultancy

Answers

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().