Runs Test for Randomness
Tests if sequence of binary outcomes is random; detects patterns, trends, or autocorrelation..
What is it?
Runs Test evaluates whether a binary sequence is ordered randomly, testing for systematic dependencies over time.
When to use it
- Sequence Randomness: Check binary data transitions (e.g. residual signs, heads/tails).
- Independence check: Verify zero autocorrelation assumptions.
Core Idea
A "run" is a consecutive block of identical states. Too few runs indicate clustering; too many runs indicate systematic oscillation:
Hypotheses
How it works
- Count occurrences of State A (n1) and State B (n2).
- Count observed run boundaries (R).
- Compute expected runs: E(R) = (2n1n2/N) + 1.
- Calculate Z-score of deviation. Low p-value indicates non-randomness.
Assumptions
Effect Size
Represented by the **ratio of observed to expected runs**. Ratio < 1 indicates clustering, while Ratio > 1 indicates fast oscillation.
Quick Example
| Seq | Observed Runs | Expected | p-value |
|---|---|---|---|
| A B A B A B | 6 | 4.0 | 0.180 (Oscillating) |
| A A A B B B | 2 | 4.0 | 0.015 (Clustered) |
Sequence Runs Live Laboratory
Change the probability bias of State A to see how clustering blocks form.
| Metric | Value |
|---|---|
| Count State A | 11 |
| Count State B | 19 |
| Observed Runs (R) | 11 |
| Expected Runs E(R) | 14.93 |
| p-value (two-tailed) | 0.1176 |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: Sequence is random (independent observations)
Hₐ: Sequence is NOT random (has pattern/trend/autocorrelation)
Counts 'runs' (uninterrupted sequences of same value). Too few runs = clustering/trend/positive autocorrelation. Too many runs = oscillation/alternation/negative autocorrelation. Can test one-tailed for specific patterns.
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.
- Number of runs (R) — Count of uninterrupted sequences of identical valuesR observed; compare to expected E(R). Low R = clustering, high R = alternation. Range: 2 to n (2 = perfect clustering, n = perfect alternation).
- Expected runs E(R) — Expected number of runs under randomness: E(R) = (2n₁n₂)/(n₁+n₂) + 1Baseline for comparison. Depends on n₁ and n₂. Balanced samples maximize E(R).
- Variance of runs — Var(R) = (2n₁n₂(2n₁n₂ - n₁ - n₂)) / ((n₁+n₂)²(n₁+n₂-1)); used for Z-score calculationQuantifies sampling variability. Larger samples → smaller variance → more precise test.
- Z-score (standardized runs statistic) — Z = (R - E(R)) / √Var(R); standardized deviation from expectedZ < -2: too few runs (clustering). Z > 2: too many runs (alternation). |Z| < 2: consistent with randomness.
- p-value — Probability of observing R as extreme under H₀ (randomness)p < 0.05 → reject randomness. But consider context: block randomization produces low R intentionally.
- Sequence plot (time series visualization) — Visual inspection of sequence for patterns, trends, clustersLook for: runs of same value, temporal trends, periodic patterns, regime changes. Essential complement to test.
- Autocorrelation function (ACF) — Quantifies autocorrelation at multiple lags; more informative than runs testACF(1) > 0: positive autocorr (clustering). ACF(1) < 0: negative autocorr (alternation). More powerful than runs test.
- Run lengths distribution — Histogram of lengths of runs (how long sequences persist)Long runs suggest clustering. All runs length 1 = perfect alternation. Geometric distribution expected under randomness.
- Comparison with Durbin-Watson test — Alternative autocorrelation test (for regression residuals); compare resultsIf both agree → strong evidence. Durbin-Watson more powerful for lag-1 autocorrelation. Use for residuals.
- Cumulative proportions plot — Shows if category proportions stable over time (e.g., cum. prop. of heads should → 0.5)Systematic drift suggests bias. Should fluctuate randomly around expected proportion.
- Expected vs observed runs confidence interval — Shows sampling variability of R; 95% CI for R under randomnessIf observed R outside CI → significant departure. Visual assessment of effect magnitude.
- Permutation test (exact p-value) — Compute exact p-value via permutation (for small samples or verification)More accurate than normal approximation for small n. Use as gold standard for validation.
- Temporal stability check (split-half analysis) — Test randomness separately in first vs second half of sequenceIf one half random, other not → temporal change in process. Identifies when non-randomness emerges.
- Rolling/moving runs test — Apply runs test to sliding window to detect localized patternsIdentifies when/where randomness violated. E.g., early clustering, later random.
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Test Randomness of Coin Flips
Test if sequence of 100 coin flips (H/T) is random or shows patterns (e.g., clustering, alternation). Classic application of runs test.
# Runs Test for Randomness: Coin Flip Sequence
# Test if sequence of H/T is random
library(randtests) # runs.test
library(ggplot2)
library(gridExtra)
set.seed(42)
# Generate coin flip data
# Scenario: Mostly random, but slight tendency toward clustering
flips <- sample(c("H", "T"), 100, replace = TRUE, prob = c(0.5, 0.5))
# Add some artificial clustering (for demonstration)
flips[20:25] <- "H" # Cluster of heads
flips[60:67] <- "T" # Cluster of tails
# Convert to binary (0/1 for calculations)
flips_binary <- ifelse(flips == "H", 1, 0)
cat("RUNS TEST FOR RANDOMNESS: COIN FLIPS\n")
cat("======================================\n\n")
# ====================
# 1. DATA SUMMARY
# ====================
cat("DATA SUMMARY\n")
cat("=============\n\n")
cat(" Total flips: n =", length(flips), "\n")
n_heads <- sum(flips == "H")
n_tails <- sum(flips == "T")
cat(" Heads: n₁ =", n_heads, "(", round(n_heads/length(flips)*100, 1), "%)\n")
cat(" Tails: n₂ =", n_tails, "(", round(n_tails/length(flips)*100, 1), "%)\n\n")
cat(" Sample sequence(first 30):")
cat("\n ", paste(flips[1:30], collapse = " "), "\n\n")
# ====================
# 2. CHECK ASSUMPTIONS
# ====================
cat("ASSUMPTION CHECKS\n")
cat("==================\n\n")
# A1: Binary data
cat("1. Binary data:\n")
cat(" Categories: H and T\n")
cat(" ✓ Binary outcomes\n\n")
# A2: Sequential
cat("2. Sequential order:\n")
cat(" ✓ Flips ordered by time\n\n")
# A3: Sample size
cat("3. Sample size check:\n")
cat(" n₁ =", n_heads, ", n₂ =", n_tails, "\n")
if (n_heads >= 10 & n_tails >= 10) {
cat(" ✓ Both n₁, n₂ ≥ 10 (normal approximation valid)\n\n")
} else {
cat(" ⚠ Small sample - use exact test\n\n")
}
# A4: Balance
cat("4. Category balance:\n")
ratio <- min(n_heads, n_tails) / max(n_heads, n_tails)
cat(" Ratio min/max =", round(ratio, 3), "\n")
if (ratio >= 0.5) {
cat(" ✓ Reasonably balanced\n\n")
} else {
cat(" ⚠ Imbalanced - may reduce power\n\n")
}
# ====================
# 3. COUNT RUNS MANUALLY
# ====================
cat("RUNS CALCULATION\n")
cat("=================\n\n")
# Count runs
runs_count <- 1
for (i in 2:length(flips)) {
if (flips[i] != flips[i-1]) {
runs_count <- runs_count + 1
}
}
cat(" Observed runs(R):", runs_count, "\n\n")
# Expected runs under randomness
expected_runs <- (2 * n_heads * n_tails) / (n_heads + n_tails) + 1
cat(" Expected runs E(R):", round(expected_runs, 2), "\n\n")
# Variance of runs
var_runs <- (2 * n_heads * n_tails * (2 * n_heads * n_tails - n_heads - n_tails)) /
((n_heads + n_tails)^2 * (n_heads + n_tails - 1))
sd_runs <- sqrt(var_runs)
cat(" SD of runs σ(R):", round(sd_runs, 2), "\n\n")
# Z-score
z_score <- (runs_count - expected_runs) / sd_runs
cat(" Z-score:", round(z_score, 3), "\n")
cat(" Interpretation:\n")
if (z_score < -2) {
cat(" Too FEW runs → clustering/trend(positive autocorrelation)\n")
} else if (z_score > 2) {
cat(" Too MANY runs → oscillation/alternation(negative autocorrelation)\n")
} else {
cat(" Runs count consistent with randomness\n")
}
cat("\n")
# ====================
# 4. RUNS TEST (FORMAL)
# ====================
cat("RUNS TEST\n")
cat("==========\n\n")
# Note: randtests::runs.test expects factor
flips_factor <- factor(flips_binary)
runs_result <- runs.test(flips_factor, alternative = "two.sided")
cat("Test Results:\n")
cat(" Observed runs:", runs_result$statistic, "\n")
cat(" Expected runs:", round(expected_runs, 2), "\n")
cat(" Z-statistic:", round(runs_result$statistic, 3), "\n")
cat(" p-value:", round(runs_result$p.value, 4), "\n\n")
alpha <- 0.05
if (runs_result$p.value < alpha) {
cat("Decision: REJECT H₀ (α = 0.05)\n")
cat("Interpretation: Sequence is NOT random\n")
if (runs_count < expected_runs) {
cat(" Pattern: Too few runs → CLUSTERING\n\n")
} else {
cat(" Pattern: Too many runs → ALTERNATION\n\n")
}
else {
cat("Decision: FAIL TO REJECT H₀ (α = 0.05)\n")
cat("Interpretation: Sequence consistent with randomness\n\n")
}
# ====================
# 5. AUTOCORRELATION CHECK
# ====================
cat("AUTOCORRELATION ANALYSIS\n")
cat("=========================\n\n")
# Lag-1 autocorrelation
acf_result <- acf(flips_binary, lag.max = 10, plot = FALSE)
cat(" Lag-1 autocorrelation:", round(acf_result$acf[2], 3), "\n")
if (abs(acf_result$acf[2]) > 0.2) {
cat(" ⚠ Moderate autocorrelation detected\n\n")
} else {
cat(" ✓ Low autocorrelation\n\n")
}
# ====================
# 6. VISUALIZATIONS (6 plots)
# ====================
# Plot 1: Sequence plot
sequence_df <- data.frame(
Position = 1:length(flips),
Outcome = flips,
Binary = flips_binary
)
p1 <- ggplot(sequence_df, aes(x = Position, y = Binary, color = Outcome)) +
geom_point(size = 2, alpha = 0.7) +
geom_line(alpha = 0.3) +
scale_color_manual(values = c("H" = "blue", "T" = "red")) +
scale_y_continuous(breaks = c(0, 1), labels = c("T", "H")) +
labs(title = "Coin Flip Sequence",
subtitle = paste0("Observed runs: ", runs_count, ", Expected: ",
round(expected_runs, 1)),
x = "Flip Number", y = "Outcome") +
theme_minimal() +
theme(legend.position = "top")
# Plot 2: Runs distribution (under H₀)
p2_data <- data.frame(
runs = seq(max(1, expected_runs - 4*sd_runs), expected_runs + 4*sd_runs, 0.5)
)
p2_data$density <- dnorm(p2_data$runs, mean = expected_runs, sd = sd_runs)
p2 <- ggplot(p2_data, aes(runs, density)) +
geom_line(size = 1, color = "darkgreen") +
geom_area(alpha = 0.3, fill = "darkgreen") +
geom_vline(xintercept = runs_count, color = "red", linetype = "dashed", size = 1.2) +
geom_vline(xintercept = expected_runs, color = "blue", linetype = "dotted", size = 1) +
annotate("text", x = runs_count, y = max(p2_data$density) * 0.8,
label = paste0("Observed\nR = ", runs_count),
hjust = -0.1, color = "red") +
labs(title = "Runs Distribution Under H₀",
subtitle = "Red = Observed, Blue = Expected",
x = "Number of Runs", y = "Density") +
theme_minimal()
# Plot 3: Run lengths distribution
run_lengths <- rle(as.character(flips))
run_df <- data.frame(
Length = run_lengths$lengths,
Value = run_lengths$values
)
p3 <- ggplot(run_df, aes(x = Length, fill = Value)) +
geom_histogram(binwidth = 1, alpha = 0.7, position = "dodge", color = "black") +
scale_fill_manual(values = c("H" = "blue", "T" = "red")) +
labs(title = "Run Lengths Distribution",
subtitle = "Length of consecutive H or T sequences",
x = "Run Length", y = "Frequency",
fill = "Outcome") +
theme_minimal() +
theme(legend.position = "top")
# Plot 4: Cumulative proportion
cumulative_df <- data.frame(
Position = 1:length(flips),
CumProp_H = cumsum(flips == "H") / (1:length(flips)),
CumProp_T = cumsum(flips == "T") / (1:length(flips))
)
p4 <- ggplot(cumulative_df, aes(x = Position)) +
geom_line(aes(y = CumProp_H, color = "Heads"), size = 1) +
geom_line(aes(y = CumProp_T, color = "Tails"), size = 1) +
geom_hline(yintercept = 0.5, linetype = "dashed", color = "gray50") +
scale_color_manual(values = c("Heads" = "blue", "Tails" = "red")) +
labs(title = "Cumulative Proportions Over Time",
subtitle = "Should converge to 0.5 if fair coin",
x = "Flip Number", y = "Cumulative Proportion",
color = "") +
theme_minimal() +
theme(legend.position = "top")
# Plot 5: Autocorrelation function (ACF)
acf_df <- data.frame(
Lag = 1:10,
ACF = acf_result$acf[2:11]
)
p5 <- ggplot(acf_df, aes(x = Lag, y = ACF)) +
geom_col(fill = "steelblue", alpha = 0.7) +
geom_hline(yintercept = 0, color = "black") +
geom_hline(yintercept = c(-1.96/sqrt(length(flips)), 1.96/sqrt(length(flips))),
linetype = "dashed", color = "blue") +
labs(title = "Autocorrelation Function(ACF)",
subtitle = "Blue lines = 95% significance bounds",
x = "Lag", y = "Autocorrelation") +
theme_minimal()
# Plot 6: Comparison to random sequences
set.seed(999)
n_sim <- 500
random_runs <- replicate(n_sim, {
sim_flips <- sample(c("H", "T"), 100, replace = TRUE)
sum(rle(sim_flips)$lengths > 0)
})
p6 <- ggplot(data.frame(Runs = random_runs), aes(Runs)) +
geom_histogram(bins = 30, fill = "lightgray", color = "black", alpha = 0.7) +
geom_vline(xintercept = runs_count, color = "red", linetype = "dashed", size = 1.2) +
geom_vline(xintercept = expected_runs, color = "blue", linetype = "dotted", size = 1) +
annotate("text", x = runs_count, y = 40,
label = paste0("Observed = ", runs_count),
hjust = -0.1, color = "red") +
labs(title = "Simulated Runs Distribution(500 random sequences)",
subtitle = paste0("Percentile of observed: ",
round(mean(random_runs <= runs_count) * 100, 1), "%"),
x = "Number of Runs", y = "Frequency") +
theme_minimal()
grid.arrange(p1, p2, p3, p4, p5, p6, ncol = 2)
# ====================
# 7. INTERPRETATION
# ====================
cat("\nINTERPRETATION\n")
cat("===============\n\n")
cat("The runs test assessed randomness of coin flip sequence.\n\n")
cat("Key findings:\n")
cat(" • Observed runs:", runs_count, "\n")
cat(" • Expected runs:", round(expected_runs, 2), "\n")
cat(" • Z-score:", round(z_score, 3), "\n")
cat(" • p-value:", round(runs_result$p.value, 3), "\n")
cat(" • Lag-1 autocorrelation:", round(acf_result$acf[2], 3), "\n\n")
if (runs_result$p.value < 0.05) {
cat("Conclusion: Sequence is NOT random.\n")
if (runs_count < expected_runs) {
cat(" Pattern detected: TOO FEW runs\n")
cat(" Interpretation: Clustering or trend present\n")
cat(" Implication: Positive autocorrelation(outcomes tend to persist)\n")
} else {
cat(" Pattern detected: TOO MANY runs\n")
cat(" Interpretation: Excessive alternation\n")
cat(" Implication: Negative autocorrelation(outcomes tend to switch)\n")
}
else {
cat("Conclusion: Sequence is consistent with randomness.\n")
cat(" No evidence of clustering, trends, or systematic patterns.\n")
cat(" Coin flipping process appears to produce independent outcomes.\n")
}
cat("\nPractical notes:\n")
cat(" • Runs test is simple screening tool for randomness\n")
cat(" • Less powerful than ACF for detecting autocorrelation\n")
cat(" • Useful for quick visual + statistical check\n")
cat(" • Follow up with ACF/PACF for detailed autocorrelation analysis\n")
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Conditional Runs Test — Adjust the p-value if one binary state (e.g., Success) is 3x more common than the other.
- Wald-Wolfowitz Strike — The exact alternative for small imbalanced sequences.
- Breusch-Godfrey Test — Audit for higher-order serial dependence if the runs test reveals clustering.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
- Compare with Wald-Wolfowitz runs test (two-sample version)
- Examine runs above/below median separately
- Use exact vs asymptotic p-values for small samples
- Plot sequence to visually assess patterns
- Compare with Ljung-Box test for autocorrelation
Runs test assesses randomness of a sequence. Post-hoc tests are not applicable.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Serial Independence' Minimum: A minimum of 20 observations is required. Randomness cannot be meaningfully distinguish from pattern in a sequence that is too short to allow for 'Runs' to emerge.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | Subtle Clustering | n ≈ 100 |
| Medium Effect | Moderate Clustering | n ≈ 40 |
| Large Effect | Severe Clustering | n ≈ 20 |
The 'Binary Pivot': Runs test power is maximized when the two binary states (e.g., Above/Below Median) are balanced (50/50). If one state is rare (e.g., 90% Success), you must triple your sequence length to maintain discovery authority.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Total Runs | Expected Runs | z-statistic | p-value | Result |
|---|---|---|---|---|
| 32 | 50.5 | -3.82 | < .001 | NON-RANDOM (Clustered) |
The 'Switch' Count. The number of times the sequence crosses the median. Too few runs = Clustering; Too many runs = Alternating.
Independence Probability. If p < .05, the sequence has 'Memory' and the observations are not independent.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Runs Test for randomness
randtests::runs.test(x)Runs test detects non-random patterns like clustering or systematic oscillation. Always supplement with a sequence line plot to observe the cycle.
# Check for sequential autocorrelation to detect time dependency
acf(x, plot = TRUE)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.