One-Way ANOVA in R: Compare Several Group Means

Test whether three or more group means differ — rstatix the modern way, base R for the classics

Biostatistics

Learn to run a one-way ANOVA in R to compare the means of three or more groups. Use rstatix anova_test() for a tidy result with an effect size, and base R aov() + summary() for the classic table — with the normality and equal-variance assumption checks, Tukey HSD post-hoc comparisons (and Games-Howell when variances differ), and a ggpubr boxplot that prints the p-values on the panel.

Published

June 23, 2026

Modified

July 17, 2026

TipKey takeaways
  • A one-way ANOVA compares the means of three or more groups defined by a single factor — it’s the extension of the independent t-test beyond two groups.
  • Use anova_test() from rstatix for a tidy result (F, p, and the ges effect size) or base R aov() + summary() for the classic table. Both compute the same F-test.
  • The omnibus test only says at least one mean differs — follow a significant result with a post-hoc test: tukey_hsd() (equal variances) or games_howell_test() (unequal).
  • Check the two assumptions: normality of residuals (shapiro_test() + Q-Q plot) and homogeneity of variance (levene_test()). If normality fails, use the non-parametric Kruskal-Wallis test instead.
  • Visualize it with ggpubr::ggboxplot() + stat_pvalue_manual() — the boxplot with the comparison p-values printed on the panel.

Introduction

A one-way ANOVA (analysis of variance) answers a question that comes up constantly in experiments: do these several groups have different means, or is the spread just noise? It is the natural extension of the independent two-samples t-test to more than two groups, where the data are split by a single grouping variable (the factor).

The test reports an F-statistic and a p-value for the omnibus hypothesis — that all group means are equal. A significant p-value tells you at least one group differs, but not which one; for that you run a post-hoc pairwise comparison. This lesson runs the whole workflow two ways — the modern, pipe-friendly rstatix anova_test() and the classic base-R aov() — using the built-in PlantGrowth data, and shows the assumption checks that tell you whether to trust the F-test.

Note

ANOVA hypotheses. Null: the means of all groups are equal. Alternative: at least one group mean differs from the others. With exactly two groups, the F-test and the t-test are equivalent.

How one-way ANOVA works

The idea is simple: split the total variation in the outcome into two pieces and compare them.

  1. Within-group variance (the residual variance) — how much each observation differs from its own group mean.
  2. Between-group variance — how much the group means differ from the overall mean.
  3. The F-statistic is the ratio between / within.

A small F (≈ 1) means the groups barely differ relative to their internal spread; a large F means the group means are spread far apart compared with the noise — evidence that at least one mean is genuinely different.

ANOVA partitions the total variation in the outcome into the part between the groups and the part within them. Writing \(y_{ij}\) for observation \(i\) in group \(j\), \(\bar{y}_j\) for that group’s mean and \(\bar{y}\) for the overall (grand) mean, the sums of squares — total (T), between-groups (B) and within-groups (W) — add up exactly:

\[ SS_T = SS_B + SS_W \]

where \(SS_B = \sum_j n_j (\bar{y}_j - \bar{y})^2\) measures how far the group means sit from the grand mean, and \(SS_W = \sum_j \sum_i (y_{ij} - \bar{y}_j)^2\) measures the spread of observations inside their own group (the residual noise).

Each sum of squares becomes a mean square when you divide by its degrees of freedom, \(MS = SS / df\): with \(k\) groups and \(N\) observations, \(df_B = k - 1\) and \(df_W = N - k\). The F-statistic is then the ratio of the two mean squares:

\[ F = \frac{MS_B}{MS_W} \]

If the groups truly share one mean, both mean squares estimate the same noise variance and \(F\) sits near 1; a real between-group difference inflates the numerator and pushes \(F\) above 1. (For PlantGrowth: \(k = 3\), \(N = 30\), so \(df = 2\) and \(27\) — the \(F(2, 27)\) you see in the output.)

The data: three treatment groups

We use the built-in PlantGrowth data — the dried weight of plants grown under a control and two different treatment conditions (ctrl, trt1, trt2), 10 plants per group. The research question is whether the mean plant weight differs across the three conditions. Inspect one random row per group with sample_n_by():

library(rstatix)

data("PlantGrowth")
set.seed(1234)

PlantGrowth %>% sample_n_by(group, size = 1)
# A tibble: 3 × 2
  weight group
   <dbl> <fct>
1   5.14 ctrl 
2   3.83 trt1 
3   5.37 trt2 

R orders factor levels alphabetically by default. Here that already gives ctrl, trt1, trt2 — but it’s good practice to set the order explicitly with reorder_levels() so the control group comes first in every table and plot:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

levels(PlantGrowth$group)
[1] "ctrl" "trt1" "trt2"

Put numbers behind the plot with rstatix get_summary_stats() — the per-group counts, means and standard deviations you’ll report with the test:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

PlantGrowth %>%
  group_by(group) %>%
  get_summary_stats(weight, type = "mean_sd")
# A tibble: 3 × 5
  group variable     n  mean    sd
  <fct> <fct>    <dbl> <dbl> <dbl>
1 ctrl  weight      10  5.03 0.583
2 trt1  weight      10  4.66 0.794
3 trt2  weight      10  5.53 0.443

The control group averages about 5.03, trt1 is a touch lower (4.66) and trt2 is the highest (5.53) — the numbers behind the boxplot that follows.

Look at the data first

Always plot the groups before testing them. A boxplot of weight by group shows the three distributions side by side:

library(ggpubr)
library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

ggboxplot(PlantGrowth, x = "group", y = "weight", fill = "group", palette = "jco",
          xlab = "Treatment", ylab = "Plant weight")

A boxplot of PlantGrowth plant weight by treatment group (ctrl, trt1, trt2). The trt2 group sits highest, trt1 lowest, with overlapping but visibly shifted distributions.

A mean plot with error bars makes the pattern explicit — ggline() plots the group means joined by a line, with the standard error as the band and the raw points jittered on top:

library(ggpubr)
library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

ggline(PlantGrowth, x = "group", y = "weight",
       add = c("mean_se", "jitter"), palette = "jco",
       xlab = "Treatment", ylab = "Plant weight")

A line plot of mean plant weight at each treatment group with standard-error bars and the jittered raw points; trt1 dips below the control and trt2 rises above it.

trt2 sits clearly above the control while trt1 dips slightly below it — the picture suggests a real treatment effect. Let’s confirm it with the test, then verify the assumptions.

Check the assumptions

A one-way ANOVA trusts a few assumptions about the data (beyond independent, random observations): no extreme outliers, the residuals are normally distributed, and the groups have equal variance. Check them before believing the p-value.

Outliers

identify_outliers() flags extreme values in each group with the boxplot rule — an empty result means none would distort the group means:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

PlantGrowth %>%
  group_by(group) %>%
  identify_outliers(weight)
# A tibble: 2 × 4
  group weight is.outlier is.extreme
  <fct>  <dbl> <lgl>      <lgl>     
1 trt1    5.87 TRUE       FALSE     
2 trt1    6.03 TRUE       FALSE     

There were no extreme outliers.

Note

Where you do have extreme outliers, they can come from data-entry or measurement errors, or be genuinely unusual values. You can keep an outlier if you don’t believe it will change the conclusion — compare the ANOVA with and without it — or run a robust ANOVA (e.g. the WRS2 package) that down-weights it.

Normality of residuals — Shapiro-Wilk + Q-Q plot

The normality assumption can be checked two ways: by analysing the model residuals (one check for all groups together — handy with many groups or few points per group), or per group (useful with few groups and many points each). We show both.

On the residuals. Fit the model, extract the residuals, and test them with Shapiro-Wilk (p > 0.05 → no departure from normal), backed by a Q-Q plot — the points should hug the reference line:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

# Shapiro-Wilk on the model residuals
model <- lm(weight ~ group, data = PlantGrowth)
shapiro_test(residuals(model))
# A tibble: 1 × 3
  variable         statistic p.value
  <chr>                <dbl>   <dbl>
1 residuals(model)     0.966   0.438
library(ggpubr)
library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))
model <- lm(weight ~ group, data = PlantGrowth)

ggqqplot(residuals(model))

A Q-Q plot of the ANOVA model residuals against the normal distribution; the points fall close to the 45-degree reference line inside the shaded confidence band, indicating approximately normal residuals.

The Shapiro-Wilk p-value is non-significant (p ≈ 0.13) and the points fall along the line — normality holds.

Per group. With only three groups you can also test each one and draw a faceted Q-Q plot:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

PlantGrowth %>%
  group_by(group) %>%
  shapiro_test(weight)
# A tibble: 3 × 4
  group variable statistic     p
  <fct> <chr>        <dbl> <dbl>
1 ctrl  weight       0.957 0.747
2 trt1  weight       0.930 0.452
3 trt2  weight       0.941 0.564
library(ggpubr)
library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

ggqqplot(PlantGrowth, "weight", facet.by = "group")

Three Q-Q plots of plant weight, one per treatment group; within each panel the points fall approximately along the reference line, indicating normality in every group.

The weights are normally distributed (p > 0.05) in every group.

Note

If your sample size is large (n > 50), prefer the Q-Q plot: Shapiro-Wilk becomes very sensitive to even minor departures from normality at large n. And if you have real doubt about normality, use the non-parametric Kruskal-Wallis test instead.

Homogeneity of variance — Levene’s test

ANOVA also assumes the groups share a common variance. The residuals-vs-fitted plot is the visual check — a roughly flat, even band of points (no funnel shape) means the spread is constant across the groups:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))
model <- aov(weight ~ group, data = PlantGrowth)

plot(model, 1)   # residuals vs fitted — a flat band = equal variance

A residuals-versus-fitted plot of the one-way ANOVA model; the residuals scatter in an even horizontal band around zero across the three fitted group means, with no funnel shape, indicating constant variance.

Back it with Levene’s test, which asks formally whether the group variances are equal (p > 0.05 → no evidence they differ, which is what we want). With rstatix it’s one pipe:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

PlantGrowth %>% levene_test(weight ~ group)
# A tibble: 1 × 4
    df1   df2 statistic     p
  <int> <int>     <dbl> <dbl>
1     2    27      1.12 0.341

A non-significant Levene’s test (p > 0.05) means equal-variance holds, so the classic ANOVA (and Tukey post-hoc) is appropriate. If it were significant, switch to the Welch ANOVA (welch_anova_test()) and the Games-Howell post-hoc test, which don’t assume equal variances — see the relaxing the assumption section below.

Run the one-way ANOVA

With base R

aov() fits the model and summary() prints the classic ANOVA table:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

res.aov.base <- aov(weight ~ group, data = PlantGrowth)
summary(res.aov.base)
            Df Sum Sq Mean Sq F value Pr(>F)  
group        2  3.766  1.8832   4.846 0.0159 *
Residuals   27 10.492  0.3886                 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Read the table: Df are the degrees of freedom, F value is the statistic, and Pr(>F) is the p-value (the stars flag significance). It’s the same F and p as the rstatix call — aov() simply prints the textbook layout and keeps the fitted model for diagnostics and post-hoc tests.

Post-hoc: which groups differ?

A significant ANOVA says some mean differs — to find which pairs, run a post-hoc test that corrects for the multiple comparisons.

Tukey HSD (equal variances)

tukey_hsd() compares every pair and adjusts the p-values (Tukey Honest Significant Differences):

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

PlantGrowth %>% tukey_hsd(weight ~ group)
# A tibble: 3 × 9
  term  group1 group2 null.value estimate conf.low conf.high  p.adj p.adj.signif
* <chr> <chr>  <chr>       <dbl>    <dbl>    <dbl>     <dbl>  <dbl> <chr>       
1 group ctrl   trt1            0   -0.371   -1.06      0.320 0.391  ns          
2 group ctrl   trt2            0    0.494   -0.197     1.19  0.198  ns          
3 group trt1   trt2            0    0.865    0.174     1.56  0.0120 *           

The output gives estimate (the difference between the two group means), conf.low/conf.high (the 95% confidence interval) and p.adj (the adjusted p-value). Only the difference between trt2 and trt1 is significant (adjusted p = 0.012); the control-vs-treatment differences are not.

The base-R equivalent takes the fitted aov() object:

library(rstatix)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

res.aov.base <- aov(weight ~ group, data = PlantGrowth)
TukeyHSD(res.aov.base)
  Tukey multiple comparisons of means
    95% family-wise confidence level

Fit: aov(formula = weight ~ group, data = PlantGrowth)

$group
            diff        lwr       upr     p adj
trt1-ctrl -0.371 -1.0622161 0.3202161 0.3908711
trt2-ctrl  0.494 -0.1972161 1.1852161 0.1979960
trt2-trt1  0.865  0.1737839 1.5562161 0.0120064

Other pairwise options

Tukey HSD isn’t the only post-hoc route. Base R’s pairwise.t.test() runs every pairwise comparison and lets you pick the multiple-comparison correction directly. Use the Benjamini-Hochberg ("BH") method to control the false-discovery rate — the expected proportion of false positives among the significant results — a less conservative alternative to Tukey when you have several pairs:

data("PlantGrowth")

pairwise.t.test(PlantGrowth$weight, PlantGrowth$group, p.adjust.method = "BH")

    Pairwise comparisons using t tests with pooled SD 

data:  PlantGrowth$weight and PlantGrowth$group 

     ctrl  trt1 
trt1 0.194 -    
trt2 0.132 0.013

P value adjustment method: BH 

The result is a triangular matrix of adjusted p-values, one per pair. Only trt2 vs trt1 comes out significant (p ≈ 0.013) — the same conclusion as Tukey: the two treatments differ from each other, but neither differs significantly from the control.

If you already use multcomp for other contrasts, its glht() function gives the same Tukey comparisons as TukeyHSD() from the fitted aov() model — summary(multcomp::glht(res.aov.base, linfct = multcomp::mcp(group = "Tukey"))). It’s worth knowing when you need general linear hypotheses beyond the standard all-pairs Tukey set.

Plot it with the p-values on it

The publication-ready figure puts the test result on the plot — the overall ANOVA in the subtitle (get_test_label()) and the significant pairwise bracket via stat_pvalue_manual(), after add_xy_position() computes where the bracket sits. hide.ns = TRUE hides the non-significant pairs so only the real difference (trt1 vs trt2) is drawn:

library(rstatix)
library(ggpubr)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

res.aov <- PlantGrowth %>% anova_test(weight ~ group)
pwc <- PlantGrowth %>%
  tukey_hsd(weight ~ group) %>%
  add_xy_position(x = "group")

ggboxplot(PlantGrowth, x = "group", y = "weight",
          xlab = "Treatment", ylab = "Plant weight") +
  stat_pvalue_manual(pwc, hide.ns = TRUE) +
  labs(
    subtitle = get_test_label(res.aov, detailed = TRUE),
    caption = get_pwc_label(pwc)
  )

A boxplot of PlantGrowth plant weight by treatment group (ctrl, trt1, trt2) with the overall ANOVA F(2,27)=4.85, p=0.016 and generalized eta-squared = 0.26 printed as a subtitle, and a significance bracket between trt1 and trt2 drawn above the boxes.

This is the figure to report: the omnibus F, degrees of freedom, p-value and effect size up top, and the one significant pair (trt1 vs trt2) marked right on the boxes.

Report the result

A publication-style write-up pulls the omnibus result, the effect size and the significant pairwise comparisons into one sentence — for example:

A one-way ANOVA was performed to compare plant weight across the three treatment groups — control (n = 10), trt1 (n = 10) and trt2 (n = 10). Plant weight differed significantly between groups, F(2, 27) = 4.85, p = 0.016, generalized η² = 0.26. Tukey post-hoc comparisons showed that the increase from trt1 (4.66 ± 0.79) to trt2 (5.53 ± 0.44) was significant (difference 0.87, 95% CI [0.17, 1.56], p.adj = 0.012), with no other pair significantly different.

Report the F-statistic with its degrees of freedom, the p-value, the effect size (ges), and the significant pairwise differences — not just “p < 0.05”.

Relaxing the equal-variance assumption

The classic one-way ANOVA assumes equal variances across groups. Here Levene’s test was non-significant, so it’s fine — but when equal variance fails, the Welch one-way ANOVA (welch_anova_test()) is the alternative that does not require it, paired with the Games-Howell post-hoc test (built on Welch’s correction, no equal-variance or equal-size assumption). The figure below swaps in both — note the same get_test_label() / stat_pvalue_manual() recipe:

library(rstatix)
library(ggpubr)

data("PlantGrowth")
PlantGrowth <- PlantGrowth %>%
  reorder_levels(group, order = c("ctrl", "trt1", "trt2"))

# Welch one-way ANOVA + Games-Howell post-hoc (no equal-variance assumption)
res.aov2 <- PlantGrowth %>% welch_anova_test(weight ~ group)
pwc2 <- PlantGrowth %>%
  games_howell_test(weight ~ group) %>%
  add_xy_position(x = "group", step.increase = 1)

ggboxplot(PlantGrowth, x = "group", y = "weight",
          xlab = "Treatment", ylab = "Plant weight") +
  stat_pvalue_manual(pwc2, hide.ns = TRUE) +
  labs(
    subtitle = get_test_label(res.aov2, detailed = TRUE),
    caption = get_pwc_label(pwc2)
  )

A boxplot of plant weight by treatment group with the Welch one-way ANOVA result in the subtitle and Games-Howell pairwise brackets drawn for the significant comparisons.

Tip

Which post-hoc? Run Levene’s test first. Equal variances → Tukey HSD. Unequal variances → Games-Howell (often the safer default, since it degrades gracefully when variances are equal). You can also use pairwise t-tests with pool.sd = FALSE when variances differ.

Note

If the residuals stray far from the line, or Levene’s test is significant, the data may not fit the classic ANOVA. Use the Welch ANOVA for unequal variances, or the non-parametric Kruskal-Wallis test when normality fails. See the dedicated normality test lesson for the full assumption workflow.

TipWhich test when?

Let the assumptions pick the test:

  • Equal variances + normal residuals → classic one-way ANOVA (anova_test()), with Tukey HSD (tukey_hsd()) post-hoc.
  • Unequal variances (Levene’s test significant) → Welch ANOVA (welch_anova_test()), with Games-Howell (games_howell_test()) post-hoc.
  • Non-normal residuals → the non-parametric Kruskal-Wallis test (kruskal_test()), with Dunn’s test post-hoc.
  • More than one grouping factor → step up to a two-way or three-way ANOVA to test two or three factors and their interactions at once.

Try it live

Run the full workflow on a different dataset — InsectSprays (insect count by spray type, six groups A–F). Edit the columns, the dataset, or the post-hoc test and re-run; the sandbox boots on first Run.

🟢 With an AI agent

Ask Prova “I have a numeric outcome and one grouping column — run the right ANOVA on my data, check the assumptions, and tell me which groups differ” — it answers with rstatix code you can run on your own data, then helps you read the F-test, the effect size and the post-hoc table. The runtime is the judge. Ask Prova →

Common issues

anova_test() reports the factor as numeric / gives 1 degree of freedom. Your grouping variable is still numeric — ANOVA is treating it as a continuous predictor (a regression slope), not several groups. Make it a factor first: mydata$group <- factor(mydata$group) (in PlantGrowth, group is already a factor).

The ANOVA is significant but every post-hoc pair is non-significant (or vice-versa). The omnibus F-test and the multiplicity-corrected pairwise tests answer different questions; borderline cases can disagree. In PlantGrowth the omnibus is significant yet only trt1-vs-trt2 survives the Tukey correction — that’s normal. Trust the post-hoc comparisons for which groups differ, and always report the effect size, not just the p-value.

Levene’s test is significant, so my Tukey HSD may be wrong. Equal variance has failed. Re-run the comparisons with games_howell_test() (and report a welch_anova_test() omnibus), which don’t assume equal variances.

Frequently asked questions

The tidy way is rstatix: make your grouping column a factor, then mydata %>% anova_test(outcome ~ group) — it returns the F-statistic, p-value and a ges effect size in one row. The base-R way is summary(aov(outcome ~ group, data = mydata)), which prints the classic ANOVA table. Both compute the same F-test.

A t-test compares the means of exactly two groups; a one-way ANOVA compares three or more. Running many t-tests instead inflates the false-positive rate, which ANOVA avoids with a single omnibus F-test. With exactly two groups, the ANOVA and the independent t-test give identical results (F = t²).

If Levene’s test shows equal variances, use Tukey HSD (tukey_hsd()) — it compares all pairs and corrects for multiplicity. If variances are unequal, use Games-Howell (games_howell_test()), which is built on Welch’s correction and doesn’t assume equal variances or equal group sizes. Run a post-hoc test only after a significant omnibus ANOVA.

Four, really: the observations are independent and random, there are no extreme outliers (identify_outliers()), the residuals are normally distributed (check with shapiro_test() on the model residuals and a Q-Q plot), and the groups have equal variance (check with levene_test()). If equal-variance fails, use the Welch ANOVA; if normality fails, use the non-parametric Kruskal-Wallis test.

The F-statistic is the ratio of the between-group variance to the within-group variance — how far the group means spread apart relative to the noise inside each group. A large F means the group means differ by more than chance noise would explain, which is evidence that at least one mean is genuinely different.

For unequal variances, use welch_anova_test() plus the Games-Howell post-hoc test — neither assumes equal variances. For non-normal data, use the Kruskal-Wallis rank-sum test (kruskal_test()), the non-parametric alternative to one-way ANOVA, followed by Dunn’s test for pairwise comparisons.

Test your understanding

  1. Run it. In the live cell below, run a one-way ANOVA of plant weight by group in PlantGrowth. Fill the blanks. Is the omnibus test significant? What is the ges effect size?
  2. Conceptual. Your omnibus ANOVA is significant, but you have three groups. Why can’t you stop there, and what should you run next?

Fill the two blanks with the outcome weight and the factor group. Look at the p column for significance and the ges column for the effect size. For question 2, remember that the omnibus F-test answers a different question from the pairwise comparisons.

library(rstatix)
data("PlantGrowth")
PlantGrowth %>% anova_test(weight ~ group)
#> F(2, 27) = 4.85, p = 0.016, ges = 0.26  → significant; treatment explains ~26% of the variation.

The omnibus ANOVA is significant (p = 0.016) and ges = 0.26 — group explains about 26% of the variation in plant weight.

For question 2: a significant omnibus ANOVA only tells you that at least one of the three group means differs — not which ones. You must run a post-hoc pairwise test (tukey_hsd(), or games_howell_test() if variances are unequal) that corrects for the multiple comparisons to identify the specific pairs that differ (here, only trt1 vs trt2).

Conclusion

You can now run and interpret a one-way ANOVA in R two ways: the tidy rstatix anova_test() (with its ges effect size) and the classic base-R aov() + summary(). Check the assumptions — no outliers, normality of residuals (shapiro_test) and equal variance (levene_test) — read the omnibus F-test, then follow a significant result with a post-hoc comparison (tukey_hsd(), or games_howell_test() when variances differ). When the assumptions fail, the Kruskal-Wallis test is the non-parametric fallback.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {One-Way {ANOVA} in {R:} {Compare} {Several} {Group} {Means}},
  date = {2026-06-23},
  url = {https://www.datanovia.com/learn/biostatistics/anova/anova-in-r},
  langid = {en}
}
For attribution, please cite this work as:
“One-Way ANOVA in R: Compare Several Group Means.” 2026. June 23. https://www.datanovia.com/learn/biostatistics/anova/anova-in-r.