コルモゴロフ・スミルノフ検定の理論と導出をわかりやすく解説

手元のデータが「ある特定の分布に従っているかどうか」を判定したい場面は数多くあります。たとえば、製造プロセスの出力が正規分布に従っているかを確認したり、シミュレーションで生成した乱数が意図した分布に従っているかを検証したりする場面です。

また、2つのデータセットが「同じ分布に従っているか」を調べたい場面もあります。異なるバッチの製品が同じ品質分布を持つか、A/Bテストの2群が同じ行動パターンを示すかといった問いです。

これらの問いに答える古典的かつ強力な手法がコルモゴロフ・スミルノフ検定(Kolmogorov-Smirnov test、以下KS検定)です。KS検定は経験分布関数(empirical distribution function, EDF)と理論的な分布関数の間の最大乖離を検定統計量として用い、分布全体の形状を評価します。

KS検定を理解すると、以下のような応用が可能です。

  • モデル検証: 統計モデルの仮定(正規性、指数性など)がデータに適合しているかを客観的に評価できます
  • シミュレーション検証: モンテカルロシミュレーションの乱数生成器が正しい分布を出力しているかの検証に使われます
  • 品質管理: 製品の特性値が仕様通りの分布に従っているかの検査に利用されます
  • 金融工学: リターンの分布が正規分布やt分布に従うかの仮定を検証する際に用いられます

本記事の内容

  • 経験分布関数の定義と性質
  • 一標本KS統計量の定義と帰無分布の導出
  • コルモゴロフ分布とグリベンコ・カンテリの定理
  • 二標本KS検定への拡張
  • Pythonによる実装と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

経験分布関数 — データから分布を推定する

累積分布関数の復習

確率変数 $X$ の累積分布関数(CDF)$F(x)$ は、$X$ が $x$ 以下になる確率を表します。

$$ F(x) = P(X \leq x) $$

CDFは $-\infty$ で0、$+\infty$ で1に近づく単調非減少関数です。CDFを知ることは分布を完全に特定することと同じです。

経験分布関数の定義

$n$ 個の観測値 $X_1, X_2, \dots, X_n$ が与えられたとき、経験分布関数(EDF)$F_n(x)$ は次のように定義されます。

$$ \begin{equation} F_n(x) = \frac{1}{n}\sum_{i=1}^{n}\mathbf{1}(X_i \leq x) \end{equation} $$

これは単に「$n$ 個のデータの中で $x$ 以下の値の割合」です。EDFは階段関数であり、データの各観測値で $\frac{1}{n}$ だけジャンプします。

イメージとしては、EDFは「データから直接読み取れるCDFの推定量」です。データが多くなるほど、EDFは真のCDFに近づきます。

グリベンコ・カンテリの定理

EDFが真のCDFに一様に収束することを保証するのが、グリベンコ・カンテリの定理(Glivenko-Cantelli theorem)です。

$$ \begin{equation} \sup_{x} |F_n(x) – F(x)| \xrightarrow{a.s.} 0 \quad (n \to \infty) \end{equation} $$

「$\sup_x$」はすべての $x$ にわたる最大値(上限)を意味します。つまり、EDFと真のCDFの最大のずれが、$n \to \infty$ で概ね確実にゼロに収束します。

この定理は「統計学の基本定理」と呼ばれることもあるほど重要な結果です。KS検定は、この $\sup_x |F_n(x) – F(x)|$ がゼロに近いかどうかを定量的に評価する検定です。

EDFの収束の速さを定量化するために、KS統計量の帰無分布を導出しましょう。

一標本KS検定

検定問題の設定

一標本KS検定は、データが特定の連続分布に従うかどうかを検定します。

$$ H_0: F = F_0 \quad \text{vs.} \quad H_1: F \neq F_0 $$

ここで $F$ はデータの真の分布、$F_0$ は帰無仮説で指定される分布(例: $N(0, 1)$)です。

KS統計量の定義

コルモゴロフ・スミルノフ統計量(KS statistic)$D_n$ は、EDFと帰無仮説の分布関数の間の最大乖離として定義されます。

$$ \begin{equation} D_n = \sup_{x} |F_n(x) – F_0(x)| \end{equation} $$

この統計量の直感的な意味は明確です。EDFと $F_0$ のグラフを重ねて描いたとき、2つの曲線の間の垂直距離が最大になる点を探し、その距離が $D_n$ です。

$D_n$ が小さければ、EDFが $F_0$ に近く、データは帰無仮説の分布に適合していると判断します。$D_n$ が大きければ、EDFが $F_0$ から大きく離れており、帰無仮説を棄却します。

片側KS統計量

両側の最大乖離 $D_n$ に加えて、片側のKS統計量も定義されます。

$$ D_n^+ = \sup_{x} [F_n(x) – F_0(x)], \quad D_n^- = \sup_{x} [F_0(x) – F_n(x)] $$

$D_n = \max(D_n^+, D_n^-)$ の関係があります。$D_n^+$ はEDFが $F_0$ の「上にずれている」最大量、$D_n^-$ は「下にずれている」最大量を測ります。

計算方法

$D_n$ の計算は、データを並べ替えることで効率的に行えます。データを昇順に $X_{(1)} \leq X_{(2)} \leq \cdots \leq X_{(n)}$ と並べると、

$$ D_n^+ = \max_{1 \leq i \leq n}\left[\frac{i}{n} – F_0(X_{(i)})\right] $$

$$ D_n^- = \max_{1 \leq i \leq n}\left[F_0(X_{(i)}) – \frac{i-1}{n}\right] $$

これは、EDFが $X_{(i)}$ でジャンプする性質を利用しています。$F_n(X_{(i)}) = \frac{i}{n}$(ジャンプ直後の値)であり、$F_n(X_{(i)}^-) = \frac{i-1}{n}$(ジャンプ直前の値)なので、EDFと $F_0$ の乖離は各ジャンプ点で評価すれば十分です。

ジャンプ点の間では $F_n$ は定数で $F_0$ は単調増加なので、乖離の最大値は必ずジャンプ点のいずれかで達成されます。

$D_n$ の帰無分布を次に見ていきましょう。

KS統計量の帰無分布 — コルモゴロフ分布

分布自由性

KS検定の最も注目すべき性質は、帰無仮説のもとでの $D_n$ の分布が帰無仮説の分布 $F_0$ に依存しないことです。つまり、$F_0$ が正規分布であっても指数分布であっても一様分布であっても、$D_n$ は同じ分布に従います。

この性質は確率積分変換(probability integral transform)から示されます。帰無仮説のもとで $U_i = F_0(X_i)$ と変換すると、$U_i \sim \text{Uniform}(0, 1)$ となります($F_0$ が連続分布の場合)。KS統計量は

$$ D_n = \sup_u |G_n(u) – u| $$

と書き換えられます。ここで $G_n(u)$ は $U_1, \dots, U_n$ のEDFです。この式は $F_0$ に依存しないため、$D_n$ の分布も $F_0$ に依存しません。

この分布自由性のおかげで、$D_n$ の帰無分布を一度求めておけば、どのような $F_0$ に対しても使えます。

有限標本での帰無分布

$D_n$ の正確な分布は、$[0, 1]^n$ 上の一様分布に基づく組合せ論的な計算で求められますが、閉じた形の式は複雑です。特に、小標本($n \leq 40$ 程度)では正確なp値の表が用意されています。

$n$ が小さい場合の棄却点の例:

$n$ $\alpha = 0.10$ $\alpha = 0.05$ $\alpha = 0.01$
5 0.509 0.565 0.669
10 0.369 0.410 0.490
20 0.265 0.294 0.356
50 0.170 0.188 0.230

$n$ が大きくなるほど棄却点が小さくなる(=より小さな乖離でも検出できる)ことがわかります。

漸近分布 — コルモゴロフ分布

$n \to \infty$ のとき、$\sqrt{n} D_n$ の分布はコルモゴロフ分布に収束します。これはコルモゴロフ(1933年)とスミルノフ(1948年)によって導かれた結果です。

$$ \begin{equation} P\left(\sqrt{n} D_n \leq t\right) \to K(t) = 1 – 2\sum_{j=1}^{\infty}(-1)^{j-1}e^{-2j^2 t^2} \end{equation} $$

この無限級数は非常に速く収束します。$j = 1$ の項だけでも良い近似が得られ、$j = 5$ まで取ればほぼ厳密な値になります。

$$ K(t) \approx 1 – 2e^{-2t^2} + 2e^{-8t^2} – 2e^{-18t^2} + \cdots $$

コルモゴロフ分布の生存関数 $1 – K(t)$ を使って、漸近的なp値は次のように計算されます。

$$ p = 1 – K(\sqrt{n} D_n) $$

ドンスカーの定理との関係

KS統計量の漸近分布は、ドンスカーの定理(Donsker’s theorem)、すなわち経験過程の不変原理から導かれます。

確率積分変換の後、帰無仮説のもとでの経験過程は次のように定義されます。

$$ \alpha_n(u) = \sqrt{n}\left[G_n(u) – u\right], \quad u \in [0, 1] $$

ドンスカーの定理によれば、この過程は $n \to \infty$ でブラウン橋(Brownian bridge)$B(u)$ に弱収束します。

$$ \alpha_n \Rightarrow B \quad \text{in } D[0, 1] $$

ブラウン橋は、$B(0) = B(1) = 0$ を満たすガウス過程です。KS統計量の漸近分布は、ブラウン橋の上限の分布に帰着します。

$$ \sqrt{n} D_n \xrightarrow{d} \sup_{0 \leq u \leq 1} |B(u)| $$

$\sup |B(u)|$ の分布がまさにコルモゴロフ分布です。

一標本KS検定の理論を理解したところで、二標本への拡張を見ましょう。

二標本KS検定

二標本問題の設定

二標本KS検定は、2つの独立な標本が同じ分布に従うかどうかを検定します。

$$ H_0: F_X = F_Y \quad \text{vs.} \quad H_1: F_X \neq F_Y $$

群1のデータ $X_1, \dots, X_{n_1}$ のEDFを $F_{n_1}(x)$、群2のデータ $Y_1, \dots, Y_{n_2}$ のEDFを $G_{n_2}(x)$ とします。

二標本KS統計量

$$ \begin{equation} D_{n_1, n_2} = \sup_x |F_{n_1}(x) – G_{n_2}(x)| \end{equation} $$

一標本の場合と同様に、2つのEDFの間の最大の垂直距離を測ります。

漸近分布

帰無仮説のもとで、$n_1, n_2 \to \infty$ のとき、

$$ \begin{equation} \sqrt{\frac{n_1 n_2}{n_1 + n_2}} D_{n_1, n_2} \xrightarrow{d} K \end{equation} $$

ここで $K$ は一標本の場合と同じコルモゴロフ分布です。スケーリング因子 $\sqrt{\frac{n_1 n_2}{n_1 + n_2}}$ は、2つの標本の有効サンプルサイズに対応しています。

一標本と二標本の比較

特徴 一標本KS検定 二標本KS検定
帰無仮説 $F = F_0$(理論分布と一致) $F_X = F_Y$(2つの分布が一致)
比較対象 EDF vs 理論CDF EDF vs EDF
統計量 $D_n = \sup\|F_n – F_0\|$ $D = \sup\|F_{n_1} – G_{n_2}\|$
分布自由性 $F_0$ が完全に指定されている場合のみ 常に成り立つ

重要な注意点として、一標本KS検定で $F_0$ のパラメータをデータから推定した場合(例: 平均と分散を推定した正規分布との比較)、KS統計量の帰無分布はコルモゴロフ分布ではなくなります。パラメータを推定する場合は、リリフォース検定(Lilliefors test)を使う必要があります。

次に、Pythonで実装して理論を検証しましょう。

Pythonによる実装

一標本KS検定のスクラッチ実装と可視化

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

def ks_test_one_sample(data, cdf_func):
    """
    一標本KS検定のスクラッチ実装

    Parameters
    ----------
    data : array-like  観測データ
    cdf_func : callable  帰無仮説のCDF

    Returns
    -------
    D : float  KS統計量
    D_plus, D_minus : float  片側統計量
    max_idx : int  最大乖離の位置
    """
    x = np.sort(data)
    n = len(x)

    # F_0の値
    F0 = cdf_func(x)

    # D+とD-
    D_plus_vals = np.arange(1, n + 1) / n - F0
    D_minus_vals = F0 - np.arange(0, n) / n

    D_plus = np.max(D_plus_vals)
    D_minus = np.max(D_minus_vals)
    D = max(D_plus, D_minus)

    if D_plus >= D_minus:
        max_idx = np.argmax(D_plus_vals)
    else:
        max_idx = np.argmax(D_minus_vals)

    return D, D_plus, D_minus, max_idx

# --- テストデータ ---
np.random.seed(42)
n = 30
data = np.random.normal(loc=0.3, scale=1.0, size=n)  # 少しずれた正規分布

# H0: N(0, 1) に従う
D, Dp, Dm, idx = ks_test_one_sample(data, stats.norm.cdf)

# SciPyとの比較
D_scipy, p_scipy = stats.kstest(data, "norm")

print(f"KS統計量:  D = {D:.4f} (SciPy: {D_scipy:.4f})")
print(f"D+ = {Dp:.4f}, D- = {Dm:.4f}")
print(f"p値 (SciPy): {p_scipy:.6f}")

# --- 可視化 ---
fig, ax = plt.subplots(figsize=(10, 6))

# ソートされたデータ
x_sorted = np.sort(data)
n = len(x_sorted)

# 経験分布関数(階段関数)
for i in range(n):
    x_left = x_sorted[i - 1] if i > 0 else x_sorted[0] - 1
    x_right = x_sorted[i]
    ax.plot([x_left, x_right], [i / n, i / n], "b-", linewidth=1.5)
    ax.plot([x_right, x_right], [i / n, (i + 1) / n], "b-", linewidth=1.5)
# 最後のステップ
ax.plot([x_sorted[-1], x_sorted[-1] + 1.5], [1, 1], "b-", linewidth=1.5,
        label=r"$F_n(x)$ (EDF)")

# 理論分布
x_theo = np.linspace(x_sorted[0] - 1, x_sorted[-1] + 1.5, 500)
ax.plot(x_theo, stats.norm.cdf(x_theo), "r-", linewidth=2,
        label=r"$F_0(x) = \Phi(x)$ (Standard Normal)")

# 最大乖離の表示
x_max = x_sorted[idx]
F_n_val = (idx + 1) / n if Dp >= Dm else idx / n
F_0_val = stats.norm.cdf(x_max)
ax.annotate("", xy=(x_max, F_n_val), xytext=(x_max, F_0_val),
            arrowprops=dict(arrowstyle="<->", color="green", lw=2))
ax.text(x_max + 0.15, (F_n_val + F_0_val) / 2, f"D = {D:.3f}",
        fontsize=12, color="green", fontweight="bold")

ax.set_xlabel("x", fontsize=12)
ax.set_ylabel("Cumulative probability", fontsize=12)
ax.set_title(f"KS test: EDF vs Standard Normal (n={n})", fontsize=13)
ax.legend(fontsize=11, loc="upper left")
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("ks_test_one_sample.png", dpi=150, bbox_inches="tight")
plt.show()

上のグラフから、以下のことが読み取れます。

  1. 青い階段関数がEDF、赤い滑らかな曲線が標準正規分布のCDFです。EDFはデータの各観測値で $1/n$ だけジャンプする離散的な関数であり、$n = 30$ では階段の形が明確に見えます

  2. 緑の矢印がKS統計量 $D$ を示している。EDFと理論CDFの間の垂直距離が最大になる点で測定されています。この距離が大きいほど、データと理論分布の乖離が大きいことを意味します

  3. EDFが理論CDFの右側にずれている傾向がある。データは $N(0.3, 1)$ から生成されているため、$N(0, 1)$ と比較すると右にシフトしており、EDFが理論CDFよりも左側で低く、右側で高くなっています

コルモゴロフ分布の検証

帰無仮説のもとでシミュレーションを行い、$\sqrt{n} D_n$ の分布がコルモゴロフ分布に従うことを確認します。

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

np.random.seed(42)

def kolmogorov_cdf(t, n_terms=100):
    """コルモゴロフ分布のCDF"""
    result = 0.0
    for j in range(1, n_terms + 1):
        result += (-1)**(j - 1) * np.exp(-2 * j**2 * t**2)
    return 1 - 2 * result

n_list = [10, 30, 100, 500]
n_sim = 10000

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

for ax, n in zip(axes.flat, n_list):
    # シミュレーション: 帰無仮説のもとでKS統計量を計算
    D_values = np.zeros(n_sim)
    for i in range(n_sim):
        data = np.random.uniform(0, 1, n)  # U(0,1)から生成
        data_sorted = np.sort(data)
        F0 = data_sorted  # U(0,1)のCDF = x
        D_plus = np.max(np.arange(1, n + 1) / n - F0)
        D_minus = np.max(F0 - np.arange(0, n) / n)
        D_values[i] = max(D_plus, D_minus)

    # sqrt(n) * D の分布
    scaled_D = np.sqrt(n) * D_values

    ax.hist(scaled_D, bins=50, density=True, alpha=0.6, color="steelblue",
            edgecolor="white", label=f"Simulation (n={n})")

    # コルモゴロフ分布の密度関数(数値微分で近似)
    t = np.linspace(0.2, 2.5, 200)
    K_cdf = np.array([kolmogorov_cdf(ti) for ti in t])
    K_pdf = np.gradient(K_cdf, t)
    ax.plot(t, K_pdf, "r-", linewidth=2, label="Kolmogorov dist.")

    ax.set_xlabel(r"$\sqrt{n} \cdot D_n$", fontsize=11)
    ax.set_ylabel("Density", fontsize=11)
    ax.set_title(f"n = {n}", fontsize=12)
    ax.legend(fontsize=9)
    ax.grid(True, alpha=0.3)
    ax.set_xlim(0, 2.5)

plt.suptitle(r"Convergence of $\sqrt{n}D_n$ to Kolmogorov distribution",
             fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig("ks_kolmogorov_convergence.png", dpi=150, bbox_inches="tight")
plt.show()

この収束の可視化から、以下のことが確認できます。

  1. $n = 10$ でもすでにコルモゴロフ分布への近似はかなり良い。青いヒストグラム(シミュレーション)と赤い曲線(コルモゴロフ分布の理論的な密度)が概ね一致しています。これは、KS統計量の漸近近似が比較的小さな $n$ から有効であることを示しています

  2. $n$ が増えるにつれて近似精度が向上する。$n = 100$ では実質的に完全な一致が見られ、$n = 500$ ではヒストグラムと理論曲線が区別できないほどです

  3. コルモゴロフ分布は右に裾の長い非対称な分布です。ピークは $\sqrt{n} D_n \approx 0.7$ 付近にあり、右側に長い裾を持ちます。有意水準 $5\%$ の臨界値は $\sqrt{n} D_n \approx 1.36$ です

二標本KS検定の実装

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

np.random.seed(42)

# --- 2つの標本を生成 ---
n1, n2 = 50, 60
sample1 = np.random.normal(0, 1, n1)         # N(0, 1)
sample2 = np.random.normal(0.5, 1.2, n2)     # N(0.5, 1.2)

# SciPyでの二標本KS検定
D_stat, p_val = stats.ks_2samp(sample1, sample2)

# --- 可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))

# EDF の比較
ax = axes[0]
x_all = np.sort(np.concatenate([sample1, sample2]))
x_plot = np.concatenate([[-4], x_all, [5]])

# 各標本のEDF
edf1 = np.array([np.mean(sample1 <= x) for x in x_plot])
edf2 = np.array([np.mean(sample2 <= x) for x in x_plot])

ax.step(x_plot, edf1, where="post", linewidth=2, color="blue",
        label=f"Sample 1 (n={n1})")
ax.step(x_plot, edf2, where="post", linewidth=2, color="orange",
        label=f"Sample 2 (n={n2})")

# 最大乖離の表示
diff = np.abs(edf1 - edf2)
max_idx = np.argmax(diff)
x_max = x_plot[max_idx]
ax.annotate("", xy=(x_max, edf1[max_idx]),
            xytext=(x_max, edf2[max_idx]),
            arrowprops=dict(arrowstyle="<->", color="green", lw=2))
ax.text(x_max + 0.15, (edf1[max_idx] + edf2[max_idx]) / 2,
        f"D = {D_stat:.3f}", fontsize=11, color="green", fontweight="bold")

ax.set_xlabel("x", fontsize=12)
ax.set_ylabel("Cumulative probability", fontsize=12)
ax.set_title(f"Two-sample KS test (p = {p_val:.4f})", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

# ヒストグラム
ax = axes[1]
bins = np.linspace(-4, 5, 30)
ax.hist(sample1, bins=bins, density=True, alpha=0.5, color="blue",
        edgecolor="white", label=f"Sample 1: N(0, 1)")
ax.hist(sample2, bins=bins, density=True, alpha=0.5, color="orange",
        edgecolor="white", label=f"Sample 2: N(0.5, 1.2)")
ax.set_xlabel("x", fontsize=12)
ax.set_ylabel("Density", fontsize=12)
ax.set_title("Data distributions", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("ks_test_two_sample.png", dpi=150, bbox_inches="tight")
plt.show()

print(f"二標本KS統計量: D = {D_stat:.4f}")
print(f"p値: {p_val:.6f}")

この可視化から、以下のことが確認できます。

  1. 左図のEDFの比較が二標本KS検定の本質を示している。2つのEDFの間の最大の垂直距離(緑の矢印)がKS統計量 $D$ です。この距離が大きいほど、2つの標本が異なる分布から来ている証拠が強くなります

  2. 右図のヒストグラムは2群の分布の違いを示している。標本2は標本1に比べて右にシフトしており、やや幅が広いことがわかります。KS検定はこのような位置とスケールの両方の違いを同時に検出できます

  3. p値が小さい場合、帰無仮説(2群の分布が同一)を棄却する。この例では平均と分散の両方が異なるため、KS検定は差を検出しやすくなっています

KS検定の検出力特性

KS検定がどのような分布の違いを検出しやすいかをシミュレーションで調べます。

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

np.random.seed(42)

n = 50
n_sim = 3000
alpha = 0.05

# 3つのタイプの対立仮説
# (1) 位置の差、(2) スケールの差、(3) 形状の差
shift_vals = np.linspace(0, 1.5, 12)

power_ks_shift = []
power_t_shift = []
power_ks_scale = []
power_ks_shape = []

for delta in shift_vals:
    rej_ks_s, rej_t_s, rej_ks_sc, rej_ks_sh = 0, 0, 0, 0

    for _ in range(n_sim):
        x_base = np.random.randn(n)

        # (1) 位置シフト
        y_shift = np.random.randn(n) + delta
        _, p_ks = stats.ks_2samp(x_base, y_shift)
        _, p_t = stats.ttest_ind(x_base, y_shift)
        if p_ks < alpha: rej_ks_s += 1
        if p_t < alpha: rej_t_s += 1

        # (2) スケール変化
        y_scale = np.random.randn(n) * (1 + delta)
        _, p_ks = stats.ks_2samp(x_base, y_scale)
        if p_ks < alpha: rej_ks_sc += 1

        # (3) 形状変化(混合)
        y_shape = np.where(
            np.random.rand(n) < 0.5 + delta / 3,
            np.random.randn(n),
            np.random.randn(n) + 3
        )
        _, p_ks = stats.ks_2samp(x_base, y_shape)
        if p_ks < alpha: rej_ks_sh += 1

    power_ks_shift.append(rej_ks_s / n_sim)
    power_t_shift.append(rej_t_s / n_sim)
    power_ks_scale.append(rej_ks_sc / n_sim)
    power_ks_shape.append(rej_ks_sh / n_sim)

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

ax = axes[0]
ax.plot(shift_vals, power_ks_shift, "b-o", markersize=5, label="KS test")
ax.plot(shift_vals, power_t_shift, "r-s", markersize=5, label="t-test")
ax.axhline(alpha, color="gray", linestyle="--", linewidth=0.8)
ax.set_title("Location shift", fontsize=13)
ax.set_xlabel("Shift amount", fontsize=11)
ax.set_ylabel("Power", fontsize=11)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)

ax = axes[1]
ax.plot(shift_vals, power_ks_scale, "b-o", markersize=5, label="KS test")
ax.axhline(alpha, color="gray", linestyle="--", linewidth=0.8)
ax.set_title("Scale change", fontsize=13)
ax.set_xlabel("Scale increase", fontsize=11)
ax.set_ylabel("Power", fontsize=11)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)

ax = axes[2]
ax.plot(shift_vals, power_ks_shape, "b-o", markersize=5, label="KS test")
ax.axhline(alpha, color="gray", linestyle="--", linewidth=0.8)
ax.set_title("Shape change (mixture)", fontsize=13)
ax.set_xlabel("Mixture proportion change", fontsize=11)
ax.set_ylabel("Power", fontsize=11)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)

plt.tight_layout()
plt.savefig("ks_test_power.png", dpi=150, bbox_inches="tight")
plt.show()

この検出力シミュレーションから、以下の重要な知見が得られます。

  1. 位置シフト(左図)では、KS検定はt検定よりも検出力が低い。位置の差のみを検出する場合、t検定は平均の差に特化しているため効率的です。KS検定は分布全体の形状を見るため、位置の差だけでなくスケールや形状の差も含めて検出しますが、特定の種類の差に対する検出力は劣ります

  2. スケール変化(中央図)では、KS検定は適度な検出力を持つ。分散の違いは分布のCDFの形状を変えるため、KS検定で検出可能です。ただし、分散の差に特化したルビーン検定やバートレット検定の方が検出力は高くなります

  3. 形状の変化(右図)でも、KS検定は差を検出できる。混合分布の混合比率が変化すると、CDFの形状が変わります。KS検定はこのような「全体的な分布の違い」を検出する汎用性を持っています

KS検定の注意点と限界

離散分布への適用

KS検定は連続分布を前提としています。離散分布(ポアソン分布、二項分布など)に適用すると、KS統計量の帰無分布が変わるため、p値が保守的になります(実際の有意水準が名目水準よりも小さくなる)。離散データに対しては、$\chi^2$ 適合度検定の方が適切です。

パラメータ推定の問題

一標本KS検定で $F_0$ のパラメータをデータから推定した場合、標準的なKS検定の臨界値はもはや正しくありません。例えば、「データが正規分布に従うか」を検定する際に、平均と分散をデータから推定すると、KS統計量は標準的な帰無分布よりも小さくなる傾向があり、検定が保守的になります。

この場合は、リリフォース検定(Lilliefors test)を使うか、ブートストラップ法でp値を推定する必要があります。

感度の特徴

KS検定は分布の中央付近の乖離に最も敏感であり、裾の部分の乖離には感度が低いという特徴があります。裾の違いに敏感な検定としては、アンダーソン・ダーリング検定(Anderson-Darling test)が推奨されます。

まとめ

本記事では、コルモゴロフ・スミルノフ検定の理論を経験分布関数から導出し、実装まで解説しました。

  • KS統計量はEDFと帰無仮説の分布関数の間の最大垂直距離 $D_n = \sup_x |F_n(x) – F_0(x)|$ であり、分布全体の適合度を評価します
  • 分布自由性: 帰無仮説のもとでの $D_n$ の分布は $F_0$ の形に依存しません。これは確率積分変換の結果です
  • 漸近分布: $\sqrt{n} D_n$ はコルモゴロフ分布に収束し、$n \geq 30$ 程度で良い近似が得られます
  • 二標本KS検定は2つのEDFの最大乖離を見る自然な拡張です
  • 検出力の特徴: KS検定は分布全体の差を検出する汎用的な検定ですが、位置シフトのみの場合はt検定に劣り、裾の差にはアンダーソン・ダーリング検定に劣ります

次のステップとして、以下の記事も参考にしてください。