ANOVA in R: One-Way & Two-Way Test with Examples

โšก Smart Summary

ANOVA in R compares the means of three or more groups by splitting total variation into between-group and within-group components. This walkthrough runs one-way and two-way tests on the poisons dataset, checks assumptions, and isolates differing pairs with Tukey HSD.

  • ๐Ÿ“ Core Statistic: The F-ratio divides between-group variability by within-group variability, so larger values argue against equal means.
  • ๐Ÿงช One-Way Syntax: aov(time~poison, data = df) followed by summary() returns the degrees of freedom, F value, and p-value.
  • โœ… Assumption Checks: Test independence, normality of residuals with shapiro.test(), and equal variance with leveneTest() before trusting the p-value.
  • ๐Ÿ”Ž Post Hoc Analysis: TukeyHSD() compares every pair of groups while holding the family-wise error rate at the stated level.
  • ๐Ÿงฎ Two-Way Extension: aov(time~poison + treat, data = df) adds a second factor, and poison:treat adds the interaction term.
  • ๐Ÿ“Š Reading Results: A p-value below 0.05 rejects the null hypothesis of equal means but never identifies which group differs.

ANOVA in R One Way Two Way

What is ANOVA?

Analysis of Variance (ANOVA) is a statistical technique used to compare the means of two or more groups. The test works by splitting the total variation in a measurement into the part explained by group membership and the part left over as random noise. ANOVA in R therefore tells you whether at least one group mean differs from the others, not which one. It is a direct extension of the t-test to situations where the factor variable has more than two levels.

Before running a test, it helps to know which member of the ANOVA family fits your design.

Types of ANOVA Tests in R

“ANOVA” is a family of tests rather than a single procedure. Choosing the right member depends on how many factors you have and how the data were collected.

Test When to use it R call
One-way ANOVA One factor with three or more levels aov(y ~ x, data = df)
Two-way ANOVA Two independent factors aov(y ~ x1 + x2, data = df)
Two-way with interaction The effect of one factor depends on the other aov(y ~ x1 * x2, data = df)
Repeated measures ANOVA The same subjects measured more than once aov(y ~ x + Error(subject/x))
ANCOVA A continuous covariate must be controlled for aov(y ~ x + covariate, data = df)
MANOVA Two or more response variables at once manova(cbind(y1, y2) ~ x)

This tutorial covers the first three. The remaining variants use the same aov() interface, so once you can read one output table you can read them all.

ANOVA vs T-Test in R: Key Differences

Both tests compare means, so it is worth being precise about where one replaces the other.

Criteria T-Test ANOVA
Number of groups Exactly two Two or more
Test statistic t F, equal to t squared when there are two groups
Result Names the direction of the difference Only reports that a difference exists
Follow-up needed None Post hoc test such as Tukey HSD
R function t.test() aov()

The temptation with three groups is to run three separate t-tests. Resist it. Each test carries its own 5 percent error rate, so three comparisons push the chance of a false positive to roughly 14 percent. ANOVA answers the same question with a single test, and Tukey HSD then handles the pairwise detail with the error rate held in check. For the two-group case, see the t-test tutorial.

One-way ANOVA

There are many situations where you need to compare the mean between multiple groups. For instance, the marketing department wants to know if three teams have the same sales performance.

  • Team: 3 level factor: A, B, and C
  • Sale: A measure of performance

The ANOVA test can tell if the three groups have similar performances.

To clarify if the data comes from the same population, you can perform a one-way analysis of variance (one-way ANOVA hereafter). Like any other statistical test, it gives evidence about whether the H0 hypothesis can be rejected. Note that failing to reject H0 is not the same as proving it true.

Hypothesis in one-way ANOVA test

  • H0: The means between groups are identical
  • H1: At least, the mean of one group is different

In other words, failing to reject H0 means there is not enough evidence to conclude that any group mean differs from the others.

This test is similar to the t-test, but ANOVA is the correct choice when there are more than two groups. With exactly two groups, the two tests are equivalent and the F-statistic equals the square of the t-statistic.

Assumptions

The one-way ANOVA rests on three conditions: observations are randomly sampled and independent of one another, the residuals within each group are approximately normally distributed, and the variance is the same in every group (homogeneity of variance). The section on checking assumptions below shows how to test each one in R.

Interpret ANOVA test

The F-statistic is used to test if the data are from significantly different populations, i.e., different sample means.

To compute the F-statistic, you need to divide the between-group variability over the within-group variability.

The between-group variability reflects how far each group mean sits from the overall mean. Compare the two graphs below to see the idea.

The left graph shows very little variation between the three groups, so all three group means sit close to the overall mean.

The right graph plots three distributions far apart with no overlap, so the gap between the overall mean and each group mean is large.

Interpret ANOVA test

The within-group variability measures how far individual observations fall from the mean of their own group. Some points sit far from their group average, and the within-group term captures exactly that spread, which is the sampling error.

To understand visually the concept of within group variability, look at the graph below.

The left part plots the distribution of three different groups. You increased the spread of each sample and it is clear the individual variance is large. The F-statistic falls, so you would fail to reject the null hypothesis

The right part shows samples with the same means but much lower spread. That raises the F-statistic and points in favour of the alternative hypothesis.

Interpret ANOVA test

You can use both measures to construct the F-statistics. It is very intuitive to understand the F-statistic. If the numerator increases, it means the between-group variability is high, and it is likely the groups in the sample are drawn from completely different distributions.

In other words, a low F-statistic indicates little or no meaningful difference between the group averages.

Example One way ANOVA Test

You will use the poison dataset to implement the one-way ANOVA test. The dataset contains 48 rows and 3 variables:

  • Time: Survival time of the animal
  • poison: Type of poison used: factor level: 1,2 and 3
  • treat: Type of treatment used: factor level: 1,2 and 3

Before you start to compute the ANOVA test, you need to prepare the data as follow:

  • Step 1: Import the data
  • Step 2: Remove unnecessary variable
  • Step 3: Convert the variable poison as ordered level
library(dplyr)
PATH <- "https://raw.githubusercontent.com/guru99-edu/R-Programming/master/poisons.csv"
df <- read.csv(PATH) %>%
select(-X) %>% 
mutate(poison = factor(poison, ordered = TRUE))
glimpse(df)

Output:

## Observations: 48
## Variables: 3
## $ time   <dbl> 0.31, 0.45, 0.46, 0.43, 0.36, 0.29, 0.40, 0.23, 0.22, 0...
## $ poison <ord> 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2...
## $ treat  <fctr> A, A, A, A, A, A, A, A, A, A, A, A, B, B, B, B, B, B, ...		

Our objective is to test the following assumption:

  • H0: There is no difference in survival time average between group
  • H1: The survival time average is different for at least one group.

In other words, you want to know if there is a statistical difference between the mean of the survival time according to the type of poison given to the Guinea pig.

You will proceed as follow:

  • Step 1: Check the format of the variable poison
  • Step 2: Print the summary statistic: count, mean and standard deviation
  • Step 3: Plot a box plot
  • Step 4: Compute the one-way ANOVA test
  • Step 5: Run a pairwise comparison with Tukey HSD

Step 1) Check the levels of poison with the code below. You should see three character values, because the mutate verb converted the column into an ordered factor.

levels(df$poison)

Output:

## [1] "1" "2" "3"

Step 2) You compute the mean and standard deviation.

df %>%
	group_by(poison) %>%
	summarise(
		count_poison = n(),
		mean_time = mean(time, na.rm = TRUE),
		sd_time = sd(time, na.rm = TRUE)
	)

Output:

## 
# A tibble: 3 x 4
##   poison count_poison mean_time    sd_time
##    <ord>        <int>     <dbl>      <dbl>
## 1      1           16  0.617500 0.20942779
## 2      2           16  0.544375 0.28936641
## 3      3           16  0.276250 0.06227627

Step 3) In step three, you can graphically check if there is a difference between the distribution. Note that you include the jittered dot.

ggplot(df, aes(x = poison, y = time, fill = poison)) +
    geom_boxplot() +
    geom_jitter(shape = 15,
        color = "steelblue",
        position = position_jitter(0.21)) +
    theme_classic()

Output:

One way ANOVA Test Example

Step 4) You can run the one-way ANOVA test with the command aov. The basic syntax for an ANOVA test is:

aov(formula, data)
Arguments:			
- formula: The equation you want to estimate
- data: The dataset used	

The syntax of the formula is:

y ~ X1+ X2+...+Xn # X1 +  X2 +... refers to the independent variables
y ~ . # use all the remaining variables as independent variables

You can now answer the question: is there any difference in survival time between the guinea pigs, given the type of poison administered?

Store the model in an object and pass it to summary() to get a readable print of the results.

anova_one_way <- aov(time~poison, data = df)
summary(anova_one_way)

Code Explanation

  • aov(time ~ poison, data = df): Run the ANOVA test with the following formula
  • summary(anova_one_way): Print the summary of the test

Output:

##             Df Sum Sq Mean Sq F value   Pr(>F)
## poison       2  1.033  0.5165   11.79 7.66e-05 ***
## Residuals   45  1.972  0.0438                     
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

The p-value is 7.66e-05, far below the usual threshold of 0.05, and the three stars mark the strongest significance code. You can reject H0 and conclude that at least one poison group has a different mean survival time.

How to Check ANOVA Assumptions in R

An ANOVA p-value is only trustworthy when the three conditions listed earlier hold. Each one has a direct check in R, and all of them run on the fitted model object.

1. Independence of observations. This is a property of the study design, not of the data, so no test can rescue it. Each guinea pig must be measured once and assigned to its group at random. If the same subject appears in several rows, you need a repeated measures model instead.

2. Normality of residuals. ANOVA assumes the residuals, not the raw data, are approximately normal. Inspect the Q-Q plot and confirm with the Shapiro-Wilk test:

par(mfrow = c(2, 2))
plot(anova_one_way)          # four diagnostic plots

shapiro.test(residuals(anova_one_way))

Points hugging the diagonal of the Normal Q-Q plot indicate normal residuals. A Shapiro-Wilk p-value above 0.05 means you cannot reject normality.

3. Homogeneity of variance. Every group should show a similar spread. The Residuals vs Fitted plot should look like a flat band rather than a funnel. Confirm it with Levene’s test, which is more robust to non-normality than Bartlett’s:

library(car)
leveneTest(time ~ poison, data = df)

bartlett.test(time ~ poison, data = df)

A p-value above 0.05 supports equal variances.

What to do when an assumption fails. If variances are unequal, run oneway.test(time ~ poison, data = df, var.equal = FALSE), the Welch correction. If the residuals are clearly non-normal and the sample is small, switch to the Kruskal-Wallis rank test, kruskal.test(time ~ poison, data = df). With large balanced samples, ANOVA is fairly robust to moderate departures from normality, so a borderline Shapiro-Wilk result is rarely fatal.

Pairwise comparison

A significant F-test tells you that the group means are not all equal, but not which pair differs. The Tukey Honest Significant Difference test answers that by comparing every pair while controlling the family-wise error rate.

TukeyHSD(anova_one_way)

Output:

Pairwise comparison

Read the output one row per pair. The diff column holds the difference between the two group means, lwr and upr bound the 95 percent confidence interval for that difference, and p adj is the p-value adjusted for multiple comparisons. A pair differs significantly when its interval excludes zero, equivalently when p adj is below 0.05. In this dataset the comparisons involving poison 3 are the significant ones, which matches the box plot: group 3 has a clearly lower mean survival time than groups 1 and 2, while groups 1 and 2 are statistically indistinguishable from each other.

Two-way ANOVA

A two-way ANOVA adds a second factor to the formula. It works exactly like the one-way test, only the formula changes:

y ~ x1 + x2

Here y is the quantitative response variable, while x1 and x2 are both categorical factors.

Hypothesis in two-way ANOVA test

  • H0: The group means are equal for both factor variables
  • H1: At least one group mean differs, for at least one of the two factors

You add the treat variable to the model. This variable records the treatment given to the guinea pig. The additive formula below tests whether each factor affects survival time on its own, after accounting for the other.

Adjust the code by adding treat alongside the first independent variable.

anova_two_way <- aov(time~poison + treat, data = df)
summary(anova_two_way)

Output:

##             Df Sum Sq Mean Sq F value  Pr(>F)    
## poison       2 1.0330  0.5165   20.64 5.7e-07 ***
## treat        3 0.9212  0.3071   12.27 6.7e-06 ***
## Residuals   42 1.0509  0.0250                    
## ---

Both p-values (5.7e-07 for poison and 6.7e-06 for treat) sit far below 0.05, so you reject H0 for both factors and conclude that changing either the poison or the treatment affects survival time.

Adding an interaction term

The additive model above assumes the effect of the poison is the same regardless of the treatment. To test that assumption, replace the plus sign with an asterisk, which fits both main effects and their interaction:

anova_interaction <- aov(time~poison * treat, data = df)
summary(anova_interaction)

If the poison:treat row is not significant, the additive model is the better choice because it spends fewer degrees of freedom.

ANOVA in R: Quick Test Reference

The table below lists each test used above, the R call that runs it, and the hypothesis it evaluates:

Test Code Hypothesis P-value
One way ANOVA
aov(y ~ X, data = df)
H1: Average is different for at least one group 0.05
Pairwise
TukeyHSD(ANOVA summary)
0.05
Two way ANOVA
aov(y ~ X1 + X2, data = df)
H1: At least one group mean differs for either factor 0.05

FAQs

The F-value is the ratio of between-group variance to within-group variance. Values near 1 suggest the group means are alike. Large values indicate the groups are drawn from populations with different means.

Use oneway.test() with var.equal = FALSE when variances are unequal, and kruskal.test() when residuals are clearly non-normal. Transforming a skewed response with log() often restores both normality and equal variance.

Each t-test carries its own 5 percent false-positive risk. Three pairwise tests raise the family-wise error rate to about 14 percent. ANOVA keeps a single overall test at 5 percent, and Tukey HSD adjusts the pairwise comparisons.

ANOVA is a standard feature-selection filter: it ranks categorical predictors by how strongly they separate a numeric target. AI teams also use it to compare model variants across cross-validation folds.

Yes. AI assistants can explain degrees of freedom, translate p-values into plain language, and flag assumption breaches in diagnostic plots. Always confirm the reading against your own leveneTest() and shapiro.test() results.

Summarize this post with: