How to Do an ANOVA in R: A Step‑by‑Step Guide for Beginners and Intermediate Users
Performing an analysis of variance (ANOVA) in R is a fundamental skill for anyone working with experimental or observational data. Even so, whether you are comparing the means of three or more groups, testing the interaction between two factors, or simply verifying that your treatment effects are statistically significant, R provides a flexible and powerful environment to carry out these tests. This article walks you through the entire workflow—from data preparation and assumption checking to model fitting, post‑hoc comparisons, and result reporting—so you can confidently answer the question “how to do an ANOVA in R” and apply it to your own projects That's the whole idea..
Understanding ANOVA and Its Variants
Before diving into code, it helps to recall what ANOVA does. ANOVA (Analysis of Variance) partitions the total variance in a dataset into components attributable to different sources (e.That said, g. , treatment groups, blocks, or interaction effects) and tests whether any of those sources explain a significant amount of variation beyond random error.
- One‑way ANOVA compares means across a single factor with two or more levels.
- Two‑way ANOVA evaluates the main effects of two factors and their interaction.
- Repeated‑measures or mixed‑effects ANOVA handles correlated observations (e.g., longitudinal data).
In R, the base aov() function and the lm() (linear model) framework are the most common ways to fit ANOVA models, while packages like car, emmeans, and ggplot2 extend functionality for diagnostics, post‑hoc testing, and visualization Nothing fancy..
Preparing Your Data in R
Proper data structure is crucial. ANOVA expects a tidy format where each row corresponds to a single observation and columns represent the response variable and predictor(s) Took long enough..
# Example: loading a built‑in dataset
data(PlantGrowth) # contains weight (response) and group (factor)
# Inspect the first few rows
head(PlantGrowth)
If you are importing your own data, use read.csv(), read_excel() (from readxl), or read_delim() (from readr) and then check that categorical variables are declared as factors:
df <- read.csv("my_experiment.csv")
df$treatment <- factor(df$treatment) # force factor type
df$block <- factor(df$block)
Check for missing values (NA) and decide whether to remove or impute them, because aov() will drop rows with any NA in the model terms Worth keeping that in mind..
One‑Way ANOVA in R
Step 1: Fit the Model
The simplest way to run a one‑way ANOVA is with aov():
oneway_mod <- aov(weight ~ group, data = PlantGrowth)
summary(oneway_mod)
The summary() output provides the ANOVA table: Degrees of Freedom (Df), Sum Sq, Mean Sq, F value, and Pr(>F) (the p‑value). A small p‑value (typically < 0.05) indicates that at least one group mean differs from the others No workaround needed..
Step 2: Check Model Assumptions
ANOVA relies on three key assumptions:
- Independence of observations (usually satisfied by study design).
- Normality of residuals.
- Homogeneity of variances (equal spread across groups).
You can assess these with diagnostic plots and formal tests:
# Extract residuals
res <- residuals(oneway_mod)
# Normality: QQ plot and Shapiro‑Wilk test
qqnorm(res); qqline(res, col = "red")
shapiro.test(res)
# Homogeneity of variances: Levene's test (from car)
library(car)
leveneTest(weight ~ group, data = PlantGrowth)
If normality is violated, consider a transformation (e.g.Consider this: , log or sqrt) or a non‑parametric alternative like the Kruskal‑Wallis test (kruskal. test()). If variances are heterogeneous, you may use oneway.test() with var.equal = FALSE or fit a linear model with heteroscedasticity‑consistent standard errors (sandwich package) That alone is useful..
Step 3: Post‑hoc Comparisons
When the overall F‑test is significant, you need to know which specific groups differ. Tukey’s Honest Significant Difference (HSD) test is the default choice:
TukeyHSD(oneway_mod)
The output lists pairwise comparisons, confidence intervals, and adjusted p‑values. For a more compact view, you can use the emmeans package:
library(emmeans)
pairs(emmeans(oneway_mod, ~ group))
Two‑Way ANOVA in R
Step 1: Fit the Model with Interaction
Suppose you have two factors, factorA and factorB, and a continuous response y. The model including their interaction is:
twoway_mod <- aov(y ~ factorA * factorB, data = mydata)
summary(twoway_mod)
The asterisk * expands to factorA + factorB + factorA:factorB, giving you main effects and the interaction term.
Step 2: Assumption Checks
Run the same residual diagnostics as before:
res2 <- residuals(twoway_mod)
qqnorm(res2); qqline(res2, col = "blue")
shapiro.test(res2)
leveneTest(y ~ factorA * factorB, data = mydata)
If the interaction is significant, interpret it carefully: the effect of one factor depends on the level of the other.
Step 3: Simple Effects and Post‑hoc Tests
When an interaction exists, you often examine simple effects—the effect of one factor at each level of the other. Using emmeans simplifies this:
library(emmeans)
emm <- emmeans(twoway_mod, ~ factorA | factorB) # effect of A within each B
pairs(emm) # pairwise comparisons
You can also test the interaction directly:
contrast(emm, method = "pairwise", adjust = "tukey")
Visualizing ANOVA Results
Graphical checks complement numerical tests and make your findings accessible Small thing, real impact..
Boxplots (or Violin Plots)
library(ggplot2)
ggplot(PlantGrowth, aes(x = group, y = weight)) +
geom_boxplot(fill = "lightblue", alpha = 0.7) +
geom_jitter(width = 0.1, size = 1.5, color = "darkgray") +
labs(title = "Plant Growth by Treatment Group",
ylab("Average Weight") +
theme_minimal()
For two‑way designs, interaction plots are particularly insightful:
```r
interaction.plot(mydata$factorA, mydata$factorB, mydata$y,
type = "b", col = c("red", "blue"),
pch = 1:2, xlab = "Factor A",
ylab = "Mean Response")
Alternatively, ggplot2 offers a more modern approach:
ggplot(mydata, aes(x = factorA, y = y, color = factorB)) +
geom_point() +
geom_line(aes(group = factorB)) +
facet_grid(. ~ factorB)
Reporting ANOVA Results
When writing up your analysis, always include:
- Model specification (e.g., one‑way, two‑way with interaction).
- Assumption checks and any transformations applied.
- F‑statistic, degrees of freedom, and p‑value from
summary(). - Effect size (e.g., η² from
eta_sq()in effectsize). - Post‑hoc results if the main effect is significant, noting which groups differ.
- Interaction interpretation for factorial designs, including simple effects when relevant.
Example paragraph:
A one‑way ANOVA revealed a significant effect of treatment on plant weight, F(2, 27) = 4.25. Tukey’s HSD indicated that the fertilized group (M = 1.8, SD = 0.3) grew significantly more than the control group (M = 0.57, p = .Consider this: 2, SD = 0. 020, η² = 0.In practice, 2), p = . 015, while the unfertilized group (M = 0.9, SD = 0.3) did not differ significantly from either.
Conclusion
ANOVA is a versatile and widely used framework for comparing means across multiple groups. R provides a complete ecosystem—from model fitting and assumption diagnostics to post‑hoc comparisons and visualization—for both one‑way and factorial designs. Day to day, the key to a reliable analysis lies in carefully checking assumptions, interpreting interactions in context, and communicating results with clarity. By integrating the tools and techniques outlined here, you can confidently apply ANOVA to a broad range of experimental and observational data.
Below the basic ANOVA workflow, R offers a suite of extensions that let you tackle more complex experimental structures while keeping the inferential core intact.
Estimated marginal means and pairwise contrasts
The code you inserted (contrast(emm, method = "pairwise", adjust = "tukey")) is a convenient entry point for obtaining adjusted pairwise differences from an emmeans object. After fitting a linear model with aov() or lm(), you can create the emmeans object and then request the contrasts in a single line:
library(emmeans)
emm <- emmeans(fit, ~ group) # fit <- aov(weight ~ group, data = PlantGrowth)
pairwise <- contrast(emm, method = "pairwise", adjust = "tukey")
print(pairwise)
The resulting table supplies the estimated mean difference, standard error, confidence interval, and the adjusted p‑value, which can be quoted directly in the manuscript.
Mixed‑effects ANOVA for repeated measures
When observations are nested within subjects (e.That's why g. , the same plant measured before and after treatment), a conventional one‑way ANOVA can be misleading Easy to understand, harder to ignore..
library(lme4)
fit_me <- lmer(weight ~ group + (1 | plant_id), data = PlantGrowth)
anova(fit_me) # ANOVA table with Satterthwaite df
The lmerTest extension adds an F‑statistic and associated p‑value, mirroring the output of aov(). For fully crossed within‑subject designs, you may specify (1 | subject) and (1 | subject:period) to capture the appropriate error terms Turns out it matters..
Diagnostic tools beyond residuals
The classic residual‑vs‑fitted plot is only the first check. The DHARMa package generates simulated residuals that are especially handy for mixed models:
library(DHARMa)
sim_res <- simulateResiduals(fit_me, n = 250)
plot(sim_res) # residual vs. predicted, QQ plot, etc.
If the simulated residuals reveal over‑dispersion or heteroscedasticity, consider a variance‑stabilizing transformation (log, square‑root) or a generalized linear mixed model (glmer) with an appropriate error distribution.
Effect‑size metrics for mixed models
Effect size reporting becomes more nuanced when random effects are involved. The performance package offers several options:
library(performance)
r2_nakagawa(fit_me) # marginal and conditional R²
cohens_d(fit_me) # Cohen's d for fixed effects
These statistics complement the F‑test and help convey the practical magnitude of the treatment effect, especially when the denominator degrees of freedom are approximated.
Visualizing mixed‑effects results
ggplot2 works without friction with lme4 objects via the ggplot2‑compatible geom_errorbar or stat_summary layers. For a two‑way crossed design with a random subject effect, you might plot the estimated marginal means with 95 % confidence bands:
library(emmeans)
emm_me <- emmeans(fit_me, ~ group * period)
as.data.frame(emm_me) %>%
ggplot(aes(x = group, y = emmean, colour = period, group = period)) +
geom_point() +
geom_line() +
geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = .2) +
labs(title = "Growth Trajectories by Treatment and Period",
x = "Treatment", y = "Weight") +
theme_minimal()
Such plots make the interaction between treatment and time (or any other within‑subject factor) immediately visible.
Automated reporting with R Markdown
To streamline the production of reproducible manuscripts, embed the analysis chunks within an R Markdown document. The knitr engine will render the model summary, diagnostic plots, and post‑hoc tables directly into the final PDF or HTML, ensuring that the numbers in the text match the underlying code. A minimal template might look like:
---
title: "Plant Growth Experiment"
output: pdf_document
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(lme4); library(emmeans); library(ggplot2); library(DHARMa)
Model fitting
fit <- lmer(weight ~ group + (1|plant_id), data = PlantGrowth)
summary(fit)
Assumption checks
sim_res <- simulateResiduals(fit)
plot(sim_res)
Post‑hoc contrasts
emm <- emmeans(fit, ~ group)
contrast(emm, method = "pairwise", adjust = "tukey")
Visualization
emm_df <- as.data.frame(emm)
ggplot(emm_df, aes(x = group, y = emmean, colour = group)) +
geom_point(size = 2) +
geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = .2) +
labs(title = "Treatment Effects with 95 % CI", y = "Weight") +
theme_minimal()
Running the document reproduces every table and figure, guaranteeing transparency and reproducibility.
### Final take‑away
ANOVA remains a cornerstone for comparing group means, and R equips you with a comprehensive toolbox that extends far beyond the basic one‑way layout. By integrating `emmeans` for precise pairwise comparisons, `lme4` (or `nlme`) for hierarchical data structures, `DHARMa` for strong diagnostics, and effect‑size estimators from `performance`, you can conduct a fully vetted analysis that respects both statistical rigor and scientific clarity. When these components are woven together—often within a single R Markdown workflow—the resulting report is not only statistically sound but also readily interpretable by peers and stakeholders alike.
Quick note before moving on.