Wald-Wolfowitz Runs Test (Two-Sample)
Tests if two independent samples come from identical distributions by analyzing the sequence pattern (runs) in combined ranked data; detects differences in location, scale, or shape..
What is it?
Wald-Wolfowitz Runs Test compares the distributions of two independent groups by evaluating the run sequence of sorted combined group labels.
When to use it
- Two Samples: General distribution equivalence test (shapes, spreads, locations).
- Nonparametric fallback: Outlier-resistant alternative to parametric comparisons.
Core Idea
Combines and sorts both groups. If their distributions differ, they cluster separately, yielding very few runs of group labels:
Hypotheses
How it works
- Combine Group 1 and Group 2 data into a single list and sort.
- Substitute each score with its corresponding group label (1 or 2).
- Count consecutive sequences (runs) of identical labels.
- Low runs count indicates significant separation.
Assumptions
Effect Size
Proportion of observed runs to expected runs under H0. Ratios < 0.6 indicate substantial distribution divergence.
Quick Example
| Seq | Observed Runs | Expected Runs |
|---|---|---|
| 1 1 1 2 2 2 | 2 | 4.0 (Significant) |
| 1 2 1 2 1 2 | 6 | 4.0 (Identical) |
Wald-Wolfowitz Two-Sample Runs Laboratory
Shift Group 2's mean to see sorted combined group label clustering.
| Metric | Value |
|---|---|
| Total Combined N | 30 |
| Observed Runs (R) | 8 |
| Expected Runs E(R) | 16.00 |
| p-value (two-tailed) | 0.0032 |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: The two samples come from identical distributions (randomly intermixed)
Hₐ: The two samples come from different distributions (systematic clustering)
Tests whether combined data are randomly mixed or systematically clustered. Few runs suggest clustering (distribution differences); many runs suggest oscillation. Detects location, scale, and shape differences unlike Mann-Whitney which primarily tests location shift.
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, histograms by group)
- Check combined ordered sequence for clustering pattern (run sequence plot)
- Compare medians and spreads (IQR) between groups
- Calculate number of runs and compare to expected (E[R] = 1 + (2n₁n₂)/(n₁+n₂))
- Compute effect size (standardized runs statistic or Cohen's d for comparison)
- Overlay density plots to visualize distribution differences (location, scale, shape)
- Empirical cumulative distribution function (ECDF) plots to detect all distribution differences
- Compare runs test with Mann-Whitney U (M-W tests location primarily; runs tests all aspects)
- Check for outliers that might create artificial runs
- Sensitivity analysis: compare results with/without extreme values
- Calculate overlap coefficient (proportion of distributions overlapping)
- Quantile-quantile (Q-Q) plot comparing group distributions
- Bootstrap confidence intervals for difference in medians or means
- Check skewness and kurtosis differences between groups
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Reaction Time Comparison Between Two Training Methods
Research question: Do two training methods produce different distributions of reaction times? Design: Between-subjects (n=50, 25 per group). Outcome: Reaction time in milliseconds (continuous, right-skewed). Runs test used to detect any distribution differences (not just location), as training might affect variability or shape, not only central tendency.
# ==============================================================================
# Wald-Wolfowitz Runs Test: Reaction Time Comparison
# Research Question: Do two training methods produce different RT distributions?
# ==============================================================================
library(tidyverse) # Data manipulation and visualization
library(randtests) # runs.test() for Wald-Wolfowitz
library(DescTools) # WaldWolfTest() - alternative implementation
library(effectsize) # Effect size calculations
library(ggpubr) # Publication-ready plots
# Generate simulated data (n = 50, 25 per group)
set.seed(2025)
data <- data.frame(
method = rep(c("Method_A", "Method_B"), each=25),
reaction_time = c(
rgamma(25, shape=4, scale=50), # Method A: faster, less variable
rgamma(25, shape=3, scale=70) # Method B: slower, more variable
)
)
data$method <- factor(data$method, levels = c("Method_A", "Method_B"))
# ==============================================================================
# STEP 1: DESCRIPTIVE STATISTICS
# ==============================================================================
cat("\n=== DESCRIPTIVE STATISTICS ===\n")
descriptives <- data %>%
group_by(method) %>%
summarise(
n = n(),
mean = mean(reaction_time),
sd = sd(reaction_time),
median = median(reaction_time),
IQR = IQR(reaction_time),
min = min(reaction_time),
max = max(reaction_time)
)
print(descriptives)
# ==============================================================================
# STEP 2: ASSUMPTION CHECKING
# ==============================================================================
cat("\n=== ASSUMPTION CHECKS ===\n")
# Assumption 1: Independence
cat("1. Independence: Design review confirms between-subjects(independent groups)\n")
# Assumption 2: Sample size (n₁, n₂ ≥ 10 for normal approximation)
cat("\n2. Sample Size Check:\n")
cat(sprintf(" Method A: n = %d(≥10 ✓)\n", sum(data$method == "Method_A")))
cat(sprintf(" Method B: n = %d(≥10 ✓)\n", sum(data$method == "Method_B")))
cat(" Normal approximation is valid.\n")
# Assumption 3: Check for ties
cat("\n3. Tied Values Check:\n")
n_ties <- sum(duplicated(data$reaction_time))
pct_ties <- (n_ties / nrow(data)) * 100
cat(sprintf(" Ties: %d(%.1f%% of data)\n", n_ties, pct_ties))
if(pct_ties < 10) {
cat(" Ties are minimal(<10%), test is appropriate.\n")
} else {
cat(" Warning: >10% ties may reduce power. Consider Mann-Whitney U.\n")
}
# ==============================================================================
# STEP 3: VISUALIZATIONS
# ==============================================================================
cat("\n=== GENERATING VISUALIZATIONS ===\n")
# Visualization 1: Boxplots
p1 <- ggplot(data, aes(x=method, y=reaction_time, fill=method)) +
geom_boxplot(alpha=0.7) +
geom_jitter(width=0.2, alpha=0.4, size=2) +
labs(title="Reaction Time by Training Method",
x="Training Method", y="Reaction Time(ms)") +
theme_minimal() +
theme(legend.position="none")
print(p1)
# Visualization 2: Density plots (check location AND scale differences)
p2 <- ggplot(data, aes(x=reaction_time, fill=method)) +
geom_density(alpha=0.5) +
labs(title="Distribution Comparison: Density Plots",
subtitle="Runs test detects differences in location, scale, and shape",
x="Reaction Time(ms)", y="Density") +
theme_minimal()
print(p2)
# Visualization 3: ECDF plot (empirical cumulative distribution)
p3 <- ggplot(data, aes(x=reaction_time, color=method)) +
stat_ecdf(geom="step", size=1.2) +
labs(title="Empirical Cumulative Distribution Functions",
subtitle="Separation indicates distribution differences",
x="Reaction Time(ms)", y="Cumulative Probability") +
theme_minimal()
print(p3)
# Visualization 4: Run sequence plot
cat("\n4. Creating Run Sequence Plot...\n")
# Combine and rank data
data_combined <- data %>%
arrange(reaction_time) %>%
mutate(
rank = row_number(),
group_code = ifelse(method == "Method_A", "A", "B")
)
p4 <- ggplot(data_combined, aes(x=rank, y=reaction_time, color=method, shape=method)) +
geom_point(size=3, alpha=0.7) +
geom_line(aes(group=1), color="gray80", size=0.5) +
labs(title="Run Sequence: Ordered Data by Rank",
subtitle="Clustering of colors indicates distribution differences",
x="Rank(ordered by RT)", y="Reaction Time(ms)") +
theme_minimal()
print(p4)
# ==============================================================================
# STEP 4: WALD-WOLFOWITZ RUNS TEST
# ==============================================================================
cat("\n=== WALD-WOLFOWITZ RUNS TEST ===\n")
# Extract group data
group_a <- data %>% filter(method == "Method_A") %>% pull(reaction_time)
group_b <- data %>% filter(method == "Method_B") %>% pull(reaction_time)
# Method 1: randtests package
cat("\nMethod 1: randtests::runs.test\n")
runs_result <- runs.test(x=group_a, y=group_b, alternative="two.sided")
print(runs_result)
# Method 2: DescTools package (alternative)
cat("\nMethod 2: DescTools::WaldWolfTest\n")
ww_result <- WaldWolfTest(x=group_a, y=group_b)
print(ww_result)
# Extract key statistics
n1 <- length(group_a)
n2 <- length(group_b)
runs_observed <- runs_result$statistic
runs_expected <- 1 + (2*n1*n2)/(n1+n2)
runs_sd <- sqrt((2*n1*n2*(2*n1*n2 - n1 - n2)) / ((n1+n2)^2 * (n1+n2-1)))
z_stat <- (runs_observed - runs_expected) / runs_sd
p_value <- runs_result$p.value
cat("\n--- Test Summary ---\n")
cat(sprintf("n₁ (Method A): %d\n", n1))
cat(sprintf("n₂ (Method B): %d\n", n2))
cat(sprintf("Runs observed: %.0f\n", runs_observed))
cat(sprintf("Runs expected(H₀): %.2f\n", runs_expected))
cat(sprintf("Standard deviation: %.2f\n", runs_sd))
cat(sprintf("Z-statistic: %.3f\n", z_stat))
cat(sprintf("p-value: %.4f\n", p_value))
cat(sprintf("Result: %s at α = .05\n",
ifelse(p_value < 0.05, "SIGNIFICANT", "Not significant")))
# ==============================================================================
# STEP 5: EFFECT SIZE
# ==============================================================================
cat("\n=== EFFECT SIZE ===\n")
# Standardized runs effect size
standardized_runs <- (runs_observed - runs_expected) / runs_sd
cat(sprintf("Standardized runs statistic: %.3f\n", standardized_runs))
cat("Interpretation: |z| < 1.96 (small), 1.96-2.58 (medium), > 2.58 (large)\n")
# Cohen's d for comparison (location effect)
cohen_d <- (mean(group_a) - mean(group_b)) /
sqrt(((n1-1)*var(group_a) + (n2-1)*var(group_b)) / (n1+n2-2))
cat(sprintf("\nCohen's d(for comparison): %.3f\n", cohen_d))
cat("Note: Runs test is sensitive to ALL distribution differences, not just location.\n")
# Overlap coefficient
min_max_a <- max(group_a)
max_min_b <- min(group_b)
overlap <- sum(group_a > max_min_b & group_a < min_max_a) / (n1 + n2)
cat(sprintf("\nDistribution overlap: %.1f%%\n", overlap*100))
# ==============================================================================
# STEP 6: COMPARE WITH MANN-WHITNEY U TEST
# ==============================================================================
cat("\n=== COMPARISON WITH MANN-WHITNEY U TEST ===\n")
cat("Mann-Whitney tests primarily location shift; Runs test detects ALL differences.\n\n")
mw_result <- wilcox.test(group_a, group_b, alternative="two.sided")
cat("Mann-Whitney U Test:\n")
cat(sprintf("W = %.1f, p = %.4f\n", mw_result$statistic, mw_result$p.value))
cat("\nInterpretation:\n")
if(p_value < 0.05 && mw_result$p.value < 0.05) {
cat("Both tests significant: distributions differ(likely in location AND scale/shape).\n")
} else if(p_value < 0.05 && mw_result$p.value >= 0.05) {
cat("Runs significant, M-W not: distributions differ in scale/shape, not location.\n")
} else if(p_value >= 0.05 && mw_result$p.value < 0.05) {
cat("M-W significant, Runs not: location shift present, but distributions similarly shaped.\n")
} else {
cat("Both tests non-significant: distributions are similar.\n")
}
# ==============================================================================
# STEP 7: INTERPRETATION
# ==============================================================================
cat("\n=== INTERPRETATION ===\n")
cat(sprintf(
"The Wald-Wolfowitz runs test showed %s difference between the two training\n",
ifelse(p_value < 0.05, "a significant", "no significant")))
cat(sprintf(
"methods, Z = %.3f, p = %.4f. The observed number of runs(%.0f) was %s\n",
z_stat, p_value, runs_observed,
ifelse(runs_observed < runs_expected, "fewer than expected", "similar to expected")))
cat(sprintf(
"under the null hypothesis(expected: %.2f), indicating %s.\n",
runs_expected,
ifelse(runs_observed < runs_expected, "clustering(distribution differences)", "random intermixing")))
if(p_value < 0.05) {
cat("\nThe distributions differ in location(Method A faster: M=%.1f vs M=%.1f)\n",
mean(group_a), mean(group_b))
cat("and/or variability(Method A SD=%.1f vs Method B SD=%.1f).\n",
sd(group_a), sd(group_b))
cat("This comprehensive distribution difference(detected by runs test) suggests\n")
cat("Method A produces more consistent, faster reaction times than Method B.\n")
}
# ==============================================================================
# APA REPORTING TEMPLATE
# ==============================================================================
cat("\n=== APA-STYLE REPORTING ===\n")
cat("A Wald-Wolfowitz runs test was conducted to compare reaction time distributions\n")
cat("between two training methods(Method A: n=25, Method B: n=25). The test evaluates\n")
cat("whether the two samples are randomly intermixed(null hypothesis) or systematically\n")
cat(sprintf("clustered(alternative). Results showed %s, Z = %.3f,\n",
ifelse(p_value < 0.05, "a significant difference", "no significant difference"),
z_stat))
cat(sprintf("p = %.4f, with %.0f runs observed versus %.2f expected under random mixing.\n",
p_value, runs_observed, runs_expected))
if(p_value < 0.05) {
cat(sprintf("Method A(M=%.1f ms, SD=%.1f) produced faster and less variable reaction times\n",
mean(group_a), sd(group_a)))
cat(sprintf("than Method B(M=%.1f ms, SD=%.1f), d = %.2f (large effect). The runs test detected\n",
mean(group_b), sd(group_b), abs(cohen_d)))
cat("comprehensive distribution differences beyond location shift alone, suggesting\n")
cat("Method A produces superior and more consistent performance.\n")
}
Z = -2.45, p = .014. Observed 18 runs vs. 26 expected, indicating clustering (systematic distribution differences). Method A showed faster (M=195ms) and less variable (SD=48ms) performance than Method B (M=215ms, SD=72ms), d = 0.34. Runs test detected comprehensive distribution differences beyond location alone.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Mann-Whitney U Test — Switch if you specifically need to test for a 'Shift in Median' rather than global distribution equality.
- Kolmogorov-Smirnov Test — Use the KS strike for higher sensitivity to deviations in the center of the distribution.
- Exact Permutation Strike — Use Monte Carlo simulations to calculate significance when many ranks are identical.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
- Compare with Mann-Whitney U (tests location shift)
- Compare with Kolmogorov-Smirnov (tests any distributional difference)
- Use exact vs asymptotic p-values for small samples
- Examine number of runs to characterize difference type
- Combine with graphical methods (Q-Q plots, density plots)
Wald-Wolfowitz runs test compares two distributions. Post-hoc tests are not applicable.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Z = (R - E[R]) / SD(R), where R = observed runs, E[R] = 1 + (2n₁n₂)/(n₁+n₂), SD(R) = sqrt((2n₁n₂(2n₁n₂-n₁-n₂))/((n₁+n₂)²(n₁+n₂-1))). Interpretation: |Z| < 1.96 (small), 1.96-2.58 (medium), > 2.58 (large effect at α=.05). Negative Z indicates fewer runs (clustering); positive Z indicates more runs (oscillation).
d = (M₁ - M₂) / SDpooled. Useful for comparing location component: |d| = 0.2 (small), 0.5 (medium), 0.8 (large). Note: Runs test is sensitive to ALL distribution differences, not just location, so Cohen's d only captures one component.
Proportion of values in overlapping range of distributions. 100% = complete overlap (distributions identical), 0% = no overlap (completely separated). Provides intuitive measure of distribution similarity.
Var₁/Var₂ or SD₁/SD₂. Quantifies scale differences. Ratio close to 1.0 indicates similar spreads; ratio >> 1 indicates one group has much greater variability. Useful when runs test detects differences but Mann-Whitney does not (indicating scale not location difference).
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
Each group should have n ≥ 10 for normal approximation to be accurate. With smaller samples (n < 10), use exact permutation test or lookup tables for critical values.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | Z ≈ 0.3 | Total N ≈ 350 |
| Medium Effect | Z ≈ 0.5 | Total N ≈ 130 |
| Large Effect | Z ≈ 0.8 | Total N ≈ 50 |
Runs test power depends on TYPE of distribution difference (location, scale, or shape). Power is highest for location shifts, moderate for scale differences, lower for pure shape differences. Extensive ties (>25%) reduce power by 10-20%. Unequal group sizes acceptable but reduce power; aim for balanced design.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A Wald-Wolfowitz runs test was conducted to compare outcome distributions between Group 1 (n=X) and Group 2 (n=X). The test evaluates whether the two samples are randomly intermixed (null hypothesis) or systematically clustered (alternative). Results showed a significant/no significant difference, Z = X.XX, p = .XXX, with X runs observed versus X.XX expected under random mixing. If significant: The Group 1 (M=XX, SD=XX, Mdn=XX) showed direction of difference compared to Group 2 (M=XX, SD=XX, Mdn=XX), indicating comprehensive distribution differences in location/scale/shape. Compare with Mann-Whitney if relevant to clarify what aspect differs. Conclude with interpretation.
- Z-statistic
- p-value
- number of runs observed
- number of runs expected
- n₁ and n₂ (sample sizes)
- descriptive statistics (means, SDs, medians, IQRs) for both groups
- effect size (standardized runs statistic, Cohen's d, and/or variance ratio)
- comparison with Mann-Whitney U if relevant to clarify distribution aspect
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Observed Runs | Expected Runs | z-statistic | p-value | Result |
|---|---|---|---|---|
| 28 | 50.5 | -4.12 | < .001 | Significantly Different Shapes |
The 'Mixing' count. We combine both samples and sort them. If they are from the same population, they should be well-mixed (high runs). If different, they will cluster (low runs).
The Deviation Meter. Measures how many standard errors the mixing level is away from perfect randomness.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Two-Sample Runs Test
randtests::runs.test(combined_vector)
# 2. Sequential Independence Test
tseries::runs.test(factor(df$y > median(df$y)))The Runs test is the most general test for distribution equality. It assumes NOTHING about shape, mean, or variance. It only looks at 'Clustering' in the data sequence.
# Audit for Autocorrelation in Residuals
randtests::runs.test(residuals(model))Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.