ECDF 比较

两组的经验累积分布,并标出 Kolmogorov–Smirnov 最大差距。

ECDF 比较

经验累积分布函数(ECDF)对 x 轴上的每个值,绘出小于或等于该值的观测所占的比例。与直方图不同,它无需分箱,任何分位数都可以直接读出。将两条 ECDF 画在一起时,二者之间的最大垂直距离正是双样本 Kolmogorov–Smirnov 统计量 D,图中以红色线段表示。

数据为 20 世纪 70 年代从马里兰州州立监狱获释并被追踪一年的 432 名男性(Rossi、Berk 与 Lenihan 的实验),随 lifelines 提供(lifelines.datasets.load_rossi()):比较一年内再次被捕者与未被捕者获释时的年龄。scipy.stats.ecdf 构建曲线,scipy.stats.ks_2samp 返回 D、其所在位置及 p 值。

代码及图中文字均为英文。

ecdf-comparison.py · 42 行
下载 GitHub
import matplotlib.pyplot as plt
import numpy as np
from lifelines.datasets import load_rossi
from scipy import stats

# Data: age at release of former prisoners, split by rearrest within one year
df = load_rossi()
groups = {"Rearrested": df.loc[df.arrest == 1, "age"].to_numpy(),
          "Not rearrested": df.loc[df.arrest == 0, "age"].to_numpy()}
palette = ["#007AFF", "#FF9500"]

# Two-sample Kolmogorov–Smirnov test: D is the largest vertical gap
ks = stats.ks_2samp(*groups.values())
x_gap = ks.statistic_location
F = [stats.ecdf(d).cdf for d in groups.values()]
y_gap = [f.evaluate(x_gap) for f in F]

lo, hi = df.age.min() - 1, df.age.max() + 1  # common x-range for both curves
fig, ax = plt.subplots(figsize=(8, 5))
for (name, d), f, c in zip(groups.items(), F, palette):
    ax.step(np.r_[lo, f.quantiles, hi], np.r_[0, f.probabilities, 1], where="post",
            color=c, lw=2, label=f"{name} (n = {len(d)}, median {np.median(d):.0f} years)")

# Mark the KS distance
ax.plot([x_gap, x_gap], y_gap, color="#FF3B30", lw=2.5, solid_capstyle="round", zorder=5)
ax.scatter([x_gap, x_gap], y_gap, color="#FF3B30", s=30, zorder=6)
ax.annotate(f"D = {ks.statistic:.2f}\nat age {x_gap:.0f}", (x_gap + 0.2, min(y_gap)),
            xytext=(x_gap + 2, min(y_gap)), ha="left", va="center", fontsize=10,
            fontweight="semibold", color="#FF3B30",
            arrowprops=dict(arrowstyle="-", color="#FF3B30", lw=0.8, shrinkB=2))

ax.set_xlim(lo, hi)
ax.set_ylim(0, 1.02)
ax.set_yticks(np.linspace(0, 1, 5), [f"{v:.0%}" for v in np.linspace(0, 1, 5)])
ax.set_xlabel("Age at release (years)")
ax.set_ylabel("Cumulative share of men")
ax.legend(loc="lower right")
ax.set_title(f"Rearrested men are younger: {y_gap[0]:.0%} were {x_gap:.0f} or under "
             f"at release, vs {y_gap[1]:.0%}", pad=26)
ax.text(0, 1.02, f"Two-sample Kolmogorov–Smirnov D = {ks.statistic:.3f}, p = {ks.pvalue:.1e}"
        f" · n = {len(df)}", transform=ax.transAxes, fontsize=9.5, color="#6E6E73")
plt.show()

图表使用了可选的 PyStatlas 样式:添加 plt.style.use("pystatlas.mplstyle") 即可完全还原其外观。

数据来源

  • Rossi recidivism experiment (432 released prisoners). Rossi, P. H., Berk, R. A., & Lenihan, K. J. (1980). Money, Work, and Crime: Some Experimental Results. Academic Press. https://lifelines.readthedocs.io/en/latest/lifelines.datasets.html
    Men released from Maryland state prisons in the 1970s, half randomly assigned to financial aid. Distributed with lifelines (lifelines.datasets.load_rossi).

这些示例用于说明统计方法。图表标题描述的是该图在这些特定数据中呈现的内容——这些数据往往样本较小、年代较早或并不完整——而不是作者对实质问题的立场。关于解释、背景和数据质量,请以此处引用的原始来源为准。

如何引用

PyStatlas 由 Thierry Warin 创建并维护。如果它对你的工作有所帮助,敬请引用。

Warin, T. (2026). ECDF comparison. In PyStatlas: A Python Gallery of Statistical Graphics for the Social Sciences. https://warint.github.io/pystatlas/distributions/ecdf-comparison/

@misc{warin2026statlas,
  author       = {Warin, Thierry},
  title        = {PyStatlas: A Python Gallery of Statistical Graphics for the Social Sciences},
  year         = {2026},
  howpublished = {\url{https://warint.github.io/pystatlas/}},
  note         = {Source code: https://github.com/warint/pystatlas}
}

更多分布图表