Kruskal-Wallis H Test
The engine for Robust Multi-Group Discovery. This model audits the stochastic differences between three or more independent groups, providing a powerful omnibus shield when ANOVA assumptions fail.
What is it?
Kruskal-Wallis H Test is a nonparametric method comparing 3+ independent groups based on overall ranks. Alternative to One-Way ANOVA.
When to use it
- Three or More Groups: Independent categorical grouping.
- Non-Normal scale: Skewed scores or ordinal metrics violating ANOVA constraints.
Core Idea
Combines and ranks all data jointly. Evaluates the variance of rank sums across groups. Large variance indicates true group differences:
Hypotheses
How it works
- Pool all observations together and sort to assign ranks.
- Calculate Rank Sums for each group (R1, R2, R3).
- Compute Kruskal-Wallis H statistic based on rank sums.
- Find p-value using Chi-Square distribution with df = k - 1.
Assumptions
Effect Size
Epsilon-squared (**e2 = H / ((N^2-1)/(N+1))**) represents the proportion of rank variance attributable to group membership.
Quick Example
| Group | n | Rank Sum |
|---|---|---|
| G1 | 10 | 92.0 |
| G2 | 10 | 165.0 |
| G3 | 10 | 208.0 |
Kruskal-Wallis Live Laboratory
Adjust individual group averages to observe stochastic separation and Chi-square probability shift.
| Group | Rank Sum | Rank Mean |
|---|---|---|
| Group 1 | 110 | 11.0 |
| Group 2 | 135 | 13.5 |
| Group 3 | 220 | 22.0 |
| H-statistic | 8.5806 | |
| p-value (ChiSq df=2) | 0.0080 | |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: All k distributions are identical
Hₐ: At least one distribution differs (stochastic dominance)
Nonparametric alternative to one-way ANOVA. Tests distributions (not means). Only interpretable as median test if group distributions have similar shapes.
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.
- Visual comparison of distributions (boxplots by group)
- Check for similar distribution shapes (density plots, violin plots)
- Levene's test for homogeneity of variance (if p > .05, shapes likely similar)
- Effect size calculation (epsilon squared: ε² = (H - k + 1)/(n - k), interpretation: .01 small, .06 medium, .14 large)
- Check for monotonic trend if groups have natural ordering (Jonckheere-Terpstra test)
- Compare skewness coefficients across groups (difference < 0.5 indicates similar shapes)
- Compare IQRs across groups (similar IQRs suggest similar spreads)
- Check proportion of tied ranks (if > 10%, verify software uses tie correction)
- Outlier detection (IQR method or z-scores > 3)
- Shapiro-Wilk test per group to confirm non-normality (justifies using K-W over ANOVA)
- Overlay density plots to visually assess shape similarity
- Sensitivity analysis: Compare results with/without top 5% extreme values
- Check distribution shapes are similar (overlay density plots, compare skewness across groups)
- Variance ratio test: Largest group variance / smallest group variance (should be < 3:1 for robust interpretation)
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Yoga Style Effects on Flexibility (3 Independent Groups)
Research question: Do different yoga styles (Hatha, Vinyasa, Yin) differentially improve flexibility? Design: Between-subjects (n=90, 30 per group). Outcome: Sit-and-reach test (cm, continuous but right-skewed). K-W used because flexibility data violated normality (skewed distribution with floor effects).
# ==============================================================================
# Kruskal-Wallis Test: Yoga Style Effects on Flexibility
# Research Question: Do different yoga styles improve flexibility differently?
# ==============================================================================
library(tidyverse) # Data manipulation and visualization
library(rstatix) # Statistical tests and effect sizes
library(FSA) # Dunn's test
library(car) # Levene's test
library(effectsize) # Effect size calculations
library(ggpubr) # Publication-ready plots
library(moments) # Skewness calculation
# Generate simulated data (n = 90, 30 per group)
set.seed(2025)
data <- data.frame(
yoga_style = rep(c("Hatha", "Vinyasa", "Yin"), each=30),
flexibility_gain = c(
rgamma(30, shape=3, scale=2.5), # Hatha: moderate gains, skewed
rgamma(30, shape=3.5, scale=2.8), # Vinyasa: moderate-high
rgamma(30, shape=4, scale=3.2) # Yin: highest gains
)
)
# Convert to factor for proper ordering
data$yoga_style <- factor(data$yoga_style, levels = c("Hatha", "Vinyasa", "Yin"))
# ==============================================================================
# STEP 1: DESCRIPTIVE STATISTICS
# ==============================================================================
# Summary statistics by group
descriptives <- data %>%
group_by(yoga_style) %>%
summarise(
n = n(),
median = median(flexibility_gain),
IQR = IQR(flexibility_gain),
mean = mean(flexibility_gain),
sd = sd(flexibility_gain),
min = min(flexibility_gain),
max = max(flexibility_gain)
)
print("Descriptive Statistics by Group:")
print(descriptives)
# ==============================================================================
# STEP 2: ASSUMPTION CHECKING
# ==============================================================================
cat("\n=== ASSUMPTION CHECKS ===\n")
# Assumption 1: Check normality per group (Shapiro-Wilk)
cat("\n1. Normality tests(Shapiro-Wilk per group):\n")
normality_tests <- data %>%
group_by(yoga_style) %>%
summarise(shapiro_p = shapiro.test(flexibility_gain)$p.value)
print(normality_tests)
cat("Interpretation: If p < .05, data are non-normal → KW appropriate\n")
# Assumption 2: Check homogeneity of variance (Levene's test)
cat("\n2. Levene's Test for Homogeneity of Variance:\n")
levene_result <- leveneTest(flexibility_gain ~ yoga_style, data=data)
print(levene_result)
cat("Interpretation: If p > .05, variances are similar → can interpret as median test\n")
# Assumption 3: Check distribution shapes (compare skewness)
cat("\n3. Skewness by group(should be similar for median interpretation):\n")
skewness_by_group <- data %>%
group_by(yoga_style) %>%
summarise(skewness = skewness(flexibility_gain))
print(skewness_by_group)
cat("Interpretation: If skewness differs <0.5, shapes similar → median test valid\n")
# Assumption 4: Check for extreme outliers
cat("\n4. Outlier Detection(values > 3 IQR beyond Q1/Q3):\n")
outliers <- data %>%
group_by(yoga_style) %>%
identify_outliers(flexibility_gain)
if(nrow(outliers) > 0) {
print(outliers)
} else {
cat("No extreme outliers detected.\n")
}
# ==============================================================================
# STEP 3: VISUALIZATIONS
# ==============================================================================
cat("\n=== GENERATING VISUALIZATIONS ===\n")
# Visualization 1: Boxplot with individual points
p1 <- ggplot(data, aes(x=yoga_style, y=flexibility_gain, fill=yoga_style)) +
geom_boxplot(alpha=0.7, outlier.shape=NA) +
geom_jitter(width=0.2, alpha=0.3, size=2) +
stat_summary(fun=median, geom="point", shape=23, size=4, fill="red") +
labs(title="Flexibility Gains by Yoga Style",
subtitle="Red diamond = median",
x="Yoga Style", y="Flexibility Gain(cm)") +
theme_minimal() +
theme(legend.position="none")
print(p1)
# Visualization 2: Violin plots showing distribution shapes
p2 <- ggplot(data, aes(x=yoga_style, y=flexibility_gain, fill=yoga_style)) +
geom_violin(alpha=0.6, trim=FALSE) +
geom_boxplot(width=0.1, fill="white", alpha=0.8) +
labs(title="Distribution Shapes: Flexibility by Yoga Style",
x="Yoga Style", y="Flexibility Gain(cm)") +
theme_minimal() +
theme(legend.position="none")
print(p2)
# Visualization 3: Density plots overlayed (check shape similarity)
p3 <- ggplot(data, aes(x=flexibility_gain, fill=yoga_style)) +
geom_density(alpha=0.5) +
labs(title="Density Distributions by Yoga Style",
subtitle="Check for similar shapes(CRITICAL for median interpretation)",
x="Flexibility Gain(cm)", y="Density") +
theme_minimal()
print(p3)
# ==============================================================================
# STEP 4: KRUSKAL-WALLIS TEST
# ==============================================================================
cat("\n=== KRUSKAL-WALLIS H TEST ===\n")
# Main test
kw_result <- kruskal.test(flexibility_gain ~ yoga_style, data=data)
print(kw_result)
# Extract test statistics
H_stat <- kw_result$statistic
df <- kw_result$parameter
p_value <- kw_result$p.value
cat(sprintf("\nH(%d) = %.2f, p = %.4f\n", df, H_stat, p_value))
# ==============================================================================
# STEP 5: EFFECT SIZE
# ==============================================================================
cat("\n=== EFFECT SIZE ===\n")
# Epsilon squared (preferred, less biased than eta squared)
epsilon_sq <- kruskal_effsize(data, flexibility_gain ~ yoga_style)
print(epsilon_sq)
cat("\nInterpretation: ε² = .01 (small), .06 (medium), .14 (large)\n")
# ==============================================================================
# STEP 6: POST-HOC TESTS (Only if p < .05)
# ==============================================================================
if(p_value < 0.05) {
cat("\n=== POST-HOC TESTS: Dunn's Test with Bonferroni Correction ===\n")
# Dunn's test (preferred for Kruskal-Wallis follow-up)
dunn_results <- dunn_test(data, flexibility_gain ~ yoga_style,
p.adjust.method="bonferroni")
print(dunn_results)
# Alternative: FSA package Dunn test
cat("\nAlternative: FSA::dunnTest\n")
dunn_fsa <- dunnTest(flexibility_gain ~ yoga_style, data=data, method="bonferroni")
print(dunn_fsa)
# Pairwise medians for interpretation
cat("\nPairwise Median Comparisons:\n")
pairwise_medians <- data %>%
group_by(yoga_style) %>%
summarise(median = median(flexibility_gain)) %>%
arrange(desc(median))
print(pairwise_medians)
} else {
cat("\nKruskal-Wallis not significant(p > .05). Post-hoc tests not needed.\n")
}
# ==============================================================================
# STEP 7: FINAL INTERPRETATION
# ==============================================================================
cat("\n=== INTERPRETATION ===\n")
cat(sprintf(
"A Kruskal-Wallis H test showed a significant difference in flexibility gains\n"))
cat(sprintf(
"across yoga styles, H(%d) = %.2f, p = %.4f, ε² = %.2f (large effect).\n",
df, H_stat, p_value, epsilon_sq$effsize))
cat("\nPost-hoc Dunn tests revealed Yin yoga produced significantly greater\n")
cat("flexibility gains than both Hatha and Vinyasa, with no difference between\n")
cat("Hatha and Vinyasa. Distribution shapes were similar across groups(Levene's\n")
cat("p > .05, similar skewness), validating interpretation as a median test.\n")
cat("\nConclusion: Yin yoga is most effective for flexibility improvement.\n")
# ==============================================================================
# APA REPORTING TEMPLATE
# ==============================================================================
cat("\n=== APA-STYLE REPORTING ===\n")
cat("A Kruskal-Wallis H test was conducted to compare flexibility gains across\n")
cat("three yoga styles(Hatha, Vinyasa, Yin). Distributions had similar shapes\n")
cat("across groups(Levene's test p = .XX, similar skewness), validating\n")
cat("interpretation as a test of medians. There was a significant difference\n")
cat(sprintf("in flexibility gains, H(%d) = %.2f, p < .001, ε² = %.2f (large effect).\n",
df, H_stat, epsilon_sq$effsize))
cat("Post-hoc Dunn tests with Bonferroni correction showed Yin yoga(Mdn = XX.X cm)\n")
cat("produced greater gains than Hatha(Mdn = XX.X cm, p = .002) and Vinyasa\n")
cat("(Mdn = XX.X cm, p = .045), with no difference between Hatha and Vinyasa\n")
cat("(p = .32). Yin yoga is recommended for flexibility improvement.\n")H(2) = 18.45, p < .001, ε² = .21 (large). Post-hoc Dunn tests: Yin > Hatha (p = .002), Yin > Vinyasa (p = .045), no difference Hatha vs Vinyasa (p = .32). Yin yoga produced greatest flexibility gains. Non-normal data justified nonparametric test.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- One-Way ANOVA — Return to the mean-based strike to maximize statistical power.
- Jonckheere-Terpstra Test — Exploit the group-order to increase trend-detection precision.
- Cochran-Armitage Strike — Audit the linear slope of proportions across the ranked categories.
- Brunner-Munzel Test — The robust alternative when group variances and distribution shapes differ wildly.
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.
A significant Kruskal-Wallis result is only an invitation to a deeper audit. Use Dunn's test to find the definitive winner while protecting your global scientific integrity.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
ε² = (H - k + 1)/(n - k), where H = Kruskal-Wallis statistic, k = number of groups, n = total sample size. Interpretation: .01 small, .06 medium, .14 large (Cohen, 1988). Represents proportion of total variance in ranks explained by groups.
η² = (H - k + 1)/(n - 1). Alternative to ε²; slightly different denominator. Interpretation same as ε².
For pairwise post-hoc comparisons: r_rb = 1 - (2U)/(n₁n₂), where U = Mann-Whitney U statistic. Ranges -1 to +1. Interpretation: |r| = .1 small, .3 medium, .5 large.
For pairwise comparisons: δ = (n_greater - n_less)/(n₁n₂). Proportion of pairs where group 1 > group 2 minus proportion group 2 > group 1. Ranges -1 to +1. Interpretation: |δ| < .147 negligible, .147-.33 small, .33-.474 medium, >.474 large (Romano et al., 2006).
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
Omnibus Rank Mandate: A minimum of 20 participants per group is required to stabilize the H-statistic and ensure the mean-rank distribution reaches statistical authority.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | f=.10 (Small) | n ≈ 1060 total |
| Medium Effect | f=.25 (Medium) | n ≈ 175 total |
| Large Effect | f=.40 (Large) | n ≈ 72 total |
The 'Shape Assumption': Kruskal-Wallis is most powerful when group distributions share the same shape (e.g., all skewed right). If shapes vary wildly, the test shifts from 'Median Comparison' to 'Stochastic Dominance', which may require larger samples to interpret.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A Kruskal-Wallis H test was conducted to compare outcome across k groups: list groups. State assumption checks: 'Distributions had similar shapes across groups (visual inspection)' OR 'Distributions differed in shape, so test interpreted as stochastic dominance'. There was a significant/non-significant difference in outcome across groups, H(df) = X.XX, p = .XXX, ε² = .XX interpret: small/medium/large effect. If significant: Post-hoc Dunn tests with Bonferroni correction showed describe pairwise comparisons with medians/IQRs and p-values. Conclude with interpretation.
- H statistic
- degrees of freedom (k-1)
- p-value
- effect size (epsilon squared or eta squared)
- medians and IQRs per group
- post-hoc test results if significant
- statement about distribution shapes (for interpretation)
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Group | Median | Mean Rank | H (χ²) | df | p | ε² (Eta-Squared) |
|---|---|---|---|---|---|---|
| Method A | 85 | 72.4 | 15.42 | 2 | .001 | .13 |
| Method B | 72 | 60.1 | — | — | — | — |
| Control | 65 | 48.5 | — | — | — | — |
The Rank Variance. Measures how much the average ranks of the groups deviate from what we would expect if they were all the same.
Rank Variance Explained. .13 indicates that 13% of the variability in ranks is directly attributable to the Grouping factor.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Kruskal-Wallis Test
kruskal.test(score ~ group, data = df)
# 2. Extract Effect Size (Eta-Squared)
rstatix::kruskal_effsize(score ~ group, data = df)
# 3. Post-hoc Rank Comparisons (Dunn's Test)
dunn.test::dunn.test(df$score, df$group, method = 'bh')Kruskal-Wallis is 'Omnibus'. It tells you THERE IS a difference, but not WHERE. You MUST follow up with Dunn's test (not Mann-Whitney) to find the specific group pairs.
# Execute Nemenyi Post-hoc (Conservative rank-sum comparison)
PMCMRplus::kwAllPairsNemenyiTest(score ~ group, data = df)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.