To run a one-way ANOVA in R, you fit the model with aov(outcome ~ group, data = df), read the omnibus F test with summary(), check homogeneity of variance and normality of the residuals, then follow a significant result with TukeyHSD() and an effect size R does not print by default. The model that survives examination depends on choosing the right follow-up test and switching to a Welch ANOVA when variances are unequal. This guide walks the full workflow with the exact code to keep.
When a one-way ANOVA is the right test
A one-way analysis of variance compares the means of three or more independent groups on one continuous outcome, using a single categorical predictor. It answers whether any group mean differs by more than chance, not which groups differ, which is the job of post hoc comparisons. With only two groups the matching test is a t-test, and the relationship between the two is set out in ANOVA versus the t-test. With two grouping factors at once you move to a two-way ANOVA in R, and when the same participants are measured repeatedly you need a repeated measures ANOVA.
Preparing the data and setting the factor
The single most common mistake is leaving the grouping variable as a number or character. R only treats a predictor as categorical when it is a factor, so convert it first and confirm the levels:
df$group <- factor(df$group)
levels(df$group)
table(df$group) # check the cell sizesInspect the group sizes before anything else. Badly unbalanced cells change which sum-of-squares and which follow-up test you should trust, and a single mislabeled level can silently split one group into two.
Fitting the model and reading the F test
Fit the model with aov() and read it with summary(). The output gives the degrees of freedom, the mean squares, the F ratio, and the p-value against an alpha of .05:
model <- aov(score ~ group, data = df)
summary(model)The group row carries the between-groups variation and the Residuals row the within-groups variation. A significant F means at least one mean stands apart, but you still cannot report the model until the assumptions are checked.
Checking the assumptions in R
One-way ANOVA assumes the residuals are roughly normally distributed, the groups share a common variance (homogeneity of variance), and observations are independent. Test variance with Levene's test from the car package, which is less sensitive to non-normality than Bartlett's, and check normality on the model residuals rather than the raw data:
library(car)
leveneTest(score ~ group, data = df) # want p > .05
shapiro.test(residuals(model)) # normality of residuals
plot(model, which = 1) # residuals vs fitted
plot(model, which = 2) # normal Q-QReading the diagnostic plots is the wider skill behind every model, and the same logic of testing residuals applies in fitting a linear model in R. If Levene's test is significant, do not report the standard F.
The Welch ANOVA when variances are unequal
When Levene's test is significant, the equal-variance F is invalid. The fix is a Welch ANOVA, which does not assume equal variances and is run in one line:
oneway.test(score ~ group, data = df, var.equal = FALSE)Report this Welch F instead of the aov() result, and say in your write-up that you did so because the homogeneity assumption failed. Examiners reward the switch far more than they would a standard ANOVA run on data that did not earn it.
Post hoc comparisons after a significant ANOVA
A significant omnibus test only says one mean differs, so a post hoc test locates the differences while holding the family-wise error rate. With equal variances, Tukey's HSD is the standard, well-powered choice:
TukeyHSD(model)
# Unequal variances: Games-Howell via the rstatix package
library(rstatix)
games_howell_test(df, score ~ group)Use Tukey when variances are equal and Games-Howell when they are not, the same rule you would apply choosing between parametric and rank-based methods in parametric versus nonparametric tests.
Getting an effect size R does not print
The summary() table gives no effect size, yet one belongs beside every F. The effectsize package computes eta squared and the less biased omega squared directly from the fitted model:
library(effectsize)
eta_squared(model) # proportion of variance explained
omega_squared(model) # less biased on small samplesEta squared is the between-groups sum of squares over the total; omega squared corrects its upward bias and is preferable on small samples. Why an effect size matters at all is covered in effect size explained.
Reporting a one-way ANOVA in APA style
Lead with the group descriptives, then the omnibus result as F(between df, within df) = value, the exact p-value, and the effect size, followed by the post hoc findings. A clean sentence reads: a one-way ANOVA showed a significant effect of teaching method on test score, F(2, 87) = 6.41, p = .003, omega squared = .11; Tukey comparisons showed the blended group scored higher than the lecture group (p = .002). State when you read the Welch F because Levene's test was significant. The full pattern sits in the APA conventions for a results sentence, and reporting goes into the wider results section of a research paper.
What to do when the assumptions fail badly
If normality is badly violated on small groups and even the Welch test is not enough, the rank-based alternative is the Kruskal-Wallis test, run with kruskal.test(score ~ group, data = df) and followed by Dunn's pairwise comparisons. Switch deliberately, report why, and the analysis stays defensible whatever the data look like. If you are still deciding which procedure fits your design, start with how to choose a statistical test.