適合度検定と独立性検定 — カイ二乗検定の2つの顔をわかりやすく解説

あなたの手元に1個のサイコロがあります。120回振ったところ、1の目が25回、3の目が15回出ました。このサイコロは公平でしょうか? あるいは、ある病院で喫煙者と非喫煙者の肺がん発症率を調べたとき、喫煙と肺がんに関連があると言い切れるでしょうか?

この2つの問いは、一見まったく異なる問題に見えます。前者は「データが特定の分布に従うか」を問い、後者は「2つの変数に関連があるか」を問うています。しかし、驚くべきことに、この2つの問いに答える統計的手法は同じ枠組みで記述できます。それがカイ二乗検定($\chi^2$ 検定)です。

カイ二乗検定は統計学の中でも最も汎用性が高い検定手法の一つであり、以下のような幅広い応用を持ちます。

  • 品質管理: 製造ラインで不良品の発生パターンが理論モデルに従うかを検証する場面で、適合度検定が使われます
  • 医学研究: 治療法と回復率の関連を調べるクリニカルトライアルで、独立性検定が使われます
  • マーケティング: 年代別の購買行動パターンに違いがあるかを分析する際にも、分割表の独立性検定が活躍します
  • 遺伝学: メンデルの法則の検証(表現型の出現比率が3:1に従うか)は、適合度検定の古典的な応用です

本記事では、カイ二乗検定の2つの顔適合度検定独立性検定 — を統一的に理解することを目指します。両者がなぜ同じ $\chi^2$ 統計量を共有するのか、その数学的構造を丁寧に導出し、Pythonでの実装を通じて体感します。

本記事の内容

  • カイ二乗検定の統一的な枠組み — 「期待と観測のずれ」を測る思想
  • 適合度検定の理論と検定統計量の導出
  • 適合度検定のPython実装(サイコロの公平性検証)
  • 独立性検定の理論(分割表、期待度数の計算)
  • 独立性検定の自由度の決め方と導出
  • 独立性検定のPython実装(2×2表、3×3表の具体例)
  • 2つの検定の統一的理解
  • 注意点(期待度数が小さい場合、イェーツの補正、フィッシャーの正確検定)

前提知識

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

カイ二乗検定の統一的な枠組み

「期待と観測のずれ」を測るという思想

カイ二乗検定の核心にあるアイデアは、驚くほどシンプルです。それは「もし仮説が正しければ、こうなるはず」という期待値と、「実際にはこうだった」という観測値のずれを数値化するということです。

日常的な例で考えてみましょう。あなたがコイン投げを100回行い、表が60回、裏が40回出たとします。公平なコインであれば、表も裏も50回ずつ出るのが「期待」です。表が60回出たのは、「期待」からのずれが10回分あったことを意味します。しかし、このずれが「偶然の範囲」なのか「コインに偏りがある証拠」なのかを判断するには、ずれの大きさを適切な尺度で測る必要があります。

カイ二乗検定では、このずれを次の統計量で測定します。

$$ \chi^2 = \sum_{\text{全カテゴリ}} \frac{(\text{観測度数} – \text{期待度数})^2}{\text{期待度数}} $$

この式の構造を直感的に理解しましょう。分子 $(\text{観測度数} – \text{期待度数})^2$ は「ずれの二乗」です。二乗することで、正のずれも負のずれも等しく評価できます。分母の $\text{期待度数}$ で割るのは、ずれの相対的な大きさを評価するためです。たとえば、期待度数が1000のカテゴリで10のずれが生じるのと、期待度数が10のカテゴリで10のずれが生じるのでは、後者のほうがはるかに深刻です。分母で割ることで、この違いを適切に反映できるのです。

一般的な公式

記号を整理しましょう。カテゴリが全部で $m$ 個あるとき、各カテゴリ $i$ の観測度数を $O_i$、帰無仮説のもとでの期待度数を $E_i$ として、カイ二乗統計量は次のように定義されます。

$$ \chi^2 = \sum_{i=1}^{m} \frac{(O_i – E_i)^2}{E_i} $$

ここで重要なのは、$O_i$ と $E_i$ の定め方が適合度検定と独立性検定で異なるという点です。しかし、統計量の形そのものは全く同じです。つまり、カイ二乗検定の枠組みとは「期待度数の計算方法」と「自由度の決め方」だけが場面ごとに変わる、共通のフレームワークなのです。

なぜカイ二乗分布に従うのか — 直感的な説明

$\chi^2$ 統計量が $\chi^2$ 分布に従う理由を直感的に理解しておきましょう。各カテゴリの観測度数 $O_i$ は、$n$ 個のデータが多項分布に従って振り分けられた結果です。$n$ が十分に大きいとき、中心極限定理により $O_i$ は正規分布で近似できます。具体的には、

$$ \frac{O_i – E_i}{\sqrt{E_i}} \approx N(0, 1) $$

が成り立ちます(正確にはこの近似は $O_i$ 間の共分散を無視しており、制約条件を考慮する必要がありますが、大枠の直感としてはこれで十分です)。

$\chi^2$ 分布は「独立な標準正規変数の二乗和」として定義されるのでした。上の近似を二乗して足し合わせたものが $\chi^2$ 統計量ですから、帰無仮説のもとで $\chi^2$ 分布に近似的に従うのは自然な帰結です。ただし、$\sum O_i = n$ という制約があるために、自由に動ける変量の数(自由度)はカテゴリ数よりも少なくなります。

この共通の基盤を頭に入れた上で、まずは適合度検定の詳細な理論を見ていきましょう。

適合度検定の理論と導出

問題設定

適合度検定(goodness-of-fit test)は、「観測データが特定の理論分布に従うか」を検定する手法です。

$k$ 個のカテゴリがあり、理論的な確率が $p_1, p_2, \dots, p_k$(ただし $\sum_{i=1}^{k} p_i = 1$)として与えられているとします。$n$ 回の独立な試行から得られた各カテゴリの観測度数を $O_1, O_2, \dots, O_k$ とします。

帰無仮説と対立仮説は次のように設定します。

$$ H_0: \text{データは確率 } p_1, p_2, \dots, p_k \text{ に従う} $$

$$ H_1: \text{少なくとも1つの } i \text{ について、真の確率は } p_i \text{ と異なる} $$

期待度数の計算

帰無仮説 $H_0$ が正しいならば、カテゴリ $i$ に振り分けられるデータ数の期待値は次のようになります。

$$ E_i = n \cdot p_i $$

たとえば、公平なサイコロを120回振る場合、各目の期待度数は $E_i = 120 \times \frac{1}{6} = 20$ です。

検定統計量の導出

ここからが理論の核心です。$\chi^2$ 統計量がなぜカイ二乗分布に従うのかを、多項分布の正規近似から導出します。

$n$ 個のデータが $k$ 個のカテゴリに独立に振り分けられるとき、観測度数ベクトル $(O_1, O_2, \dots, O_k)$ は多項分布 $\text{Multi}(n; p_1, \dots, p_k)$ に従います。多項分布の性質から、各 $O_i$ の期待値と分散は次の通りです。

$$ E[O_i] = np_i = E_i, \quad \text{Var}(O_i) = np_i(1 – p_i) = E_i(1 – p_i) $$

ここで、2つの異なるカテゴリの共分散も重要です。

$$ \text{Cov}(O_i, O_j) = -np_ip_j \quad (i \neq j) $$

この負の共分散は直感的に理解できます。全体の個数が $n$ に固定されているため、あるカテゴリの度数が増えれば、他のカテゴリの度数は減らざるを得ないからです。

$n$ が十分に大きいとき、中心極限定理の多次元版により、標準化した度数ベクトルは多次元正規分布で近似できます。具体的には、$Z_i = \frac{O_i – E_i}{\sqrt{E_i}}$ とすると、ベクトル $\bm{Z} = (Z_1, \dots, Z_k)^T$ の共分散行列は次のようになります。

$$ \text{Cov}(Z_i, Z_j) = \frac{\text{Cov}(O_i, O_j)}{\sqrt{E_i}\sqrt{E_j}} $$

$i = j$ のとき(分散)を計算すると、

$$ \text{Var}(Z_i) = \frac{E_i(1 – p_i)}{E_i} = 1 – p_i $$

$i \neq j$ のとき(共分散)を計算すると、

$$ \text{Cov}(Z_i, Z_j) = \frac{-np_ip_j}{\sqrt{np_i}\sqrt{np_j}} = -\sqrt{p_ip_j} $$

ここでカイ二乗統計量 $\chi^2 = \sum_{i=1}^{k} Z_i^2$ を考えます。$Z_i$ は独立ではなく(共分散がゼロでない)、分散も1ではない($1 – p_i$)ため、単純に「$k$ 個の標準正規変数の二乗和」とは言えません。

しかし、制約条件 $\sum_{i=1}^{k} O_i = n$(すなわち $\sum_{i=1}^{k} \sqrt{p_i} Z_i = 0$)のもとで、二次形式の理論(具体的にはコクランの定理)を適用すると、次の結論が得られます。

$$ \chi^2 = \sum_{i=1}^{k} \frac{(O_i – E_i)^2}{E_i} \xrightarrow{d} \chi^2_{k-1} \quad (n \to \infty) $$

自由度が $k$ ではなく $k – 1$ になる理由は、制約条件 $\sum_{i=1}^{k} O_i = n$ が1つあるため、$k$ 個の変量のうち自由に動けるのが $k – 1$ 個だからです。$k – 1$ 個の値が決まれば、残りの1つは $n$ からの引き算で自動的に定まります。

棄却域と判定

$\chi^2$ 統計量が大きいほど、観測と期待のずれが大きいことを意味します。したがって、カイ二乗検定は常に右片側検定です。有意水準 $\alpha$ での棄却域は次のようになります。

$$ \chi^2 > \chi^2_{\alpha,\, k-1} $$

ここで $\chi^2_{\alpha,\, k-1}$ は、自由度 $k – 1$ の $\chi^2$ 分布の上側 $\alpha$ 点です。等価的に、p値が $\alpha$ 未満であれば帰無仮説を棄却します。

$$ p = P(\chi^2_{k-1} \geq \chi^2_{\text{obs}}) $$

検定統計量の導出が完了したので、次は具体的な数値例でこの理論を実感してみましょう。

適合度検定のPython実装 — サイコロの公平性を検証する

手計算による検定

サイコロを120回振った結果が次の通りだったとします。

1 2 3 4 5 6
観測度数 $O_i$ 25 17 15 23 22 18
期待度数 $E_i$ 20 20 20 20 20 20

公平なサイコロ(各目の確率 $\frac{1}{6}$)を帰無仮説とすると、期待度数は全て $E_i = 120 \times \frac{1}{6} = 20$ です。検定統計量を計算します。

各カテゴリの寄与を一つずつ計算すると、

$$ \frac{(25-20)^2}{20} = \frac{25}{20} = 1.25, \quad \frac{(17-20)^2}{20} = \frac{9}{20} = 0.45 $$

同様に残りのカテゴリについても計算を進めると、

$$ \frac{(15-20)^2}{20} = 1.25, \quad \frac{(23-20)^2}{20} = 0.45, \quad \frac{(22-20)^2}{20} = 0.20, \quad \frac{(18-20)^2}{20} = 0.20 $$

全てを足し合わせると、

$$ \chi^2 = 1.25 + 0.45 + 1.25 + 0.45 + 0.20 + 0.20 = 3.80 $$

自由度は $k – 1 = 6 – 1 = 5$ です。有意水準 $\alpha = 0.05$ の棄却点は $\chi^2_{0.05, 5} = 11.07$ なので、$3.80 < 11.07$ となり、帰無仮説は棄却されません。

Pythonでの実装

上の計算をPythonで実行し、さらにモンテカルロ・シミュレーションで検定統計量の分布を確認してみましょう。

import numpy as np
from scipy import stats

# サイコロの観測データ
observed = np.array([25, 17, 15, 23, 22, 18])
n = observed.sum()  # 120

# 帰無仮説: 公平なサイコロ(各目の確率 1/6)
expected_prob = np.array([1/6] * 6)
expected = n * expected_prob

# カイ二乗統計量を手計算
chi2_stat = np.sum((observed - expected)**2 / expected)
df = len(observed) - 1  # 自由度 = k - 1

# p値を計算
p_value = 1 - stats.chi2.cdf(chi2_stat, df)

print("=== 適合度検定(手計算)===")
print(f"観測度数: {observed}")
print(f"期待度数: {expected}")
print(f"各カテゴリの寄与: {(observed - expected)**2 / expected}")
print(f"χ² = {chi2_stat:.4f}")
print(f"自由度: {df}")
print(f"p値: {p_value:.4f}")
print(f"判定(α=0.05): {'帰無仮説を棄却' if p_value < 0.05 else '帰無仮説を棄却しない'}")

print("\n=== scipy.stats.chisquare による検証 ===")
chi2_scipy, p_scipy = stats.chisquare(observed, f_exp=expected)
print(f"χ² = {chi2_scipy:.4f}, p値 = {p_scipy:.4f}")

手計算の結果と scipy.stats.chisquare の結果が一致していることが確認できます。$\chi^2 = 3.80$、p値は約0.578です。p値が0.05よりも十分に大きいため、このサイコロが公平でないという証拠は得られません。各カテゴリの寄与を見ると、1の目と3の目のずれ(いずれも寄与1.25)が最も大きいですが、それでも全体として統計的に有意なずれには達していません。

モンテカルロ・シミュレーションによる検証

理論的に $\chi^2$ 統計量が自由度5のカイ二乗分布に従うことを、シミュレーションで確認します。

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

np.random.seed(42)
n_sim = 50000  # シミュレーション回数
n_rolls = 120  # サイコロを振る回数
k = 6  # カテゴリ数

# 公平なサイコロを n_rolls 回振るシミュレーションを n_sim 回繰り返す
chi2_values = np.zeros(n_sim)
for i in range(n_sim):
    # 多項分布からサンプリング
    obs = np.random.multinomial(n_rolls, [1/6]*6)
    exp = np.array([n_rolls / 6] * 6)
    chi2_values[i] = np.sum((obs - exp)**2 / exp)

# 理論的なχ²分布と比較
df = k - 1
x = np.linspace(0, 25, 500)
theoretical_pdf = stats.chi2.pdf(x, df)

fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(chi2_values, bins=80, density=True, alpha=0.6,
        color='steelblue', edgecolor='black', label='Simulation')
ax.plot(x, theoretical_pdf, 'r-', linewidth=2.5,
        label=f'$\\chi^2_{{{df}}}$ (theoretical)')
ax.axvline(3.80, color='green', linewidth=2, linestyle='--',
           label=f'Observed $\\chi^2$ = 3.80')
ax.set_xlabel('$\\chi^2$', fontsize=13)
ax.set_ylabel('Density', fontsize=13)
ax.set_title('Monte Carlo Verification of $\\chi^2$ Distribution (Goodness-of-Fit)', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

シミュレーションのヒストグラムが理論的な $\chi^2_5$ 分布の曲線とほぼ完全に重なっていることが確認できます。これは、帰無仮説が正しいときに $\chi^2$ 統計量が確かに自由度5のカイ二乗分布に従うことの実証です。また、観測値 $\chi^2 = 3.80$ の位置(緑の破線)は分布の中央付近にあり、「帰無仮説のもとで十分に起こりうる値」であることが視覚的にも理解できます。

適合度検定の理論と実装が理解できたところで、次はもう一つの顔である独立性検定に進みましょう。一見別の問題に見えますが、数学的な構造が驚くほど似ていることが明らかになります。

独立性検定の理論 — 分割表と期待度数

問題設定

独立性検定(test of independence)は、「2つのカテゴリ変数が独立か、それとも関連があるか」を検定する手法です。

たとえば、ある調査で被験者の「喫煙習慣」(喫煙者/非喫煙者)と「肺がんの有無」(あり/なし)を同時に記録したとします。この2つの変数に関連があるのか、それとも統計的に独立なのかを判断したいという問題です。

2つのカテゴリ変数のデータを整理するために、分割表(contingency table、クロス集計表)を使います。一方の変数が $r$ 個のカテゴリ、もう一方の変数が $c$ 個のカテゴリを持つとき、$r \times c$ の分割表が作られます。

一般的な $r \times c$ 分割表は次のような形になります。

列1 列2 $\cdots$ 列$c$ 行計
行1 $O_{11}$ $O_{12}$ $\cdots$ $O_{1c}$ $R_1$
行2 $O_{21}$ $O_{22}$ $\cdots$ $O_{2c}$ $R_2$
$\vdots$ $\vdots$ $\vdots$ $\ddots$ $\vdots$ $\vdots$
行$r$ $O_{r1}$ $O_{r2}$ $\cdots$ $O_{rc}$ $R_r$
列計 $C_1$ $C_2$ $\cdots$ $C_c$ $n$

ここで $O_{ij}$ はセル $(i, j)$ の観測度数、$R_i = \sum_{j=1}^{c} O_{ij}$ は行 $i$ の合計(行周辺度数)、$C_j = \sum_{i=1}^{r} O_{ij}$ は列 $j$ の合計(列周辺度数)、$n = \sum_{i}\sum_{j} O_{ij}$ は全体の合計です。

帰無仮説と対立仮説は次のように設定します。

$$ H_0: \text{行変数と列変数は独立である} $$

$$ H_1: \text{行変数と列変数は独立でない(関連がある)} $$

独立の意味と期待度数

「2つの変数が独立」とは、確率論の言葉で次のように表現できます。

$$ P(\text{行} = i, \text{列} = j) = P(\text{行} = i) \times P(\text{列} = j) $$

つまり、ある個体が行カテゴリ $i$ に属する確率と列カテゴリ $j$ に属する確率が互いに影響し合わないということです。

これを具体的に考えてみましょう。喫煙と肺がんが独立であるならば、「喫煙者である確率」と「肺がんになる確率」は無関係で、喫煙者の肺がん率と非喫煙者の肺がん率は同じはずです。

帰無仮説のもとで、各セルの期待度数はどう計算するのでしょうか。行 $i$ に属する確率は $\hat{P}(\text{行} = i) = R_i / n$、列 $j$ に属する確率は $\hat{P}(\text{列} = j) = C_j / n$ と推定できます。独立の仮定から、セル $(i, j)$ に入る確率は次のようになります。

$$ \hat{P}(\text{行} = i, \text{列} = j) = \frac{R_i}{n} \times \frac{C_j}{n} $$

$n$ 個のデータがこの確率で振り分けられるので、期待度数は次の式で計算されます。

$$ E_{ij} = n \times \frac{R_i}{n} \times \frac{C_j}{n} = \frac{R_i \times C_j}{n} $$

この式は非常にシンプルで覚えやすいです。期待度数 = 行計 × 列計 / 全体計。独立であれば各セルの度数はこの値になるはずなので、実際の観測度数がこの値から大きくずれていれば、2つの変数に関連がある証拠となります。

検定統計量

期待度数が求まれば、あとは適合度検定と全く同じ形の統計量を使います。

$$ \chi^2 = \sum_{i=1}^{r}\sum_{j=1}^{c} \frac{(O_{ij} – E_{ij})^2}{E_{ij}} $$

この二重の総和は、分割表の全セルにわたって「観測と期待のずれ」を足し合わせていることを意味します。形式的には、$r \times c$ 個のセルを1次元に並べたカテゴリの適合度検定と見なすこともできます。

期待度数の計算が適合度検定と異なるだけで、統計量の形は同じ — これこそが「カイ二乗検定の統一的な枠組み」の本質です。次は、独立性検定に固有の問題として自由度の決め方を詳しく見ていきましょう。

独立性検定の導出 — 自由度の決め方

自由度はなぜ $(r-1)(c-1)$ になるのか

独立性検定における自由度は $(r-1)(c-1)$ です。この値がどこから来るのかを2つの方法で理解しましょう。

方法1: 制約条件の数を数える

分割表には $r \times c$ 個のセルがあります。しかし、以下の制約条件が存在します。

まず、行ごとの合計が固定されているという制約があります。

$$ \sum_{j=1}^{c} O_{ij} = R_i \quad (i = 1, 2, \dots, r) $$

これは $r$ 個の制約です。同様に、列ごとの合計が固定されているという制約があります。

$$ \sum_{i=1}^{r} O_{ij} = C_j \quad (j = 1, 2, \dots, c) $$

これは $c$ 個の制約です。ただし、全体の合計 $n = \sum R_i = \sum C_j$ という関係式により、行の制約と列の制約のうち1つは冗長です。したがって独立な制約の数は $r + c – 1$ です。

さらに、帰無仮説のもとでパラメータを推定していることによる制約があります。独立の仮定のもとで推定するパラメータは、行の確率 $p_{1\cdot}, \dots, p_{r\cdot}$($\sum = 1$ の制約で $r – 1$ 個の自由パラメータ)と列の確率 $p_{\cdot 1}, \dots, p_{\cdot c}$($\sum = 1$ の制約で $c – 1$ 個の自由パラメータ)です。

セルの数 $rc$ から制約条件の数を引くと、

$$ rc – (r – 1) – (c – 1) – 1 = rc – r – c + 1 = (r-1)(c-1) $$

したがって、自由度は $(r-1)(c-1)$ となります。

方法2: 直感的な数え方

$r \times c$ の分割表で、行計と列計が固定されているとします。行の最後の行と列の最後の列は、合計の制約から自動的に決まるので、自由に値を設定できるのは左上の $(r-1) \times (c-1)$ のブロックだけです。

例として $2 \times 3$ の分割表を考えましょう。

列1 列2 列3 行計
行1 ? ? 自動 $R_1$
行2 自動 自動 自動 $R_2$
列計 $C_1$ $C_2$ $C_3$ $n$

?」の位置の2つの値を自由に決めれば、残りの4つのセルは行計・列計の制約から一意に決まります。自由に動かせるセルの数は $(2-1)(3-1) = 2$ 個です。

帰無分布

以上の議論から、帰無仮説のもとで次が成り立ちます。

$$ \chi^2 = \sum_{i=1}^{r}\sum_{j=1}^{c} \frac{(O_{ij} – E_{ij})^2}{E_{ij}} \xrightarrow{d} \chi^2_{(r-1)(c-1)} \quad (n \to \infty) $$

棄却域は適合度検定と同様に右片側です。

$$ \chi^2 > \chi^2_{\alpha,\, (r-1)(c-1)} $$

適合度検定との対応

ここで、適合度検定と独立性検定の対応を整理しておきましょう。

適合度検定 独立性検定
カテゴリ数 $k$ 個 $r \times c$ 個のセル
期待度数 $E_i = np_i$ $E_{ij} = R_iC_j / n$
自由度 $k – 1$ $(r-1)(c-1)$
帰無仮説 データは特定の分布に従う 2変数は独立
統計量の形 $\sum \frac{(O – E)^2}{E}$ $\sum \frac{(O – E)^2}{E}$

統計量の形が完全に同じであること、そして自由度の決め方だけが異なることがわかります。実は、独立性検定は「分割表のセルを1列に並べた $rc$ カテゴリの適合度検定」と見なせるのです。ただし、独立の仮定から期待度数を周辺度数から推定するため、推定パラメータの分だけ自由度が減ります。

理論の構造が明確になったところで、次はPythonで独立性検定を実装し、具体的な数値で確認していきましょう。

独立性検定のPython実装

例1: 2×2分割表 — 喫煙と肺がんの関連

まず最もシンプルな $2 \times 2$ の分割表から始めましょう。300人を対象に喫煙習慣と肺がんの有無を調査した架空データを使います。

肺がんあり 肺がんなし 合計
喫煙者 40 110 150
非喫煙者 15 135 150
合計 55 245 300
import numpy as np
from scipy import stats

# 2x2 分割表(喫煙 × 肺がん)
observed = np.array([
    [40, 110],   # 喫煙者: 肺がんあり, 肺がんなし
    [15, 135]    # 非喫煙者: 肺がんあり, 肺がんなし
])

# 期待度数を手計算
row_sums = observed.sum(axis=1)  # [150, 150]
col_sums = observed.sum(axis=0)  # [55, 245]
n = observed.sum()               # 300

expected = np.outer(row_sums, col_sums) / n
print("期待度数(手計算):")
print(expected)

# カイ二乗統計量を手計算
chi2_manual = np.sum((observed - expected)**2 / expected)
df_manual = (observed.shape[0] - 1) * (observed.shape[1] - 1)
p_manual = 1 - stats.chi2.cdf(chi2_manual, df_manual)

print(f"\nχ² = {chi2_manual:.4f}")
print(f"自由度: {df_manual}")
print(f"p値: {p_manual:.6f}")

# scipy.stats.chi2_contingency による検証
chi2, p_value, dof, exp_table = stats.chi2_contingency(observed, correction=False)
print(f"\n=== scipy.stats.chi2_contingency(補正なし)===")
print(f"χ² = {chi2:.4f}, 自由度: {dof}, p値: {p_value:.6f}")

# イェーツの連続性補正あり(2x2表のデフォルト)
chi2_yates, p_yates, dof_yates, _ = stats.chi2_contingency(observed, correction=True)
print(f"\n=== イェーツの補正あり ===")
print(f"χ² = {chi2_yates:.4f}, p値: {p_yates:.6f}")

手計算とscipyの結果が一致しています。$\chi^2 \approx 14.81$、p値は約0.000118と非常に小さく、有意水準0.05のもとで帰無仮説(喫煙と肺がんは独立)は明確に棄却されます。期待度数を見ると、独立の仮定のもとでは喫煙者の肺がん数は27.5人のはずですが、実際には40人観測されており、大きなずれがあることがわかります。

また、イェーツの連続性補正を適用すると $\chi^2$ 値がわずかに小さくなります(より保守的な検定)。この補正については後述します。

例2: 3×3分割表 — 学習法と試験結果の関連

次に、より大きな分割表の例を見ましょう。450人の学生を対象に、学習法(テキスト、動画、演習問題)と試験結果(優、良、可)の関連を調べます。

import numpy as np
from scipy import stats

# 3x3 分割表(学習法 × 試験結果)
observed = np.array([
    [30, 55, 65],   # テキスト: 優, 良, 可
    [50, 60, 40],   # 動画: 優, 良, 可
    [70, 50, 30]    # 演習問題: 優, 良, 可
])

labels_row = ['テキスト', '動画', '演習問題']
labels_col = ['優', '良', '可']

# 期待度数を計算
row_sums = observed.sum(axis=1)
col_sums = observed.sum(axis=0)
n = observed.sum()
expected = np.outer(row_sums, col_sums) / n

print("=== 観測度数 ===")
for i, row_label in enumerate(labels_row):
    row_str = "  ".join([f"{labels_col[j]}: {observed[i,j]:3d}" for j in range(3)])
    print(f"  {row_label}: {row_str} | 計: {row_sums[i]}")
print(f"  列計: {col_sums}")

print("\n=== 期待度数 ===")
for i, row_label in enumerate(labels_row):
    row_str = "  ".join([f"{labels_col[j]}: {expected[i,j]:6.1f}" for j in range(3)])
    print(f"  {row_label}: {row_str}")

# 各セルの寄与
contributions = (observed - expected)**2 / expected
print("\n=== 各セルのχ²への寄与 ===")
for i, row_label in enumerate(labels_row):
    row_str = "  ".join([f"{labels_col[j]}: {contributions[i,j]:5.2f}" for j in range(3)])
    print(f"  {row_label}: {row_str}")

# 検定の実行
chi2, p_value, dof, _ = stats.chi2_contingency(observed, correction=False)
print(f"\nχ² = {chi2:.4f}")
print(f"自由度: {dof}")
print(f"p値: {p_value:.6f}")
print(f"判定(α=0.05): {'帰無仮説を棄却' if p_value < 0.05 else '帰無仮説を棄却しない'}")

$3 \times 3$ 分割表では自由度は $(3-1)(3-1) = 4$ です。結果を見ると、$\chi^2 \approx 28.93$、p値は約0.000008と極めて小さく、学習法と試験結果の間に有意な関連があることがわかります。各セルの $\chi^2$ への寄与を見ると、特に「テキスト学習で可の成績」(期待より多い)と「演習問題で優の成績」(期待より多い)のずれが大きく、演習問題を使った学習が良い成績と関連している傾向が読み取れます。

分割表のヒートマップによる可視化

分割表のパターンを視覚的に把握するために、観測度数と期待度数のずれをヒートマップで可視化しましょう。

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

# データの再定義
observed = np.array([
    [30, 55, 65],
    [50, 60, 40],
    [70, 50, 30]
])

row_sums = observed.sum(axis=1)
col_sums = observed.sum(axis=0)
n = observed.sum()
expected = np.outer(row_sums, col_sums) / n

# 標準化残差((O - E) / sqrt(E))
std_residuals = (observed - expected) / np.sqrt(expected)

labels_row = ['Text', 'Video', 'Exercises']
labels_col = ['Excellent', 'Good', 'Fair']

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

# 左: 観測度数
im0 = axes[0].imshow(observed, cmap='Blues', aspect='auto')
axes[0].set_title('Observed Frequencies', fontsize=13)
for i in range(3):
    for j in range(3):
        axes[0].text(j, i, f'{observed[i,j]}', ha='center', va='center',
                     fontsize=14, fontweight='bold')

# 中央: 期待度数
im1 = axes[1].imshow(expected, cmap='Oranges', aspect='auto')
axes[1].set_title('Expected Frequencies', fontsize=13)
for i in range(3):
    for j in range(3):
        axes[1].text(j, i, f'{expected[i,j]:.1f}', ha='center', va='center',
                     fontsize=14, fontweight='bold')

# 右: 標準化残差
im2 = axes[2].imshow(std_residuals, cmap='RdBu_r', aspect='auto',
                      vmin=-3, vmax=3)
axes[2].set_title('Standardized Residuals', fontsize=13)
for i in range(3):
    for j in range(3):
        axes[2].text(j, i, f'{std_residuals[i,j]:.2f}', ha='center',
                     va='center', fontsize=14, fontweight='bold')
plt.colorbar(im2, ax=axes[2], shrink=0.8)

for ax in axes:
    ax.set_xticks(range(3))
    ax.set_xticklabels(labels_col, fontsize=11)
    ax.set_yticks(range(3))
    ax.set_yticklabels(labels_row, fontsize=11)

plt.suptitle('Contingency Table Analysis: Study Method vs. Exam Result',
             fontsize=15, y=1.02)
plt.tight_layout()
plt.show()

3つのヒートマップから、分割表の構造が明確に読み取れます。左のパネル(観測度数)ではテキスト学習が「可」に偏り、演習問題が「優」に偏る傾向が見えます。中央のパネル(期待度数)は独立の場合の理論値で、行間の差が小さいです。右のパネル(標準化残差)が最も重要で、赤色(正の残差、期待より多い)と青色(負の残差、期待より少ない)が明確なパターンを形成しています。特に演習問題×優のセル(正の残差が最大)とテキスト×優のセル(負の残差)の対照が際立っており、学習法の選択が成績に影響している構造が視覚的に理解できます。

ここまでで適合度検定と独立性検定をそれぞれ詳しく見てきました。次は、この2つの検定を統一的な視点から整理し、同じ枠組みの中にどう位置づけられるかを明確にしましょう。

2つの検定の統一的理解

共通する数学的構造

適合度検定と独立性検定は、表面上は異なる問題に対応していますが、その数学的構造は同一です。両者に共通する枠組みを整理します。

ステップ1: カテゴリへの分類

どちらの検定も、$n$ 個のデータが有限個のカテゴリに分類されるという設定から出発します。適合度検定では $k$ 個のカテゴリ、独立性検定では $r \times c$ 個のセルがカテゴリに相当します。

ステップ2: 帰無仮説のもとでの期待度数の計算

帰無仮説が正しいと仮定して、各カテゴリの期待度数を計算します。適合度検定では理論確率 $p_i$ から $E_i = np_i$ を計算し、独立性検定では独立の仮定と周辺度数から $E_{ij} = R_iC_j/n$ を計算します。

ステップ3: ピアソンのカイ二乗統計量の計算

全カテゴリにわたり、$(O – E)^2 / E$ を合計します。

ステップ4: 自由度の決定と検定

自由度 = カテゴリ数 – 1 – 推定パラメータ数 に基づいて自由度を決定し、$\chi^2$ 分布で検定します。

自由度の統一的理解

自由度を「カテゴリ数 – 1 – 推定パラメータ数」として統一的に理解できます。

適合度検定の場合を考えます。カテゴリ数は $k$ で、制約は $\sum O_i = n$ の1つだけです(理論確率 $p_i$ は既知として与えられるため、追加のパラメータ推定はありません)。したがって、自由度は $k – 1$ です。

独立性検定の場合を考えます。カテゴリ数は $rc$ で、制約 $\sum\sum O_{ij} = n$ に加え、帰無仮説のもとで行の確率 $r – 1$ 個と列の確率 $c – 1$ 個を周辺度数から推定しています。したがって、自由度は次のようになります。

$$ rc – 1 – (r – 1) – (c – 1) = rc – r – c + 1 = (r-1)(c-1) $$

このように、自由度の公式は一見異なりますが、「全カテゴリ数から制約条件と推定パラメータの数を引く」という同じ原理に基づいています。

独立性検定は「特殊な適合度検定」

独立性検定を適合度検定の視点から再解釈してみましょう。$r \times c$ 個のセルを1列に並べて $m = rc$ 個のカテゴリとみなします。帰無仮説(独立)のもとで、セル $(i, j)$ に入る確率は次のようになります。

$$ p_{ij} = p_{i\cdot} \times p_{\cdot j} $$

ただし、$p_{i\cdot}$ と $p_{\cdot j}$ は未知であり、周辺度数から推定します。つまり独立性検定は、「$rc$ カテゴリの適合度検定だが、理論確率が完全には既知ではなく、一部のパラメータを推定する必要がある」というケースに相当するのです。パラメータの推定に $(r-1) + (c-1)$ 個の自由度を消費するため、最終的な自由度が $(r-1)(c-1)$ になるわけです。

この見方は、カイ二乗検定の枠組みがいかに一般的であるかを示しています。期待度数の計算方法と推定パラメータの数が異なるだけで、全ての場合に同じ $\chi^2$ 統計量が使えるのです。

Python で統一性を確認する

2つの検定が同じ枠組みにあることを、コードで確認しましょう。独立性検定の $\chi^2$ 値を、分割表を1次元に展開した適合度検定として計算しても、同じ結果が得られることを示します。

import numpy as np
from scipy import stats

# 独立性検定のデータ
observed_2d = np.array([
    [40, 110],
    [15, 135]
])

# 方法1: scipy の独立性検定
chi2_indep, p_indep, dof_indep, exp_indep = stats.chi2_contingency(
    observed_2d, correction=False
)
print("=== 方法1: 独立性検定 ===")
print(f"χ² = {chi2_indep:.4f}, 自由度 = {dof_indep}, p値 = {p_indep:.6f}")

# 方法2: 分割表を1次元に展開し、適合度検定として計算
observed_1d = observed_2d.flatten()  # [40, 110, 15, 135]
expected_1d = exp_indep.flatten()

chi2_gof = np.sum((observed_1d - expected_1d)**2 / expected_1d)
# 自由度: 4カテゴリ - 1 - (行の確率1個 + 列の確率1個) = 4 - 1 - 2 = 1
df_gof = (observed_2d.shape[0] - 1) * (observed_2d.shape[1] - 1)
p_gof = 1 - stats.chi2.cdf(chi2_gof, df_gof)

print("\n=== 方法2: 1次元適合度検定として計算 ===")
print(f"χ² = {chi2_gof:.4f}, 自由度 = {df_gof}, p値 = {p_gof:.6f}")

print(f"\nχ²値の一致: {np.isclose(chi2_indep, chi2_gof)}")

$\chi^2$ 値が完全に一致していることが確認できます。方法1は scipy.stats.chi2_contingency による独立性検定、方法2は分割表のセルを1次元に展開して手動で計算した適合度検定ですが、結果は同じです。これは、独立性検定が「期待度数を周辺度数から推定する適合度検定」に他ならないことの直接的な実証です。

このように両者が統一的に理解できると、新しい応用場面に出会ったときも「カテゴリは何か」「期待度数はどう計算するか」「自由度はいくつか」を考えるだけで、適切にカイ二乗検定を適用できるようになります。

最後に、カイ二乗検定を実用する上で知っておくべき注意点と、問題が生じた場合の代替手法を解説します。

注意点と代替手法

期待度数が小さい場合の問題

カイ二乗検定は、$\chi^2$ 統計量が $\chi^2$ 分布に近似的に従うことを利用しています。この近似は、中心極限定理に基づいているため、期待度数が十分に大きいことが前提です。

一般に、全てのカテゴリ(セル)で $E_i \geq 5$ であることが推奨されます。この条件は「コクランの規則」(Cochran’s rule)と呼ばれ、近似の精度を保証するための経験則です。

期待度数が5未満のカテゴリがある場合、$\chi^2$ 近似の精度が低下し、p値が不正確になるおそれがあります。このような場合の対処法として、いくつかの方法があります。

対処法1: カテゴリの統合

適合度検定において期待度数が小さいカテゴリがある場合、隣接するカテゴリを統合して期待度数を大きくすることができます。たとえば、サイコロの検定で「1の目」と「2の目」を統合して「1または2」というカテゴリにすれば、期待度数が2倍になります。ただし、統合するとカテゴリ数が減り、検定の解像度が下がるというトレードオフがあります。

対処法2: イェーツの連続性補正

$2 \times 2$ の分割表に限り、連続補正を適用する方法があります。これについては次の項目で詳しく解説します。

対処法3: フィッシャーの正確検定

近似を使わず、正確な確率を計算する方法です。これも後述します。

イェーツの連続性補正

$2 \times 2$ の分割表では、自由度が1になります。離散分布(度数データ)を連続分布($\chi^2$ 分布)で近似するため、特にサンプルサイズが小さいときに近似の精度が問題になります。

イェーツ(Yates, 1934)は、各セルの $|O_{ij} – E_{ij}|$ から0.5を引くことで、離散分布と連続分布の乖離を補正する方法を提案しました。

$$ \chi^2_{\text{Yates}} = \sum_{i=1}^{r}\sum_{j=1}^{c} \frac{(|O_{ij} – E_{ij}| – 0.5)^2}{E_{ij}} $$

この補正は $2 \times 2$ の分割表でのみ使用します。補正により $\chi^2$ 値が小さくなる(より保守的になる)ため、帰無仮説を棄却しにくくなります。

Pythonでは、scipy.stats.chi2_contingency のデフォルト設定(correction=True)でイェーツの補正が適用されます。

import numpy as np
from scipy import stats

# サンプルサイズが小さい 2x2 表
observed_small = np.array([
    [8, 12],
    [3, 17]
])

# 補正なし
chi2_no, p_no, _, _ = stats.chi2_contingency(observed_small, correction=False)
# 補正あり
chi2_yes, p_yes, _, _ = stats.chi2_contingency(observed_small, correction=True)

print("=== イェーツの補正の効果 ===")
print(f"補正なし: χ² = {chi2_no:.4f}, p値 = {p_no:.4f}")
print(f"補正あり: χ² = {chi2_yes:.4f}, p値 = {p_yes:.4f}")

補正なしの場合と補正ありの場合を比較すると、補正により $\chi^2$ 値が小さくなり、p値が大きくなっている(帰無仮説を棄却しにくくなっている)ことが確認できます。サンプルサイズが小さい $2 \times 2$ 表では、この補正を適用するほうが保守的で安全な判断が得られます。ただし、サンプルサイズが十分に大きい場合は補正の影響は無視できるほど小さくなります。

フィッシャーの正確検定

期待度数が5未満のセルが存在する場合、$\chi^2$ 近似自体が信頼できなくなります。このような場合に用いられるのがフィッシャーの正確検定(Fisher’s exact test)です。

フィッシャーの正確検定は、$\chi^2$ 近似を一切使わず、周辺度数を固定したもとでの超幾何分布に基づいて正確なp値を計算します。特に $2 \times 2$ の分割表でよく使われます。

import numpy as np
from scipy import stats

# 期待度数が小さくなるデータ
observed_rare = np.array([
    [3, 1],
    [1, 6]
])

# カイ二乗検定(参考 — 期待度数が小さいので近似が不正確)
chi2, p_chi2, dof, expected = stats.chi2_contingency(observed_rare, correction=True)
print("=== カイ二乗検定(イェーツ補正あり)===")
print(f"期待度数:\n{expected}")
print(f"χ² = {chi2:.4f}, p値 = {p_chi2:.4f}")
print(f"警告: 期待度数に5未満のセルがあります({(expected < 5).sum()}セル)")

# フィッシャーの正確検定
odds_ratio, p_fisher = stats.fisher_exact(observed_rare)
print(f"\n=== フィッシャーの正確検定 ===")
print(f"オッズ比: {odds_ratio:.4f}")
print(f"p値: {p_fisher:.4f}")

期待度数を見ると、4つのセル中3つで5未満の値が出ており、$\chi^2$ 近似は信頼できません。フィッシャーの正確検定は超幾何分布から正確なp値を計算するため、このような場合でも信頼性の高い結果が得られます。オッズ比も同時に計算されるため、2つの変数の関連の強さも定量的に評価できます。

使い分けのまとめ

以下に、状況に応じた検定手法の選択指針をまとめます。

期待度数が全て5以上の場合: 通常の $\chi^2$ 検定を使用します。$2 \times 2$ 表ではイェーツの補正を適用するかどうかは任意ですが、サンプルが大きければ影響は小さいです。

$2 \times 2$ 表でサンプルが小さい場合: イェーツの補正付き $\chi^2$ 検定、またはフィッシャーの正確検定を使用します。期待度数に5未満のセルがあれば、フィッシャーの正確検定が推奨されます。

$r \times c$ 表($r, c > 2$)で期待度数が小さい場合: カテゴリを統合して期待度数を5以上にするか、Fisher-Freeman-Haltonの正確検定(フィッシャーの正確検定を一般化したもの)を検討します。ただし、大きな分割表での正確検定は計算コストが高くなることがあります。

import numpy as np
from scipy import stats

# 各手法の適用条件を可視化するフローチャート的な出力
def recommend_test(observed):
    """分割表から適切な検定手法を推奨する"""
    r, c = observed.shape
    n = observed.sum()
    row_sums = observed.sum(axis=1)
    col_sums = observed.sum(axis=0)
    expected = np.outer(row_sums, col_sums) / n

    print(f"分割表のサイズ: {r}x{c}")
    print(f"サンプルサイズ: {n}")
    print(f"期待度数の最小値: {expected.min():.2f}")
    print(f"期待度数 < 5 のセル数: {(expected < 5).sum()} / {r*c}")

    if r == 2 and c == 2:
        if expected.min() < 5:
            print("推奨: フィッシャーの正確検定")
            _, p = stats.fisher_exact(observed)
            print(f"  → p値 = {p:.4f}")
        elif n < 40:
            print("推奨: イェーツの補正付きχ²検定")
            chi2, p, _, _ = stats.chi2_contingency(observed, correction=True)
            print(f"  → χ² = {chi2:.4f}, p値 = {p:.4f}")
        else:
            print("推奨: 通常のχ²検定")
            chi2, p, _, _ = stats.chi2_contingency(observed, correction=False)
            print(f"  → χ² = {chi2:.4f}, p値 = {p:.4f}")
    else:
        if expected.min() >= 5:
            print("推奨: 通常のχ²検定")
            chi2, p, _, _ = stats.chi2_contingency(observed, correction=False)
            print(f"  → χ² = {chi2:.4f}, p値 = {p:.4f}")
        else:
            print("推奨: カテゴリ統合後にχ²検定、または正確検定を検討")

# テストケース
print("--- ケース1: 十分なサンプル ---")
recommend_test(np.array([[40, 110], [15, 135]]))

print("\n--- ケース2: 小さなサンプル ---")
recommend_test(np.array([[3, 1], [1, 6]]))

print("\n--- ケース3: 3x3表 ---")
recommend_test(np.array([[30, 55, 65], [50, 60, 40], [70, 50, 30]]))

この関数は、分割表の特徴(サイズ、サンプルサイズ、期待度数の最小値)に基づいて、適切な検定手法を自動的に推奨します。実務では、データを見てから検定手法を選ぶのではなく、事前に分析計画を立てた上で検定を行うことが望ましいですが、期待度数の条件チェックは検定の信頼性を担保する上で不可欠です。

ここまでで理論、実装、注意点を一通り網羅しました。最後に、本記事の内容を振り返りましょう。

まとめ

本記事では、カイ二乗検定の2つの主要な応用 — 適合度検定と独立性検定 — を統一的な枠組みから解説しました。

  • 統一的な枠組み: 両検定とも「観測度数と期待度数のずれ」を $\chi^2 = \sum (O – E)^2 / E$ で測定するという共通構造を持ちます。異なるのは期待度数の計算方法と自由度だけです
  • 適合度検定: データが特定の理論分布に従うかを検定します。期待度数 $E_i = np_i$、自由度 $k – 1$。サイコロの公平性検証やメンデルの法則の検証などに応用されます
  • 独立性検定: 分割表における2つのカテゴリ変数の独立性を検定します。期待度数 $E_{ij} = R_iC_j/n$、自由度 $(r-1)(c-1)$。医学研究やマーケティング分析などに応用されます
  • 統一的理解: 独立性検定は「期待度数をデータから推定する適合度検定」と見なせます。この視点により、カイ二乗検定のフレームワークの一般性が理解できます
  • 注意点: 期待度数が5未満のセルがある場合は $\chi^2$ 近似の信頼性が低下するため、イェーツの補正やフィッシャーの正確検定の使用を検討する必要があります

カイ二乗検定の理論を理解したことで、度数データの分析における強力な道具を手に入れました。この知識は、さらに発展的なトピック — 例えばログリニアモデルによる多元分割表の解析、対数尤度比検定(G検定)との比較、残差分析によるセル単位の効果の詳細な検討 — への橋渡しとなります。

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