Kolmogorov-Smirnov Plot for Distribution Comparison — ggplot2

A Kolmogorov-Smirnov (K-S) plot compares two empirical cumulative distribution functions (ECDFs) and visualizes the K-S statistic, which is the maximum vertical distance between the two distributions. The plot displays both ECDFs as step functions, highlights the point of maximum divergence, and typically reports the K-S statistic value and p-value for hypothesis testing. This visualization is essential for determining whether two samples come from the same underlying distribution.

Kolmogorov-Smirnov Plot for Distribution Comparison rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 90/100 | Created: 2026-05-29

library(ggplot2)
library(dplyr)
library(scales)
library(ragg)

set.seed(42)

# Theme tokens — Imprint palette
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"

IMPRINT_PALETTE <- c(
  "#009E73",  # 1 — brand green
  "#C475FD",  # 2 — lavender
  "#4467A3",  # 3 — blue
  "#BD8233",  # 4 — ochre
  "#AE3030",  # 5 — matte red
  "#2ABCCD",  # 6 — cyan
  "#954477",  # 7 — rose
  "#99B314"   # 8 — lime
)

# Data: credit scoring — Good vs Bad customer score distributions
n_good <- 500
n_bad  <- 500

good_scores <- rnorm(n_good, mean = 680, sd = 60)
bad_scores  <- rnorm(n_bad,  mean = 580, sd = 80)

# Kolmogorov-Smirnov test
ks_result <- ks.test(good_scores, bad_scores)

# Find K-S statistic location — maximum vertical distance between ECDFs
all_x     <- sort(unique(c(good_scores, bad_scores)))
ecdf_good <- ecdf(good_scores)
ecdf_bad  <- ecdf(bad_scores)
good_vals <- ecdf_good(all_x)
bad_vals  <- ecdf_bad(all_x)
diffs     <- abs(good_vals - bad_vals)
max_idx   <- which.max(diffs)
ks_x      <- all_x[max_idx]
ks_y_lo   <- min(good_vals[max_idx], bad_vals[max_idx])
ks_y_hi   <- max(good_vals[max_idx], bad_vals[max_idx])
ks_y_mid  <- (ks_y_lo + ks_y_hi) / 2

# Annotation text
ks_d     <- round(as.numeric(ks_result$statistic), 3)
ks_p_fmt <- formatC(ks_result$p.value, format = "e", digits = 2)
annot    <- paste0("D = ", ks_d, "\np = ", ks_p_fmt)

# Long-format data frame for stat_ecdf
df_scores <- data.frame(
  score = c(good_scores, bad_scores),
  group = factor(
    c(rep("Good Customers", n_good), rep("Bad Customers", n_bad)),
    levels = c("Good Customers", "Bad Customers")
  )
)

# K-S segment data frame
ks_seg <- data.frame(x = ks_x, xend = ks_x, y = ks_y_lo, yend = ks_y_hi)

# Plot
p <- ggplot(df_scores, aes(x = score, color = group, linetype = group)) +
  stat_ecdf(geom = "step", linewidth = 1.2, pad = FALSE) +
  geom_segment(
    data = ks_seg,
    aes(x = x, xend = xend, y = y, yend = yend),
    color       = INK,
    linewidth   = 0.9,
    linetype    = "dotdash",
    inherit.aes = FALSE
  ) +
  annotate(
    "label",
    x             = ks_x + 22,
    y             = ks_y_mid,
    label         = annot,
    color         = INK,
    fill          = ELEVATED_BG,
    size          = 3.0,
    hjust         = 0,
    label.padding = unit(0.4, "lines"),
    label.size    = 0.25,
    label.r       = unit(0.12, "lines")
  ) +
  scale_color_manual(
    name   = NULL,
    values = c("Good Customers" = IMPRINT_PALETTE[1],
               "Bad Customers"  = IMPRINT_PALETTE[2])
  ) +
  scale_linetype_manual(
    name   = NULL,
    values = c("Good Customers" = "solid", "Bad Customers" = "longdash")
  ) +
  scale_y_continuous(
    labels = percent_format(accuracy = 1),
    limits = c(0, 1),
    expand = expansion(mult = c(0.01, 0.03))
  ) +
  scale_x_continuous(expand = expansion(mult = c(0.02, 0.06))) +
  labs(
    title = "ks-test-comparison · r · ggplot2 · anyplot.ai",
    x     = "Credit Score",
    y     = "Cumulative Proportion"
  ) +
  theme_minimal(base_size = 8) +
  theme(
    plot.background   = element_rect(fill = PAGE_BG,     color = PAGE_BG),
    panel.background  = element_rect(fill = PAGE_BG,     color = NA),
    panel.grid.major  = element_line(color = INK_SOFT,   linewidth = 0.2),
    panel.grid.minor  = element_blank(),
    panel.border      = element_blank(),
    axis.title        = element_text(color = INK,        size = 10),
    axis.text         = element_text(color = INK_SOFT,   size = 8),
    axis.line         = element_line(color = INK_SOFT,   linewidth = 0.4),
    plot.title        = element_text(color = INK,        size = 12,
                                     face = "bold",
                                     margin = margin(b = 10)),
    plot.margin       = margin(t = 16, r = 24, b = 12, l = 12),
    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT,
                                     linewidth = 0.3),
    legend.text       = element_text(color = INK_SOFT,   size = 8),
    legend.title      = element_text(color = INK,        size = 10),
    legend.position   = "bottom",
    legend.key.width  = unit(1.5, "cm")
  )

# Save
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 8,
  height   = 4.5,
  units    = "in",
  dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/ks-test-comparison/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "ks-test-comparison",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/ks-test-comparison/r/ggplot2",
  "hub": "https://anyplot.ai/ks-test-comparison",
  "code_json": "https://api.anyplot.ai/specs/ks-test-comparison/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/ks-test-comparison",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/ks-test-comparison/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/ks-test-comparison/r/ggplot2/plot-dark.png",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Kolmogorov-Smirnov Plot for Distribution Comparison on anyplot.ai.

Other implementations