Before you start, make sure the ez package is installed.
If library(ez) gives you an error, run this once in the
console:
install.packages("ez")
# load libraries
library(data.table)
library(ggplot2)
library(ez)
# clean work space
rm(list = ls())
# init colorscheme
COL <- c("#2271B2", "#E69F00", "#D55E00")
names(COL) <- c("blue", "orange", "red")
theme_set(
theme_minimal(base_size = 13) +
theme(
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold"),
legend.position = "bottom"
)
)
update_geom_defaults("point", list(size = 2))
update_geom_defaults("line", list(linewidth = 0.8))
So far, you have used t-tests to compare two means. In this tutorial, you will learn what to do when you have three or more independent groups.
If you ran every possible pairwise t-test, your chance of making at least one false positive, also called a Type I error, would increase across the set of tests. One-way ANOVA solves this problem by testing all group means together in a single omnibus F-test.
In this tutorial, you will work with a simulated attention-training dataset in which different participants completed one of three training schedules:
briefmoderateextendedYour dependent variable will be mean response time
(mean_rt). Each row in the dataset is one participant, and
each participant appears in only one training condition. That makes this
a true one-factor, between-subjects design.
d <- fread("data/attention_training_summary.csv")
str(d)
## Classes 'data.table' and 'data.frame': 36 obs. of 4 variables:
## $ participant_id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ training_condition: chr "brief" "brief" "brief" "brief" ...
## $ mean_rt : num 712 690 702 725 694 ...
## $ prop_correct : num 0.79 0.82 0.8 0.78 0.81 0.77 0.8 0.82 0.78 0.79 ...
## - attr(*, ".internal.selfref")=<externalptr>
head(d)
## participant_id training_condition mean_rt prop_correct
## <int> <char> <num> <num>
## 1: 1 brief 712.4 0.79
## 2: 2 brief 689.7 0.82
## 3: 3 brief 701.9 0.80
## 4: 4 brief 725.3 0.78
## 5: 5 brief 694.5 0.81
## 6: 6 brief 718.2 0.77
This dataset contains one row per participant and four variables:
participant_id - participant identifiertraining_condition - the training schedule assigned to
that participantmean_rt - the participant’s mean response time in
millisecondsprop_correct - the participant’s mean proportion
correctBefore doing any inferential test, always check the unit of analysis. The unit of analysis is the thing that each row represents in the dataset you give to the test. For this ANOVA, each row is one participant, not one trial. That matters because a between-subjects one-way ANOVA assumes that the rows are independent observations. If we used every trial as a separate row, the same participant would appear many times and those rows would not be independent.
Here, the unit of analysis is the participant. Each participant
contributes one summary score (mean_rt) and belongs to one
training condition. That is the correct level for a between-subjects
one-way ANOVA.
For the rest of this tutorial, focus on response time.
d[, training_condition := factor(
training_condition,
levels = c("brief", "moderate", "extended")
)]
d[, participant_id := factor(participant_id)]
Start with a boxplot.
ggplot(d, aes(x = training_condition, y = mean_rt, fill = training_condition)) +
geom_boxplot(outlier.shape = NA) +
geom_jitter(width = 0.12, alpha = 0.5) +
scale_fill_manual(values = COL) +
labs(
x = "Training condition",
y = "Mean response time (ms)",
title = "Response time by attention-training condition",
fill = "Condition"
) +
theme(legend.position = "none")
Mean response time by training condition.
Now compute the group means and standard errors.
d_sum <- d[, .(
mean_rt = mean(mean_rt),
se_rt = sd(mean_rt) / sqrt(.N)
), by = training_condition]
d_sum
## training_condition mean_rt se_rt
## <fctr> <num> <num>
## 1: brief 707.2667 3.423235
## 2: moderate 652.9833 2.484980
## 3: extended 596.3333 2.177374
ggplot(d_sum, aes(x = training_condition, y = mean_rt, fill = training_condition)) +
geom_col(width = 0.6) +
geom_errorbar(
aes(ymin = mean_rt - se_rt, ymax = mean_rt + se_rt),
width = 0.15
) +
scale_fill_manual(values = COL) +
labs(
x = "Training condition",
y = "Mean response time (ms)",
title = "Mean response time by condition",
fill = "Condition"
) +
theme(legend.position = "none")
Group means with standard error bars.
From the plots, it looks as though response time decreases as training becomes more extensive. The next question is whether those differences are large enough to be statistically reliable.
With three groups, there are three pairwise comparisons:
You could run three independent-samples t-tests:
t.test(mean_rt ~ training_condition,
data = d[training_condition %in% c("brief", "moderate")],
var.equal = TRUE)
##
## Two Sample t-test
##
## data: mean_rt by training_condition
## t = 12.833, df = 22, p-value = 1.085e-11
## alternative hypothesis: true difference in means between group brief and group moderate is not equal to 0
## 95 percent confidence interval:
## 45.51066 63.05600
## sample estimates:
## mean in group brief mean in group moderate
## 707.2667 652.9833
t.test(mean_rt ~ training_condition,
data = d[training_condition %in% c("brief", "extended")],
var.equal = TRUE)
##
## Two Sample t-test
##
## data: mean_rt by training_condition
## t = 27.343, df = 22, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group brief and group extended is not equal to 0
## 95 percent confidence interval:
## 102.5196 119.3471
## sample estimates:
## mean in group brief mean in group extended
## 707.2667 596.3333
t.test(mean_rt ~ training_condition,
data = d[training_condition %in% c("moderate", "extended")],
var.equal = TRUE)
##
## Two Sample t-test
##
## data: mean_rt by training_condition
## t = 17.146, df = 22, p-value = 3.239e-14
## alternative hypothesis: true difference in means between group moderate and group extended is not equal to 0
## 95 percent confidence interval:
## 49.79803 63.50197
## sample estimates:
## mean in group moderate mean in group extended
## 652.9833 596.3333
A false positive is a Type I error: you conclude that there is a difference when, in reality, the null hypothesis is true. If you use an alpha level of .05, then one test has a 5% chance of producing a false positive when there is no real effect.
The problem is that this false-positive risk accumulates across a set
of tests. If you run three separate t-tests, you have three chances to
make a false positive error. Even if every null hypothesis is true, the
chance that at least one test gives p < .05 is larger
than 5%.
If the tests were independent, the probability of at least one false positive across three tests would be:
\[1 - (1 - .05)^3 = .143\]
That is about 14%, not 5%.
Here is the same idea using a simulation. In this simulation, we run three independent t-tests where all samples come from exactly the same population, so every “significant” result is a false positive.
set.seed(2026)
n_sim <- 5000
alpha <- .05
n_per_group <- 12
any_false_positive <- replicate(n_sim, {
p_values <- c(
t.test(rnorm(n_per_group), rnorm(n_per_group), var.equal = TRUE)$p.value,
t.test(rnorm(n_per_group), rnorm(n_per_group), var.equal = TRUE)$p.value,
t.test(rnorm(n_per_group), rnorm(n_per_group), var.equal = TRUE)$p.value
)
any(p_values < alpha)
})
mean(any_false_positive)
## [1] 0.141
The exact answer changes slightly from simulation to simulation, but it should be close to .143 and clearly bigger than .05. One-way ANOVA avoids starting with three separate tests by giving you a single omnibus test of:
\[H_0: \mu_{brief} = \mu_{moderate} = \mu_{extended}\]
Planned comparisons are a special case. If you decide before looking at the data that you only care about one specific comparison, that test is not the same as running every possible pairwise comparison and searching for significance. But if you plan several comparisons, the false-positive risk still accumulates across that planned set, so some form of correction may still be appropriate. The key distinction is whether the comparison was specified in advance or chosen after inspecting the results.
If the omnibus test is significant, you then follow it up with corrected post-hoc comparisons.
In the lectures, we use ezANOVA() from the
ez package. For a one-way between-subjects ANOVA, we
specify:
data - the data table containing the datadv - the dependent variablewid - the participant identifierwithin = NULL - there are no within-subjects factors in
this designbetween - the between-subjects factortype = 3 - Type III sums of squares, which we will use
throughout ANOVAfit <- ezANOVA(
data = d,
dv = mean_rt,
wid = .(participant_id),
within = NULL,
between = .(training_condition),
type = 3,
return_aov = TRUE
)
fit$ANOVA
## Effect DFn DFd F p p<.05 ges
## 2 training_condition 2 33 407.8289 5.38755e-24 * 0.9611151
fit$`Levene's Test for Homogeneity of Variance`
## DFn DFd SSn SSd F p p<.05
## 1 2 33 88.61556 725.6933 2.014841 0.1494188
How to read the output:
Effect is the factor being testedDFn is the numerator degrees of freedom for the
effectDFd is the denominator degrees of freedom for the error
termF is the ANOVA test statisticp is the p-value for the omnibus testp<.05 marks whether the result is significant at the
.05 levelges is generalised eta-squared, a measure of effect
sizeIf the p-value is small, you can conclude that not all three group means are equal.
If the omnibus ANOVA is significant, you still need to ask which groups differ. Tukey’s Honestly Significant Difference test is a standard choice for all pairwise comparisons in a one-way ANOVA.
Because we used return_aov = TRUE above, the
ezANOVA() result includes the underlying aov
object. We can use that object for Tukey’s test.
TukeyHSD(fit$aov)
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = formula(aov_formula), data = data)
##
## $training_condition
## diff lwr upr p adj
## moderate-brief -54.28333 -63.81523 -44.75144 0
## extended-brief -110.93333 -120.46523 -101.40144 0
## extended-moderate -56.65000 -66.18189 -47.11811 0
This output gives the mean difference for each pair, a confidence interval, and an adjusted p-value that controls the family-wise error rate.
Sometimes R prints very small p-values as 0. This does
not mean the probability is exactly zero. Report these as very small,
for example p < .001.
Interpretation guide:
One-way ANOVA depends on four main assumptions:
The lectures show how to extract residuals from
ezANOVA() using return_aov = TRUE. Once you
have the residuals, you can check their distribution visually. Here we
use a Q-Q plot as another visual check of normality.
d_resid <- data.table(resid = residuals(fit$aov))
ggplot(d_resid, aes(sample = resid)) +
stat_qq(colour = COL[1]) +
stat_qq_line(colour = COL[2]) +
labs(
x = "Theoretical quantiles",
y = "Sample quantiles",
title = "Q-Q plot of residuals"
)
Q-Q plot of ANOVA residuals.
Levene’s test is a common test for homogeneity of variance, and it is
provided in the ezANOVA() output.
fit$`Levene's Test for Homogeneity of Variance`
## DFn DFd SSn SSd F p p<.05
## 1 2 33 88.61556 725.6933 2.014841 0.1494188
Use these checks to support your interpretation, not to replace it. With moderate group sizes, ANOVA is reasonably robust to mild deviations, but you should still inspect the assumptions rather than ignore them.
Your assigned dataset is determined by your student ID number. Take
the last digit and compute last_digit %% 3 in R.
0: https://github.com/crossley/cogs2020/tree/main/final_project_data/cat_learn_switch1: https://github.com/crossley/cogs2020/tree/main/final_project_data/cat_learn_auto2: https://github.com/crossley/cogs2020/tree/main/final_project_data/cat_learn_unlearnBefore you start, check whether your assigned dataset can support an independent-groups question this week. All of the final project datasets include at least one within-subjects factor, so everyone needs to be careful about independence. If the same participant contributes scores across multiple conditions, blocks, or sessions, those rows are repeated measures and should not be treated as independent groups in a between-subjects one-way ANOVA. Do not use a within-subjects factor as the one-way ANOVA factor this week. You may need to choose a genuinely between-subjects question, aggregate to one independent score per participant or unit, or wait for the repeated-measures ANOVA in the next tutorial.
If your question uses independent groups, load your assigned dataset
and run a one-way ANOVA using ezANOVA(). Specifically:
ezANOVA().