カイ二乗検定($\chi^2$ 検定)は、カテゴリカルデータ(度数データ)の分析に使われる基本的な検定手法です。主に2つの用途があります。
- 適合度検定(goodness-of-fit test): 観測された度数が理論的な分布に従うかを検定
- 独立性検定(test of independence): 2つのカテゴリ変数が独立かどうかを検定
サイコロの公平性の検証、アンケートの分析、遺伝学のメンデルの法則の検証など、カテゴリカルデータが関わるあらゆる場面で活用されます。
本記事の内容
- $\chi^2$ 分布の定義と性質
- 適合度検定の検定統計量 $\chi^2 = \sum (O_i – E_i)^2 / E_i$ の導出
- 独立性検定(分割表)の手順
- 自由度の決め方
- Python での実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
$\chi^2$ 分布
定義
$Z_1, Z_2, \dots, Z_k$ が独立に標準正規分布 $N(0, 1)$ に従うとき、
$$ V = Z_1^2 + Z_2^2 + \dots + Z_k^2 = \sum_{i=1}^{k} Z_i^2 $$
は 自由度 $k$ のカイ二乗分布 $\chi^2_k$ に従います。
確率密度関数
$$ f(x) = \frac{1}{2^{k/2}\Gamma(k/2)} x^{k/2 – 1} e^{-x/2}, \quad x > 0 $$
基本的な性質
- 平均: $E[V] = k$
- 分散: $\text{Var}(V) = 2k$
- 非負の値のみを取る($V \geq 0$)
- $k$ が大きくなると近似的に正規分布に近づく
再生性
$V_1 \sim \chi^2_{k_1}$ と $V_2 \sim \chi^2_{k_2}$ が独立なら、
$$ V_1 + V_2 \sim \chi^2_{k_1 + k_2} $$
適合度検定
問題設定
$k$ 個のカテゴリがあり、各カテゴリの理論的な確率が $p_1, p_2, \dots, p_k$($\sum p_i = 1$)と分かっています。$n$ 回の観測から得られた度数 $O_1, O_2, \dots, O_k$ が、この理論分布に従うかを検定します。
$$ H_0: \text{観測度数は理論確率 $p_1, \dots, p_k$ に従う} $$
$$ H_1: \text{少なくとも1つのカテゴリで理論確率と異なる} $$
期待度数
$H_0$ のもとでの各カテゴリの期待度数は、
$$ E_i = n \cdot p_i $$
検定統計量の導出
各カテゴリの観測度数 $O_i$ は、$n$ が大きいとき近似的に正規分布に従います。中心極限定理の多項分布版により、
$$ O_i \approx N(E_i, E_i(1 – p_i)) $$
標準化すると、
$$ \frac{O_i – E_i}{\sqrt{E_i}} \approx N(0, 1) \quad (\text{近似的に}) $$
これらの二乗和を考えると、
$$ \chi^2 = \sum_{i=1}^{k} \frac{(O_i – E_i)^2}{E_i} $$
ただし $\sum O_i = \sum E_i = n$ という制約があるため、$k$ 個の変量のうち自由に動けるのは $k – 1$ 個です。したがって、
$$ \chi^2 \sim \chi^2_{k-1} \quad (\text{$H_0$ のもと、$n$ が十分大きいとき}) $$
自由度が $k – 1$ になる直感的な理由は、$k$ 個のカテゴリの度数の合計が $n$ に固定されているため、独立に変動できる度数は $k – 1$ 個だからです。
棄却域
$\chi^2$ 検定は常に 右片側検定 です。$\chi^2$ 値が大きいほど観測と理論のずれが大きいことを意味するためです。
$$ \chi^2 > \chi^2_{\alpha, k-1} $$
適用条件
ピアソンの $\chi^2$ 検定を適用するには、以下の条件を満たす必要があります。
- すべてのカテゴリで $E_i \geq 5$ であること
- $E_i < 5$ のカテゴリがある場合は、隣接カテゴリと統合する
具体例: サイコロの公平性検定
サイコロを120回振った結果が以下のとおりでした。
| 目 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 観測度数 $O_i$ | 25 | 17 | 15 | 23 | 22 | 18 |
| 期待度数 $E_i$ | 20 | 20 | 20 | 20 | 20 | 20 |
$$ \begin{align} \chi^2 &= \frac{(25-20)^2}{20} + \frac{(17-20)^2}{20} + \frac{(15-20)^2}{20} + \frac{(23-20)^2}{20} + \frac{(22-20)^2}{20} + \frac{(18-20)^2}{20} \\ &= \frac{25 + 9 + 25 + 9 + 4 + 4}{20} \\ &= \frac{76}{20} = 3.80 \end{align} $$
自由度 $k – 1 = 5$ の $\chi^2$ 分布で $\chi^2_{0.05, 5} = 11.07$ です。
$3.80 < 11.07$ なので $H_0$ を棄却しません。サイコロは公平であるという仮説は棄却されません。
独立性検定
問題設定
2つのカテゴリ変数が独立かどうかを、$r \times c$ の 分割表(contingency table) を用いて検定します。
$$ H_0: \text{2変数は独立} $$
$$ H_1: \text{2変数は独立でない(関連がある)} $$
分割表と期待度数
$r$ 行 $c$ 列の分割表を考えます。セル $(i, j)$ の観測度数を $O_{ij}$、行の合計を $R_i$、列の合計を $C_j$、全体の合計を $n$ とします。
独立の仮定($H_0$)のもとで、期待度数は次のようになります。
$$ E_{ij} = \frac{R_i \times C_j}{n} $$
これは、行と列が独立であれば $P(\text{行}=i, \text{列}=j) = P(\text{行}=i) \times P(\text{列}=j)$ であることから導かれます。
$$ E_{ij} = n \cdot P(\text{行}=i) \cdot P(\text{列}=j) = n \cdot \frac{R_i}{n} \cdot \frac{C_j}{n} = \frac{R_i \times C_j}{n} $$
検定統計量
$$ \chi^2 = \sum_{i=1}^{r}\sum_{j=1}^{c} \frac{(O_{ij} – E_{ij})^2}{E_{ij}} $$
自由度
自由度は $(r – 1)(c – 1)$ です。
$r \times c$ のセルがありますが、行合計と列合計が固定されているため、自由に動けるセルの数は $(r-1)(c-1)$ 個です。直感的には、$r-1$ 行と $c-1$ 列の値が決まれば、残りのセルは合計の制約から自動的に定まります。
$$ \chi^2 \sim \chi^2_{(r-1)(c-1)} \quad (\text{$H_0$ のもと}) $$
具体例: 性別と食事の好みの独立性
200人の性別と食事の好み(和食・洋食・中華)の分割表が以下のとおりでした。
| 和食 | 洋食 | 中華 | 合計 | |
|---|---|---|---|---|
| 男性 | 30 | 40 | 30 | 100 |
| 女性 | 45 | 30 | 25 | 100 |
| 合計 | 75 | 70 | 55 | 200 |
期待度数を計算します。たとえば男性×和食の期待度数は、
$$ E_{11} = \frac{100 \times 75}{200} = 37.5 $$
すべての期待度数を計算すると、
| 和食 | 洋食 | 中華 | |
|---|---|---|---|
| 男性 | 37.5 | 35.0 | 27.5 |
| 女性 | 37.5 | 35.0 | 27.5 |
$$ \begin{align} \chi^2 &= \frac{(30-37.5)^2}{37.5} + \frac{(40-35)^2}{35} + \frac{(30-27.5)^2}{27.5} \\ &\quad + \frac{(45-37.5)^2}{37.5} + \frac{(30-35)^2}{35} + \frac{(25-27.5)^2}{27.5} \\ &= 1.500 + 0.714 + 0.227 + 1.500 + 0.714 + 0.227 \\ &= 4.883 \end{align} $$
自由度 $(2-1)(3-1) = 2$ で、$\chi^2_{0.05, 2} = 5.991$ です。
$4.883 < 5.991$ なので $H_0$ を棄却しません。性別と食事の好みに有意な関連は見られません。
Pythonでの実装
適合度検定
import numpy as np
from scipy import stats
# サイコロのデータ
observed = np.array([25, 17, 15, 23, 22, 18])
expected_prob = np.array([1/6] * 6)
n = observed.sum()
expected = n * expected_prob
# カイ二乗検定統計量を手計算
chi2_manual = np.sum((observed - expected)**2 / expected)
df = len(observed) - 1
p_manual = 1 - stats.chi2.cdf(chi2_manual, df)
print("=== 適合度検定(手計算)===")
print(f"観測度数: {observed}")
print(f"期待度数: {expected}")
print(f"χ² = {chi2_manual:.4f}")
print(f"自由度: {df}")
print(f"p値: {p_manual:.4f}")
# scipy での検定
chi2_scipy, p_scipy = stats.chisquare(observed, f_exp=expected)
print(f"\n=== scipy.stats.chisquare ===")
print(f"χ² = {chi2_scipy:.4f}")
print(f"p値: {p_scipy:.4f}")
print(f"α = 0.05 での判定: {'棄却' if p_scipy < 0.05 else '棄却しない'}")
独立性検定
import numpy as np
from scipy import stats
# 分割表(性別 × 食事の好み)
observed_table = np.array([
[30, 40, 30], # 男性
[45, 30, 25] # 女性
])
# scipy での独立性検定
chi2, p_value, dof, expected_table = stats.chi2_contingency(observed_table)
print("=== 独立性検定 ===")
print(f"観測度数:\n{observed_table}")
print(f"\n期待度数:\n{expected_table}")
print(f"\nχ² = {chi2:.4f}")
print(f"自由度: {dof}")
print(f"p値: {p_value:.4f}")
print(f"α = 0.05 での判定: {'棄却' if p_value < 0.05 else '棄却しない'}")
$\chi^2$ 分布の可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
x = np.linspace(0, 25, 1000)
fig, ax = plt.subplots(figsize=(10, 6))
# 各自由度のχ²分布
for df, color in zip([1, 2, 3, 5, 10], ['red', 'orange', 'green', 'blue', 'purple']):
pdf = stats.chi2.pdf(x, df)
ax.plot(x, pdf, color=color, linewidth=2, label=f'$\\chi^2_{{{df}}}$')
ax.set_xlabel('$x$', fontsize=13)
ax.set_ylabel('Probability Density', fontsize=13)
ax.set_title('Chi-squared Distribution for Various Degrees of Freedom', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 0.5)
ax.set_xlim(0, 25)
plt.tight_layout()
plt.show()
適合度検定の可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
# サイコロの適合度検定を可視化
observed = np.array([25, 17, 15, 23, 22, 18])
expected = np.array([20, 20, 20, 20, 20, 20])
categories = ['1', '2', '3', '4', '5', '6']
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左: 観測度数と期待度数の比較
x_pos = np.arange(len(categories))
width = 0.35
axes[0].bar(x_pos - width/2, observed, width, label='Observed', color='steelblue',
edgecolor='black')
axes[0].bar(x_pos + width/2, expected, width, label='Expected', color='coral',
edgecolor='black')
axes[0].set_xlabel('Die Face', fontsize=12)
axes[0].set_ylabel('Frequency', fontsize=12)
axes[0].set_title('Observed vs Expected Frequencies', fontsize=13)
axes[0].set_xticks(x_pos)
axes[0].set_xticklabels(categories)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3, axis='y')
# 右: χ²分布と検定統計量
chi2_obs = np.sum((observed - expected)**2 / expected)
df = len(observed) - 1
x = np.linspace(0, 20, 500)
pdf = stats.chi2.pdf(x, df)
axes[1].plot(x, pdf, 'k-', linewidth=2, label=f'$\\chi^2_{{{df}}}$')
# 棄却域
chi2_crit = stats.chi2.ppf(0.95, df)
x_reject = x[x >= chi2_crit]
axes[1].fill_between(x_reject, stats.chi2.pdf(x_reject, df),
color='red', alpha=0.3, label=f'Rejection ($\\alpha$ = 0.05)')
axes[1].axvline(chi2_crit, color='red', linestyle='--', linewidth=1.5)
# 検定統計量
p_value = 1 - stats.chi2.cdf(chi2_obs, df)
axes[1].axvline(chi2_obs, color='blue', linewidth=2.5,
label=f'$\\chi^2_{{obs}}$ = {chi2_obs:.2f} (p = {p_value:.4f})')
axes[1].set_xlabel('$\\chi^2$', fontsize=12)
axes[1].set_ylabel('Probability Density', fontsize=12)
axes[1].set_title('Chi-squared Test for Dice Fairness', fontsize=13)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
axes[1].set_ylim(bottom=0)
plt.tight_layout()
plt.show()
まとめ
本記事では、$\chi^2$ 検定(適合度検定と独立性検定)について解説しました。
- $\chi^2$ 分布: 独立な標準正規変量の二乗和が従う分布。平均 $k$、分散 $2k$
- 適合度検定: $\chi^2 = \sum (O_i – E_i)^2 / E_i$、自由度 $k – 1$。観測度数が理論分布に適合するか検定
- 独立性検定: $r \times c$ 分割表に対し、$E_{ij} = R_i C_j / n$、自由度 $(r-1)(c-1)$。2変数の独立性を検定
- 適用条件: すべての期待度数が5以上であること
- 常に右片側検定: $\chi^2$ 値が大きいほど帰無仮説からのずれが大きい
次のステップとして、以下の記事も参考にしてください。