Kolmogorov-Smirnov Test
The engine for Distributional Discovery. This model audits the maximum distance between your sample distribution and a theoretical benchmark (1-sample) or another group (2-sample), revealing if your data fits the expected shape.
What is it?
Kolmogorov-Smirnov (KS) Test compares the cumulative probability distributions of two samples to verify if they are drawn from the same underlying distribution.
When to use it
- Two Distributions: Compare shapes, locations, and scales of Group 1 vs. Group 2.
- Continuity: Outcomes must be measured on a continuous interval.
Core Idea
Plots empirical cumulative frequencies. The KS statistic (D) is the maximum vertical distance between the two CDF step lines:
Hypotheses
How it works
- Construct cumulative fractions (ECDF) for both sorted samples.
- Compare heights of ECDFs across all observed values.
- Locate maximum vertical difference: D = max|F1(x) - F2(x)|.
- Compare D against critical value based on sample sizes.
Assumptions
Effect Size
The **D statistic** itself serves as a standardized effect size indicating the maximum percentile separation between the two distributions.
Quick Example
| Comparison | D Stat | p-value |
|---|---|---|
| Overlap (H0 retained) | 0.182 | 0.640 |
| Shifted (H0 rejected) | 0.534 | 0.012 |
Kolmogorov-Smirnov Live CDF Laboratory
Change the mean difference between Group 1 and Group 2 to watch empirical step divergence.
| Metric | Value |
|---|---|
| KS Distance (D) | 0.4667 |
| Critical D (alpha = 0.05) | 0.4966 |
| Hypothesis Result | Fail to Reject |
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: Sample comes from specified distribution F₀(x) (one-sample) or both samples come from same distribution (two-sample)
Hₐ: Distributions differ
Compares empirical cumulative distribution function (ECDF) to theoretical CDF (one-sample) or two empirical CDFs (two-sample). Maximum vertical distance = D statistic.
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.
- D statistic — Maximum vertical distance between ECDFs (effect size)0 ≤ D ≤ 1; D=0 perfect match, larger D = greater deviation. Guidelines: <0.10 small, 0.10-0.25 moderate, >0.25 large
- p-value — Probability of observing D as extreme under H₀p < α (typically 0.05) → reject H₀ (distributions differ). Sensitive to sample size - always check D magnitude
- ECDF plots — Visual comparison of empirical vs theoretical (one-sample) or two empirical CDFs (two-sample)Maximum vertical gap = D statistic. Identify where distributions differ (location, spread, tails, shape)
- Critical value — Threshold D must exceed for significance at α levelCritical value ≈ 1.36/√n (α=0.05, large n). If D > critical value → reject H₀
- Sample sizes — n (one-sample) or n₁, n₂ (two-sample) determine power and critical valuesLarger n → smaller critical value → more power. Two-sample: balanced n₁≈n₂ optimal; very unequal reduces power
- ECDF overlay with confidence bands — Shows sampling variability around ECDFIf theoretical CDF falls outside confidence bands → likely violation; helps distinguish sampling error from true difference
- CDF deviation plot (ECDF - Theoretical) — Highlights regions where distributions differ mostPositive deviations = empirical exceeds theoretical (heavier left tail or shifted right); shows location of maximum D
- Bootstrap D distribution — Empirical sampling distribution of D statistic; provides confidence intervalIf CI for D excludes 0 → significant difference. Shows stability of D estimate
- Q-Q plot (quantile-quantile) — Compare quantiles of two distributions; complements ECDFPoints on diagonal = identical distributions. Deviations show location/scale/shape differences. More intuitive than ECDF for many
- Comparison with Shapiro-Wilk (if testing normality) — Shapiro-Wilk more powerful for normality; compare resultsIf SW rejects but KS doesn't → suggests low power of KS. Prefer SW for normality testing
- Comparison with Anderson-Darling — AD more sensitive to tail differences; helpful comparisonIf AD rejects but KS doesn't → tail deviations present. AD weights tails more heavily
- Histogram/density overlay — Intuitive visualization of distributional differencesShows shape, location, spread differences. Easier for non-statisticians than ECDF
- Skewness and kurtosis (when testing normality) — Quantify specific departures from normalityNormal: skewness ≈ 0, kurtosis ≈ 3. High |skewness| (>1) or excess kurtosis suggests non-normality
- Power analysis / sample size sensitivity — Shows how n affects ability to detect effect of given sizeFor D=0.20 (moderate), n=100 gives ~80% power. Helps plan studies or interpret non-significant results
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Do Exam Scores Follow N(75, 15)?
Test if exam scores follow a specified normal distribution N(μ=75, σ=15). This is appropriate when theoretical distribution parameters are known a priori (e.g., from test design specifications).
# One-Sample Kolmogorov-Smirnov Test
# Test if exam scores follow N(75, 15)
# Load packages
library(ggplot2)
library(gridExtra)
library(boot)
# Set seed for reproducibility
set.seed(42)
# Generate exam score data (n=50)
# True distribution: N(73, 16) - slightly off from N(75, 15)
exam_scores <- rnorm(50, mean = 73, sd = 16)
# ====================
# 1. CHECK ASSUMPTIONS
# ====================
cat("ASSUMPTION CHECKS\n")
cat("==================\n\n")
# A1: Data is continuous
cat("1. Continuous data:\n")
cat(" Unique values:", length(unique(exam_scores)), "out of", length(exam_scores), "\n")
cat(" Data type:", class(exam_scores), "\n\n")
# A2: Independence
cat("2. Independence: Verified by design(different students)\n\n")
# A3: Fully specified distribution
cat("3. Fully specified distribution:\n")
cat(" Testing against N(75, 15) - parameters from test design\n")
cat(" NOT estimated from data(would require Lilliefors test)\n\n")
# A4: Ties check
cat("4. Ties check:\n")
ties_prop <- 1 - length(unique(exam_scores)) / length(exam_scores)
cat(" Proportion of ties:", round(ties_prop * 100, 2), "%\n")
if (ties_prop < 0.05) {
cat(" ✓ Few ties - KS test appropriate\n\n")
} else {
cat(" ⚠ Many ties - consider Anderson-Darling\n\n")
}
# ====================
# 2. RUN KS TEST
# ====================
cat("KOLMOGOROV-SMIRNOV TEST\n")
cat("========================\n\n")
# One-sample KS test
ks_result <- ks.test(exam_scores, "pnorm", mean = 75, sd = 15)
cat("Hypothesis Test Results:\n")
cat(" D statistic:", round(ks_result$statistic, 4), "\n")
cat(" p-value:", round(ks_result$p.value, 4), "\n")
cat(" Sample size: n =", length(exam_scores), "\n\n")
# Decision
alpha <- 0.05
if (ks_result$p.value < alpha) {
cat("Decision: REJECT H₀ (α = 0.05)\n")
cat("Interpretation: Scores do NOT follow N(75, 15)\n\n")
} else {
cat("Decision: FAIL TO REJECT H₀ (α = 0.05)\n")
cat("Interpretation: Insufficient evidence that scores differ from N(75, 15)\n\n")
}
# ====================
# 3. BOOTSTRAP CI FOR D
# ====================
cat("Bootstrap 95% CI for D statistic\n")
cat("=================================\n\n")
boot_ks <- function(data, indices) {
d <- data[indices]
ks_test <- ks.test(d, "pnorm", mean = 75, sd = 15)
return(ks_test$statistic)
}
boot_results <- boot(exam_scores, boot_ks, R = 2000)
boot_ci <- boot.ci(boot_results, type = "perc")
cat(" 95% CI for D: [", round(boot_ci$percent[4], 4), ",",
round(boot_ci$percent[5], 4), "]\n\n")
# ====================
# 4. EFFECT SIZE
# ====================
cat("EFFECT SIZE\n")
cat("============\n\n")
cat(" D statistic:", round(ks_result$statistic, 4), "\n")
cat(" Interpretation:\n")
if (ks_result$statistic < 0.10) {
cat(" Small deviation from N(75, 15)\n")
} else if (ks_result$statistic < 0.25) {
cat(" Moderate deviation from N(75, 15)\n")
} else {
cat(" Large deviation from N(75, 15)\n")
}
# Cohen's d for location shift
mean_diff <- mean(exam_scores) - 75
cohens_d <- mean_diff / 15
cat("\n Location shift(Cohen's d):", round(cohens_d, 3), "\n")
cat(" (Difference in means / theoretical SD)\n\n")
# ====================
# 5. VISUALIZATIONS (6 plots)
# ====================
# Plot 1: ECDF vs Theoretical CDF
p1 <- ggplot(data.frame(x = exam_scores), aes(x)) +
stat_ecdf(geom = "step", color = "blue", size = 1) +
stat_function(fun = pnorm, args = list(mean = 75, sd = 15),
color = "red", linetype = "dashed", size = 1) +
labs(title = "ECDF vs Theoretical CDF",
subtitle = "Blue = Empirical, Red = N(75, 15)",
x = "Exam Score", y = "Cumulative Probability") +
theme_minimal()
# Plot 2: Histogram with theoretical density
p2 <- ggplot(data.frame(x = exam_scores), aes(x)) +
geom_histogram(aes(y = after_stat(density)), bins = 15,
fill = "lightblue", color = "black", alpha = 0.7) +
stat_function(fun = dnorm, args = list(mean = 75, sd = 15),
color = "red", size = 1) +
labs(title = "Histogram with Theoretical Density",
subtitle = "Red curve = N(75, 15)",
x = "Exam Score", y = "Density") +
theme_minimal()
# Plot 3: Q-Q plot
p3 <- ggplot(data.frame(sample = exam_scores), aes(sample = sample)) +
stat_qq(distribution = qnorm, dparams = list(mean = 75, sd = 15)) +
stat_qq_line(distribution = qnorm, dparams = list(mean = 75, sd = 15),
color = "red") +
labs(title = "Q-Q Plot vs N(75, 15)",
x = "Theoretical Quantiles", y = "Sample Quantiles") +
theme_minimal()
# Plot 4: Deviation plot (ECDF - Theoretical CDF)
ecdf_fun <- ecdf(exam_scores)
x_seq <- seq(min(exam_scores), max(exam_scores), length.out = 200)
deviations <- ecdf_fun(x_seq) - pnorm(x_seq, mean = 75, sd = 15)
p4 <- ggplot(data.frame(x = x_seq, dev = deviations), aes(x, dev)) +
geom_line(color = "darkgreen", size = 1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
geom_hline(yintercept = c(-ks_result$statistic, ks_result$statistic),
linetype = "dotted", color = "red") +
labs(title = "CDF Deviation Plot",
subtitle = "ECDF - Theoretical CDF(red lines = ±D)",
x = "Exam Score", y = "Deviation") +
theme_minimal()
# Plot 5: Bootstrap distribution of D
p5 <- ggplot(data.frame(D = boot_results$t), aes(D)) +
geom_histogram(bins = 30, fill = "steelblue", color = "black", alpha = 0.7) +
geom_vline(xintercept = ks_result$statistic, color = "red",
linetype = "dashed", size = 1) +
labs(title = "Bootstrap Distribution of D Statistic",
subtitle = paste0("Red line = observed D = ", round(ks_result$statistic, 3)),
x = "D Statistic", y = "Frequency") +
theme_minimal()
# Plot 6: Descriptive statistics comparison
stats_df <- data.frame(
Statistic = c("Mean", "SD", "Median", "IQR"),
Sample = c(mean(exam_scores), sd(exam_scores),
median(exam_scores), IQR(exam_scores)),
Theoretical = c(75, 15, 75, 15 * 1.349)
)
p6 <- ggplot(stats_df, aes(x = Statistic)) +
geom_point(aes(y = Sample, color = "Sample"), size = 4) +
geom_point(aes(y = Theoretical, color = "Theoretical"), size = 4) +
geom_segment(aes(xend = Statistic, y = Sample, yend = Theoretical),
linetype = "dashed", color = "gray50") +
scale_color_manual(values = c("Sample" = "blue", "Theoretical" = "red")) +
labs(title = "Sample vs Theoretical Statistics",
y = "Value", color = "") +
theme_minimal() +
theme(legend.position = "top")
# Display all plots
grid.arrange(p1, p2, p3, p4, p5, p6, ncol = 2)
# ====================
# 6. INTERPRETATION
# ====================
cat("\nINTERPRETATION\n")
cat("===============\n\n")
cat("The one-sample Kolmogorov-Smirnov test compared exam scores\n")
cat("to the theoretical distribution N(75, 15).\n\n")
cat("Key findings:\n")
cat(" • D =", round(ks_result$statistic, 3), "(maximum CDF deviation)\n")
cat(" • p =", round(ks_result$p.value, 3), "\n")
cat(" • Sample mean:", round(mean(exam_scores), 1), "vs theoretical: 75\n")
cat(" • Sample SD:", round(sd(exam_scores), 1), "vs theoretical: 15\n\n")
if (ks_result$p.value < 0.05) {
cat("Conclusion: Scores significantly differ from N(75, 15).\n")
cat("The ECDF shows notable deviation from the theoretical CDF.\n")
} else {
cat("Conclusion: Scores are consistent with N(75, 15).\n")
cat("No significant evidence of distributional mismatch.\n")
}
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Monte Carlo KS — Resample the null distribution to calculate exact p-values for discrete data.
- Chi-Square GoF — Use the more robust categorical basis for highly tied samples.
- Lilliefors Strike — Apply the specialized correction when Mean/SD are estimated from the sample.
- Anderson-Darling Test — Pivot to this elite standard for higher sensitivity to tail-deviations.
- Shapiro-Wilk Test — Return to the most powerful normality-specific audit available.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
- Compare with Anderson-Darling (more sensitive to tails)
- Compare with Shapiro-Wilk for normality testing
- Use Lilliefors correction when parameters are estimated
- Examine empirical CDF plots for visual assessment
- Two-sample K-S: examine where distributions differ most (max D location)
Kolmogorov-Smirnov tests distribution fit or 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.
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Continuity Minimum': A minimum of 20 participants per group is required for a 2-sample audit. KS math requires enough temporal or score depth to construct a stable 'Cumulative Curve'.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | Low Divergence (D=.10) | n ≈ 1000 total |
| Medium Effect | Moderate Divergence (D=.20) | n ≈ 120 total |
| Large Effect | High Divergence (D=.35) | n ≈ 40 total |
The 'Tie Penalty': If your data is discrete or has many identical values, the cumulative steps will 'Jump', effectively halving the sensitivity of the KS strike. Increase N by 25% for Likert or low-resolution scales.
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.
| Comparison | D (Statistic) | p-value | Conclusion |
|---|---|---|---|
| Sample A ↔ Sample B | 0.28 | .034 | Significantly Different Distributions |
The Maximum Gap. Represents the largest vertical distance between the Cumulative Distribution Functions (CDFs) of the two samples.
The Identity Probability. If p < .05, we reject the idea that the two samples were drawn from the same population distribution.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Two-Sample K-S Test
ks.test(df$sample_a, df$sample_b)
# 2. Visualize the CDF Gap
plot(ecdf(df$sample_a))
lines(ecdf(df$sample_b), col='red')The K-S test is sensitive to ANY difference: location, scale, OR shape. If you only care about means, use Mann-Whitney. Use K-S to prove two populations are identical in every way.
# Execute K-S for Continuous Data (Corrected for ties)
dgof::ks.test(x, y)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.