Skip to content

Multiple Groups (ANOVA & Kruskal-Wallis)

When you have three or more groups, the temptation is to run a t-test on every pair. That inflates the false-positive rate fast. Analysis of variance, or ANOVA, asks one question instead: do any of the group means differ? This page compares expression across a control and two drug doses, in R and Python.

Every code block on this page runs in a container in the companion repository. The numbers and figure come from those real runs.

Use ANOVA when you compare the means of three or more groups on a roughly normal outcome. It controls the overall false-positive rate that many separate t-tests would blow through. If the outcome is not normal, use its rank-based cousin, the Kruskal-Wallis test, shown at the end.

group_expression.csv has 45 samples split evenly across three groups: control, drug_low, and drug_high.

df <- read.csv("group_expression.csv")
df$group <- factor(df$group, levels = c("control", "drug_low", "drug_high"))

A boxplot is the right first look. The high dose sits well above the other two. The brackets show the pairwise Tukey comparisons, explained in the post-hoc section below: ns means not significant, and each * is a tighter significance threshold.

library(ggpubr) # ggboxplot + stat_pvalue_manual (attaches ggplot2)
library(rstatix) # tukey_hsd() + add_xy_position()
tukey <- add_xy_position(tukey_hsd(df, expression ~ group), x = "group")
ggboxplot(df, x = "group", y = "expression", fill = "group",
palette = c("#e07a5f", "#81b29a", "#4c72b0"), add = "jitter") +
stat_pvalue_manual(tukey, label = "p.adj.signif")

Boxplot of expression across three groups with Tukey brackets (R)

ANOVA splits the total variation into the part explained by the groups and the part left over. The F statistic is their ratio. A large F, and a small p-value, means the groups explain more than chance would.

fit <- aov(expression ~ group, data = df)
summary(fit)
Df Sum Sq Mean Sq F value Pr(>F)
group 2 16.39 8.196 18.67 1.58e-06 ***
Residuals 42 18.43 0.439

Both give F = 18.67 and p = 1.6e-06. At least one group differs. But ANOVA does not say which one. For that you need a post-hoc test.

Tukey’s Honest Significant Difference compares every pair while holding the overall error rate at 5%. It reports the difference in means and an adjusted p-value for each pair.

TukeyHSD(fit)
diff lwr upr p adj
drug_low-control 0.108000 -0.4797361 0.6957361 0.8962398
drug_high-control 1.330867 0.7431306 1.9186027 0.0000061
drug_high-drug_low 1.222867 0.6351306 1.8106027 0.0000262

Both agree exactly. The high dose differs from control and from the low dose. The low dose does not differ from control. The single low-dose-versus-control p-value of 0.90 is the honest result. Running that pair as a lone t-test, ignoring the other groups, would have been the wrong analysis.

When the data is not normal: Kruskal-Wallis

Section titled “When the data is not normal: Kruskal-Wallis”

If the outcome fails the normality check, the Kruskal-Wallis test is the rank-based alternative to ANOVA. It compares the distributions across groups without assuming normality.

kruskal.test(expression ~ group, data = df)
Kruskal-Wallis rank sum test
data: expression by group
Kruskal-Wallis chi-squared = 19.926, df = 2, p-value = 4.712e-05

The same boxplot, annotated with the global Kruskal-Wallis p-value instead of the pairwise Tukey brackets.

ggboxplot(df, x = "group", y = "expression", fill = "group",
palette = c("#e07a5f", "#81b29a", "#4c72b0"), add = "jitter") +
stat_compare_means(method = "kruskal.test")

Boxplot with the Kruskal-Wallis p-value (R)

The rank-based test agrees with ANOVA. When both point the same way, the conclusion is solid.

  • Do not run all pairwise t-tests. With five groups there are ten pairs, and the chance of a false positive climbs above 40%. Use ANOVA then a post-hoc test.
  • A significant ANOVA is only step one. It says some group differs. The post-hoc test says which.
  • Check the assumptions. ANOVA assumes roughly normal residuals and similar variance across groups. See the Checking assumptions guide.
  • This is one gene. Running an ANOVA per gene across a whole transcriptome needs multiple-testing correction on top. See the Multiple testing guide.
  • ANOVA tests whether any of three or more group means differ, controlling the false-positive rate.
  • A significant ANOVA needs a post-hoc test, such as Tukey’s HSD, to find which pairs differ.
  • Kruskal-Wallis is the rank-based alternative when the outcome is not normal.
  • STHDA, one-way ANOVA and the Kruskal-Wallis test.
  • The previous guide, Two groups, for the two-group case.
  • The Multiple testing guide, for running many such tests at once.