Tukey HSD
The engine for Pairwise Discovery. Tukey's Honestly Significant Difference (HSD) audits all possible pairs of group means, providing a powerful alpha-shield while hunting for the definitive 'Group Winner'.
What is it?
Tukey HSD is designed to adjust significance thresholds or confidence intervals during multiple pairwise comparisons to protect against Family-Wise Error Rate inflation.
The engine for Pairwise Discovery. Tukey's Honestly Significant Difference (HSD) audits all possible pairs of group means, providing a powerful alpha-shield while hunting for the definitive 'Group Winner'.
Goals & Indications
- Pairwise Isolation Audit: Identify exactly which group combinations drive the global ANOVA significance.
- Alpha-Shielding Strategy: Maintain a strict 5% family-wise error rate across all simultaneous comparisons.
- Conservative Discovery Strike: Ensure that reported differences are 'Honestly Significant' and not chance sampling artifacts.
Core Idea Diagram
Claims tested
How it works
- Perform ANOVA and obtain Mean Square Error (MSE) and degrees of freedom.
- Determine Studentized Range distribution critical value q_crit.
- Calculate Tukey honest difference threshold: W = q_crit * sqrt(MSE / n).
- Compute all pairwise differences; compare against W to assess significance.
Assumptions
Important Note
Conducted AFTER significant omnibus ANOVA F-test. Controls family-wise error rate (FWER) at α across ALL pairwise comparisons using studentized range (q) distribution. Tests all k(k-1)/2 pairwise comparisons simultaneously while maintaining overall Type I error at α.
Worked Example
| Comparison | Difference | Tukey 95% CI | Significance |
|---|---|---|---|
| A vs B | 3.50 | [1.20, 5.80] | Significant |
| B vs C | 0.80 | [-1.50, 3.10] | No Difference |
Pairwise Post-Hoc Comparison Laboratory
Slide the group averages and error variance. Observe which pairwise confidence intervals cross 0 (representing no statistical difference) vs. those that stand clear.
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: μᵢ = μⱼ for all pairwise comparisons (no difference between any pair of group means)
Hₐ: μᵢ ≠ μⱼ for at least one pair (at least one pairwise difference exists)
Conducted AFTER significant omnibus ANOVA F-test. Controls family-wise error rate (FWER) at α across ALL pairwise comparisons using studentized range (q) distribution. Tests all k(k-1)/2 pairwise comparisons simultaneously while maintaining overall Type I error at α.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Omnibus ANOVA F-test (must be significant, p < .05)
- Levene's test or Brown-Forsythe test for homogeneity of variance (p > .05 required)
- Pairwise comparisons table with adjusted p-values
- 95% confidence intervals for mean differences
- Compact letter display (showing homogeneous groups)
- Q-Q plots of residuals by group (normality check)
- Effect sizes (Cohen's d) for each pairwise comparison
- Forest plot showing all pairwise differences with CIs
- Boxplots by group to visualize differences and outliers
- Power analysis for pairwise comparisons (post-hoc power)
- Residual diagnostics from ANOVA model
- Bootstrap confidence intervals (1000-2000 iterations)
- Descriptive statistics table (M, SD, n per group)
- Interaction plot showing group means with error bars
- Homogeneity of variance plot (spread vs level)
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Diet Type and Weight Loss (4-group Design with All Pairwise Comparisons)
Research question: Which diet produces the most weight loss? Design: 12-week RCT with 4 groups (Low-carb n=28, Mediterranean n=28, Low-fat n=28, Control n=28). Outcome: Weight loss in kg from baseline (continuous). Conduct all k(k-1)/2 = 6 pairwise comparisons with FWER control.
# Tukey HSD: All pairwise diet comparisons following ANOVA
# Example 1: 4 diet groups, 6 pairwise comparisons
# Load packages
library(car) # Levene's test
library(effectsize) # Effect sizes
library(tidyverse) # Data manipulation
library(multcomp) # Advanced contrasts
library(emmeans) # Estimated marginal means
# Simulate realistic data (or load: data <- read.csv("diet_study.csv"))
set.seed(2025)
data <- data.frame(
diet = rep(c("Low-carb", "Mediterranean", "Low-fat", "Control"), each=28),
weight_loss = c(
rnorm(28, mean=7.2, sd=3.1), # Low-carb: M=7.2 kg
rnorm(28, mean=6.5, sd=2.8), # Mediterranean: M=6.5 kg
rnorm(28, mean=4.3, sd=2.9), # Low-fat: M=4.3 kg
rnorm(28, mean=2.1, sd=2.5) # Control: M=2.1 kg
)
)
cat("=== TUKEY HSD POST-HOC ANALYSIS ===")
cat("\nAfter 12-week diet intervention: Which diets differ?\n\n")
# === STEP 1: Check Assumptions ===
cat("--- STEP 1: Assumption Checks ---\n")
# 1. Normality per group
cat("\n1. Normality(Shapiro-Wilk per group):\n")
by(data$weight_loss, data$diet, shapiro.test)
# All p > .05 → normality OK
# Q-Q plots
par(mfrow=c(2,2))
for (diet_type in c("Low-carb", "Mediterranean", "Low-fat", "Control")) {
qqnorm(data$weight_loss[data$diet == diet_type], main=diet_type)
qqline(data$weight_loss[data$diet == diet_type], col="red")
}
par(mfrow=c(1,1))
# 2. Homogeneity of variance (Levene's test - CRITICAL for Tukey HSD)
cat("\n2. Homogeneity of Variance(Levene's test):\n")
levene_result <- leveneTest(weight_loss ~ diet, data=data)
print(levene_result)
if (levene_result$`Pr(>F)`[1] < 0.05) {
cat("\n*** WARNING: Unequal variances detected! Use Games-Howell instead. ***\n")
} else {
cat("\n✓ Equal variances assumption met(p > .05). Tukey HSD is appropriate.\n")
}
# 3. Outliers
cat("\n3. Outliers Check:\n")
ggplot(data, aes(x=diet, y=weight_loss, fill=diet)) +
geom_boxplot(alpha=0.7) +
labs(title="Weight Loss by Diet Type\n(Check for outliers)",
x="Diet Group", y="Weight Loss(kg)") +
theme_classic() +
theme(legend.position="none")
# === STEP 2: Descriptive Statistics ===
cat("\n--- STEP 2: Descriptive Statistics ---\n")
desc_stats <- data %>%
group_by(diet) %>%
summarise(n = n(),
M = mean(weight_loss),
SD = sd(weight_loss),
SE = SD/sqrt(n),
CI_lower = M - 1.96*SE,
CI_upper = M + 1.96*SE)
print(desc_stats)
# === STEP 3: Run Omnibus ANOVA (prerequisite) ===
cat("\n--- STEP 3: Omnibus One-Way ANOVA ---\n")
anova_model <- aov(weight_loss ~ diet, data=data)
anova_summary <- summary(anova_model)
print(anova_summary)
# Extract F-statistic and p-value
f_stat <- anova_summary[[1]]$`F value`[1]
p_value <- anova_summary[[1]]$`Pr(>F)`[1]
if (p_value >= 0.05) {
cat("\n*** STOP: ANOVA non-significant(p ≥ .05). Do NOT proceed to Tukey HSD. ***\n")
cat("Conducting post-hoc tests after non-significant ANOVA inflates Type I error.\n")
stop("ANOVA must be significant before post-hoc testing.")
} else {
cat("\n✓ ANOVA is significant(p < .05). Proceed to Tukey HSD post-hoc tests.\n")
}
# Effect size
omega_sq <- omega_squared(anova_model)
cat("\nEffect size: ω² =", round(omega_sq$Omega2, 3))
if (omega_sq$Omega2 < 0.06) {
cat(" (small)")
} else if (omega_sq$Omega2 < 0.14) {
cat(" (medium)")
} else {
cat(" (large)")
}
cat("\n")
# === STEP 4: Tukey HSD Post-hoc Test ===
cat("\n--- STEP 4: Tukey HSD Pairwise Comparisons ---\n")
cat("Number of pairwise comparisons: k(k-1)/2 = 4×3/2 = 6\n")
cat("Family-wise error rate(FWER) controlled at α = .05\n\n")
tukey_result <- TukeyHSD(anova_model, conf.level=0.95)
print(tukey_result)
# Extract pairwise comparisons
tukey_df <- as.data.frame(tukey_result$diet)
tukey_df$comparison <- rownames(tukey_df)
tukey_df <- tukey_df %>%
mutate(sig = ifelse(`p adj` < 0.001, "***",
ifelse(`p adj` < 0.01, "**",
ifelse(`p adj` < 0.05, "*", "ns"))),
cohen_d = diff / sqrt(sum(anova_model$residuals^2) / anova_model$df.residual))
cat("\nPairwise Comparisons Summary:\n")
print(tukey_df[, c("comparison", "diff", "lwr", "upr", "p adj", "sig")])
# === STEP 5: Effect Sizes for Pairwise Comparisons ===
cat("\n--- STEP 5: Cohen's d for Each Pairwise Comparison ---\n")
# Calculate pooled SD from ANOVA
MSE <- sum(anova_model$residuals^2) / anova_model$df.residual
pooled_sd <- sqrt(MSE)
# Cohen's d for each comparison
tukey_df <- tukey_df %>%
mutate(cohen_d = diff / pooled_sd,
d_interpretation = case_when(
abs(cohen_d) < 0.2 ~ "negligible",
abs(cohen_d) < 0.5 ~ "small",
abs(cohen_d) < 0.8 ~ "medium",
TRUE ~ "large"
))
cat("\nEffect Sizes:\n")
print(tukey_df[, c("comparison", "diff", "cohen_d", "d_interpretation", "p adj")])
# === STEP 6: Compact Letter Display ===
cat("\n--- STEP 6: Compact Letter Display(Homogeneous Groups) ---\n")
cat("Groups sharing a letter are NOT significantly different.\n\n")
library(multcomp)
cld_result <- cld(glht(anova_model, linfct=mcp(diet="Tukey")))
print(cld_result)
# Add letters to descriptive stats
cld_letters <- data.frame(
diet = names(cld_result$mcletters$Letters),
letter = cld_result$mcletters$Letters
)
desc_stats_cld <- left_join(desc_stats, cld_letters, by="diet")
print(desc_stats_cld)
# === STEP 7: Bootstrap Confidence Intervals ===
cat("\n--- STEP 7: Bootstrap Confidence Intervals(1500 iterations) ---\n")
set.seed(2025)
n_boot <- 1500
boot_diffs <- matrix(NA, nrow=n_boot, ncol=6)
comparisons <- combn(unique(data$diet), 2, simplify=FALSE)
for (i in 1:n_boot) {
# Resample within each group
boot_sample <- data %>%
group_by(diet) %>%
slice_sample(n=n(), replace=TRUE) %>%
ungroup()
# Calculate pairwise differences
means_boot <- boot_sample %>%
group_by(diet) %>%
summarise(M = mean(weight_loss), .groups="drop")
for (j in 1:6) {
g1 <- comparisons[[j]][1]
g2 <- comparisons[[j]][2]
m1 <- means_boot$M[means_boot$diet == g1]
m2 <- means_boot$M[means_boot$diet == g2]
boot_diffs[i, j] <- m1 - m2
}
}
# Calculate bootstrap CIs
boot_cis <- apply(boot_diffs, 2, quantile, probs=c(0.025, 0.975))
colnames(boot_cis) <- sapply(comparisons, function(x) paste(x, collapse=" - "))
cat("\nBootstrap 95% CIs(compare to Tukey CIs):\n")
print(round(boot_cis, 2))
# === STEP 8: Visualizations ===
cat("\n--- STEP 8: Visualizations ---\n")
# 8a. Bar plot with compact letters
ggplot(desc_stats_cld, aes(x=reorder(diet, -M), y=M, fill=diet)) +
geom_bar(stat="identity", width=0.6, alpha=0.8) +
geom_errorbar(aes(ymin=CI_lower, ymax=CI_upper), width=0.2) +
geom_text(aes(label=letter), vjust=-0.5, size=6, fontface="bold") +
labs(title="Weight Loss by Diet Type(12-week intervention)",
subtitle="Bars with different letters differ significantly(Tukey HSD, α=.05)",
x="Diet Group", y="Mean Weight Loss(kg) ± 95% CI") +
scale_fill_brewer(palette="Set2") +
theme_classic() +
theme(legend.position="none",
plot.title = element_text(face="bold", size=14))
# 8b. Forest plot of pairwise differences
tukey_df$comparison_clean <- gsub("-", " vs ", tukey_df$comparison)
ggplot(tukey_df, aes(x=diff, y=reorder(comparison_clean, diff))) +
geom_vline(xintercept=0, linetype="dashed", color="gray50") +
geom_errorbarh(aes(xmin=lwr, xmax=upr), height=0.2, size=1) +
geom_point(aes(color=sig), size=4) +
scale_color_manual(values=c("***"="red", "**"="orange", "*"="yellow", "ns"="gray"),
name="Significance") +
labs(title="Forest Plot: All Pairwise Comparisons(Tukey HSD)",
subtitle="Error bars = 95% confidence intervals",
x="Mean Difference in Weight Loss(kg)",
y="Pairwise Comparison") +
theme_minimal() +
theme(plot.title = element_text(face="bold"))
# 8c. Interaction plot
ggplot(desc_stats, aes(x=diet, y=M, group=1)) +
geom_line(size=1.2, color="steelblue") +
geom_point(size=4, color="darkblue") +
geom_errorbar(aes(ymin=CI_lower, ymax=CI_upper), width=0.2) +
labs(title="Group Means with 95% Confidence Intervals",
x="Diet Type", y="Mean Weight Loss(kg)") +
theme_classic()
# === STEP 9: APA-Style Reporting ===
cat("\n--- STEP 9: APA-Style Results ---\n\n")
cat(paste0(
"A one-way ANOVA was conducted to compare weight loss across four diet interventions.\n",
"Data met assumptions of normality(Shapiro-Wilk p > .05 for all groups) and homogeneity\n",
"of variance(Levene's test, F(", anova_summary[[1]]$Df[1], ", ", anova_summary[[1]]$Df[2], ") = ",
round(levene_result$`F value`[1], 2), ", p = ", round(levene_result$`Pr(>F)`[1], 3), ").\n\n",
"There was a significant effect of diet type on weight loss, F(", anova_summary[[1]]$Df[1], ", ",
anova_summary[[1]]$Df[2], ") = ", round(f_stat, 2), ", p < .001, ω² = ",
round(omega_sq$Omega2, 2), " (large effect).\n\n",
"Post-hoc comparisons using Tukey HSD indicated:\n",
"• Low-carb(M = ", round(desc_stats$M[desc_stats$diet=="Low-carb"], 1), ", SD = ",
round(desc_stats$SD[desc_stats$diet=="Low-carb"], 1), ") produced significantly greater weight loss than\n",
" Low-fat(M = ", round(desc_stats$M[desc_stats$diet=="Low-fat"], 1), ", SD = ",
round(desc_stats$SD[desc_stats$diet=="Low-fat"], 1), "), p < .001, d = ",
round(abs(tukey_df$cohen_d[grepl("Low-carb.*Low-fat", tukey_df$comparison)]), 2), ",\n",
" and Control(M = ", round(desc_stats$M[desc_stats$diet=="Control"], 1), ", SD = ",
round(desc_stats$SD[desc_stats$diet=="Control"], 1), "), p < .001, d = ",
round(abs(tukey_df$cohen_d[grepl("Low-carb.*Control", tukey_df$comparison)]), 2), ".\n",
"• Mediterranean diet showed similar patterns.\n",
"• No significant difference between Low-carb and Mediterranean diets(p = ",
round(tukey_df$`p adj`[grepl("Mediterranean.*Low-carb", tukey_df$comparison)], 3), ").\n\n",
"These findings suggest low-carb and Mediterranean diets are superior to low-fat and\n",
"control conditions for weight loss, with large effect sizes(d > 0.8)."
))
cat("\n\n=== ANALYSIS COMPLETE ===")F(3, 108) = 42.3, p < .001, ω² = .53 (large effect). Tukey HSD revealed: (1) Low-carb and Mediterranean diets produced significantly greater weight loss than Low-fat and Control (all p < .001, d > 1.0); (2) No difference between Low-carb and Mediterranean (p = .47); (3) Low-fat superior to Control (p = .003, d = 0.74). Compact letter display: Low-carb/Mediterranean (group 'a'), Low-fat (group 'b'), Control (group 'c'). Family-wise error rate maintained at α = .05 across all 6 comparisons.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Games-Howell Strike — The mandatory alternative when group spreads are unequal.
- Dunnett's T3 — A robust choice for very small samples with unequal variance.
- Tukey-Kramer Adjustment — Automatic extension to handle groups with differing N.
- Scheffé Test — A more conservative strike for complex, non-pairwise contrasts.
- Dunn's Test — The non-parametric equivalent for pairwise rank-sum comparison.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
Post-hoc pairwise tests defined for this model.
No specific guidelines provided.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Standardized mean difference for each pair. Small: 0.2, Medium: 0.5, Large: 0.8 (Cohen, 1988)
Proportion of variance explained by grouping variable. Small: .01, Medium: .06, Large: .14
d = (M_i - M_j) / SD_pooled, where SD_pooled = sqrt(MSE from ANOVA)
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
At least 10-15 observations per group for Tukey HSD to control FWER
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | α=.05, power=.80 | n ≈ 64 per group |
| Medium Effect | α=.05, power=.80 | n ≈ 26 per group |
| Large Effect | α=.05, power=.80 | n ≈ 14 per group |
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
Following a significant omnibus one-way ANOVA, F(df_between, df_within) = X.XX, p < .XXX, ω² = .XX, post-hoc pairwise comparisons were conducted using Tukey HSD to control family-wise error rate at α = .05. If unequal n: The Tukey-Kramer adjustment was applied for unequal sample sizes. Describe key findings: Group A (M = X.XX, SD = X.XX) differed significantly from Group B (M = X.XX, SD = X.XX), p < .XXX, Cohen's d = X.XX interpret: small/medium/large effect. Report all significant comparisons, or use compact letter display: Compact letter display: groups sharing a letter are not significantly different (α = .05).
- Statement that ANOVA was significant (prerequisite)
- All pairwise mean differences with 95% CIs
- Adjusted p-values for each comparison
- Effect sizes (Cohen's d) for significant pairs
- Descriptive statistics per group (M, SD, n)
- Statement about FWER control at α = .05
- Compact letter display (optional but recommended)
- Statement about assumption checks
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Comparison | Diff (M1-M2) | SE | 95% CI (Lower) | 95% CI (Upper) | p-adj |
|---|---|---|---|---|---|
| Method A vs. Control | 12.4 | 2.1 | 8.2 | 16.6 | < .001 |
| Method B vs. Control | 8.5 | 2.1 | 4.3 | 12.7 | .004 |
| Method A vs. Method B | 3.9 | 2.1 | -0.3 | 8.1 | .082 |
The 'Shielded' p-value. Corrected for multiple comparisons to ensure that the total chance of a Type I error across all 3 tests remains exactly 5%.
Tukey Adjusted Intervals. If the range excludes ZERO, the difference is statistically significant.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Tukey HSD
TukeyHSD(aov(score ~ group, data = df))
# 2. Advanced EMMs method (Preferred)
model <- lm(score ~ group, data = df)
emmeans::emmeans(model, pairwise ~ group, adjust = 'tukey')
# 3. Visualize Group Mean Separation
plot(emmeans::emmeans(model, ~ group), comparisons = TRUE)Tukey assumes equal sample sizes and equal variances. If your groups are unbalanced, use the 'Tukey-Kramer' adjustment. If variances are unequal, deploy the 'Games-Howell' test.
# Automated Post-hoc Selection
rstatix::tukey_hsd(df, score ~ group)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.