F検定と分散分析(ANOVA: Analysis of Variance)は、分散の比に基づく統計的検定手法です。F検定は2群の分散が等しいかを検定し、分散分析は3つ以上の群の平均に差があるかを一度に検定します。
t検定が2群の比較に限られるのに対し、ANOVAは複数の群を同時に比較できます。「3種類の肥料で植物の成長に差があるか」「4つの教授法で成績に差があるか」など、実験研究で頻繁に登場する場面で活躍します。
本記事の内容
- F分布の定義と性質
- 等分散のF検定
- 一元配置分散分析(one-way ANOVA)の理論
- 群間分散と群内分散の分解
- F統計量の導出
- Pythonでの実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
F分布
定義
$U \sim \chi^2_{d_1}$ と $V \sim \chi^2_{d_2}$ が独立のとき、
$$ F = \frac{U / d_1}{V / d_2} $$
は 自由度 $(d_1, d_2)$ のF分布 $F_{d_1, d_2}$ に従います。
F分布は2つのカイ二乗変量の比として定義されるため、分散の比の検定 に自然に現れます。
確率密度関数
$$ f(x) = \frac{1}{B(d_1/2, d_2/2)} \left(\frac{d_1}{d_2}\right)^{d_1/2} x^{d_1/2 – 1} \left(1 + \frac{d_1}{d_2}x\right)^{-(d_1+d_2)/2}, \quad x > 0 $$
ここで $B(\cdot, \cdot)$ はベータ関数です。
基本的な性質
- $F > 0$(非負)
- 平均: $E[F] = \frac{d_2}{d_2 – 2}$($d_2 > 2$)
- t分布との関係: $T \sim t_k$ のとき $T^2 \sim F_{1, k}$
等分散のF検定
問題設定
2つの正規母集団の分散が等しいかを検定します。
$$ X_1, \dots, X_{n_1} \sim N(\mu_1, \sigma_1^2), \quad Y_1, \dots, Y_{n_2} \sim N(\mu_2, \sigma_2^2) $$
$$ H_0: \sigma_1^2 = \sigma_2^2, \quad H_1: \sigma_1^2 \neq \sigma_2^2 $$
検定統計量の導出
不偏分散 $s_1^2, s_2^2$ を計算します。正規母集団において、
$$ \frac{(n_1 – 1)s_1^2}{\sigma_1^2} \sim \chi^2_{n_1 – 1}, \quad \frac{(n_2 – 1)s_2^2}{\sigma_2^2} \sim \chi^2_{n_2 – 1} $$
2標本が独立なので、$H_0: \sigma_1^2 = \sigma_2^2$ のもとで、
$$ F = \frac{s_1^2}{s_2^2} = \frac{(n_1-1)s_1^2 / (\sigma^2(n_1-1))}{(n_2-1)s_2^2 / (\sigma^2(n_2-1))} = \frac{\chi^2_{n_1-1}/(n_1-1)}{\chi^2_{n_2-1}/(n_2-1)} \sim F_{n_1-1, n_2-1} $$
棄却域
両側検定の場合、$F$ が大きすぎても小さすぎても $H_0$ を棄却します。
$$ F > F_{\alpha/2, n_1-1, n_2-1} \quad \text{または} \quad F < F_{1-\alpha/2, n_1-1, n_2-1} $$
慣例として、分散の大きい方を分子にして $F \geq 1$ とし、右片側のみで検定することもあります。
なぜt検定の繰り返しではダメなのか
3群以上の平均を比較するとき、すべてのペアについてt検定を繰り返すことが考えられます。しかし、これには 多重比較の問題 があります。
$k$ 群あるとき、ペアの数は $\binom{k}{2} = k(k-1)/2$ です。各検定を $\alpha = 0.05$ で行うと、少なくとも1回第1種の過誤を犯す確率(族全体の過誤率)は、
$$ 1 – (1 – \alpha)^{k(k-1)/2} $$
たとえば $k = 4$ のとき、6回のt検定を行うと、
$$ 1 – (1 – 0.05)^6 = 1 – 0.735 = 0.265 $$
約26.5%もの確率で偽陽性が生じます。ANOVAはこの問題を回避し、全群を一度に検定できます。
一元配置分散分析(One-way ANOVA)
問題設定
$k$ 個の群があり、第 $i$ 群から $n_i$ 個の観測値を得ます。
$$ X_{ij} \sim N(\mu_i, \sigma^2), \quad i = 1, \dots, k, \quad j = 1, \dots, n_i $$
すべての群で分散 $\sigma^2$ が等しいことを仮定します(等分散性)。
$$ H_0: \mu_1 = \mu_2 = \cdots = \mu_k $$
$$ H_1: \text{少なくとも1つの} \mu_i \text{が異なる} $$
変動の分解(平方和の分解)
全データの総平均を $\bar{X}_{..}$、第 $i$ 群の平均を $\bar{X}_{i.}$ とします。各観測値のずれを分解します。
$$ X_{ij} – \bar{X}_{..} = (\bar{X}_{i.} – \bar{X}_{..}) + (X_{ij} – \bar{X}_{i.}) $$
両辺を二乗して全データについて合計すると、
$$ \underbrace{\sum_{i=1}^{k}\sum_{j=1}^{n_i}(X_{ij} – \bar{X}_{..})^2}_{\text{SST}} = \underbrace{\sum_{i=1}^{k} n_i(\bar{X}_{i.} – \bar{X}_{..})^2}_{\text{SSB}} + \underbrace{\sum_{i=1}^{k}\sum_{j=1}^{n_i}(X_{ij} – \bar{X}_{i.})^2}_{\text{SSW}} $$
ここで、
- SST(Total Sum of Squares): 全体の変動
- SSB(Between-group Sum of Squares): 群間変動(群の平均の違いに起因)
- SSW(Within-group Sum of Squares): 群内変動(各群内のばらつき)
交差項が消えることを確認しましょう。
$$ \sum_{i}\sum_{j} (\bar{X}_{i.} – \bar{X}_{..})(X_{ij} – \bar{X}_{i.}) = \sum_{i}(\bar{X}_{i.} – \bar{X}_{..})\underbrace{\sum_{j}(X_{ij} – \bar{X}_{i.})}_{= 0} = 0 $$
各群内の偏差の合計は常にゼロになるため、交差項は消えます。
自由度の分解
$$ \underbrace{n – 1}_{\text{SST}} = \underbrace{k – 1}_{\text{SSB}} + \underbrace{n – k}_{\text{SSW}} $$
ここで $n = \sum_{i=1}^{k} n_i$ は全標本サイズです。
平均平方(Mean Square)
$$ \text{MSB} = \frac{\text{SSB}}{k – 1}, \quad \text{MSW} = \frac{\text{SSW}}{n – k} $$
F統計量の導出
$H_0$ のもとでは、すべての群の母平均が等しく $\mu_1 = \cdots = \mu_k = \mu$ です。
$\text{SSW}/\sigma^2 \sim \chi^2_{n-k}$ が成り立ちます。これは各群内の偏差二乗和の合計が自由度 $n-k$ のカイ二乗分布に従うためです。
$H_0$ のもとでは $\text{SSB}/\sigma^2 \sim \chi^2_{k-1}$ も成り立ちます。各群の平均が同一の正規分布から得られるためです。
SSBとSSWは独立であることが知られているため、
$$ F = \frac{\text{MSB}}{\text{MSW}} = \frac{\text{SSB}/(k-1)}{\text{SSW}/(n-k)} \sim F_{k-1, n-k} \quad (\text{$H_0$ のもと}) $$
F統計量の直感的理解
- $H_0$ が真(すべての群の平均が等しい)のとき、MSBとMSWはどちらも $\sigma^2$ の推定量になるため、$F \approx 1$
- $H_1$ が真(群の平均が異なる)のとき、MSBは群間の差を反映して大きくなり、$F > 1$ となる
したがって、$F$ が十分大きければ $H_0$ を棄却 します。
ANOVA表
| 要因 | 平方和 | 自由度 | 平均平方 | F値 |
|---|---|---|---|---|
| 群間 | SSB | $k – 1$ | MSB = SSB/$(k-1)$ | $F$ = MSB/MSW |
| 群内 | SSW | $n – k$ | MSW = SSW/$(n-k)$ | |
| 全体 | SST | $n – 1$ |
具体例
3種類の肥料(A, B, C)を使った植物の成長量(cm)が以下のとおりでした。
| 肥料A | 肥料B | 肥料C |
|---|---|---|
| 20 | 23 | 18 |
| 22 | 25 | 20 |
| 19 | 22 | 17 |
| 21 | 24 | 19 |
各群の平均: $\bar{X}_{A} = 20.5$, $\bar{X}_{B} = 23.5$, $\bar{X}_{C} = 18.5$
総平均: $\bar{X}_{..} = (20.5 + 23.5 + 18.5)/3 = 20.833$
$$ \text{SSB} = 4(20.5 – 20.833)^2 + 4(23.5 – 20.833)^2 + 4(18.5 – 20.833)^2 = 0.444 + 28.444 + 21.778 = 50.667 $$
$$ \text{SSW} = \sum_A (X_{Aj} – 20.5)^2 + \sum_B (X_{Bj} – 23.5)^2 + \sum_C (X_{Cj} – 18.5)^2 = 5 + 5 + 5 = 15 $$
$$ \text{MSB} = \frac{50.667}{2} = 25.333, \quad \text{MSW} = \frac{15}{9} = 1.667 $$
$$ F = \frac{25.333}{1.667} = 15.2 $$
$F_{0.05, 2, 9} = 4.26$ であるため、$F = 15.2 > 4.26$ で $H_0$ を棄却します。3種類の肥料の間で成長量に有意な差があると結論します。
Pythonでの実装
等分散のF検定
import numpy as np
from scipy import stats
# 2群のデータ
np.random.seed(42)
group1 = np.random.normal(50, 5, 20) # σ = 5
group2 = np.random.normal(50, 8, 25) # σ = 8
# F検定
s1_sq = np.var(group1, ddof=1)
s2_sq = np.var(group2, ddof=1)
# 大きい分散を分子にする
if s1_sq >= s2_sq:
f_stat = s1_sq / s2_sq
df1, df2 = len(group1) - 1, len(group2) - 1
else:
f_stat = s2_sq / s1_sq
df1, df2 = len(group2) - 1, len(group1) - 1
# 両側検定のp値
p_value = 2 * min(stats.f.cdf(f_stat, df1, df2),
1 - stats.f.cdf(f_stat, df1, df2))
print("=== 等分散のF検定 ===")
print(f"群1: n={len(group1)}, s² = {s1_sq:.4f}")
print(f"群2: n={len(group2)}, s² = {s2_sq:.4f}")
print(f"F = {f_stat:.4f}")
print(f"自由度: ({df1}, {df2})")
print(f"p値 (両側): {p_value:.4f}")
print(f"α = 0.05 での判定: {'棄却(分散に差あり)' if p_value < 0.05 else '棄却しない(等分散)'}")
一元配置分散分析
import numpy as np
from scipy import stats
# 3種類の肥料のデータ
fertilizer_a = np.array([20, 22, 19, 21])
fertilizer_b = np.array([23, 25, 22, 24])
fertilizer_c = np.array([18, 20, 17, 19])
# scipy での一元配置ANOVA
f_stat, p_value = stats.f_oneway(fertilizer_a, fertilizer_b, fertilizer_c)
print("=== 一元配置分散分析 (scipy) ===")
print(f"群A: 平均 = {np.mean(fertilizer_a):.2f}")
print(f"群B: 平均 = {np.mean(fertilizer_b):.2f}")
print(f"群C: 平均 = {np.mean(fertilizer_c):.2f}")
print(f"F = {f_stat:.4f}")
print(f"p値: {p_value:.6f}")
print(f"α = 0.05 での判定: {'棄却(群間に差あり)' if p_value < 0.05 else '棄却しない'}")
ANOVA表のスクラッチ実装
import numpy as np
from scipy import stats
def one_way_anova(*groups):
"""
一元配置分散分析をスクラッチで実装
Parameters
----------
*groups : array-like
各群のデータ
Returns
-------
anova_table : dict
ANOVA表の各成分
"""
k = len(groups)
all_data = np.concatenate(groups)
n_total = len(all_data)
grand_mean = np.mean(all_data)
# 群間平方和 (SSB)
ssb = sum(len(g) * (np.mean(g) - grand_mean)**2 for g in groups)
# 群内平方和 (SSW)
ssw = sum(np.sum((g - np.mean(g))**2) for g in groups)
# 全体平方和 (SST)
sst = np.sum((all_data - grand_mean)**2)
# 自由度
df_between = k - 1
df_within = n_total - k
df_total = n_total - 1
# 平均平方
msb = ssb / df_between
msw = ssw / df_within
# F統計量
f_stat = msb / msw
# p値
p_value = 1 - stats.f.cdf(f_stat, df_between, df_within)
return {
'SSB': ssb, 'SSW': ssw, 'SST': sst,
'df_between': df_between, 'df_within': df_within, 'df_total': df_total,
'MSB': msb, 'MSW': msw,
'F': f_stat, 'p_value': p_value
}
# 肥料データで実行
result = one_way_anova(
np.array([20, 22, 19, 21]),
np.array([23, 25, 22, 24]),
np.array([18, 20, 17, 19])
)
print("=== ANOVA表 ===")
print(f"{'要因':<8} {'平方和':>10} {'自由度':>6} {'平均平方':>10} {'F値':>8} {'p値':>10}")
print("-" * 60)
print(f"{'群間':<8} {result['SSB']:>10.3f} {result['df_between']:>6} {result['MSB']:>10.3f} {result['F']:>8.3f} {result['p_value']:>10.6f}")
print(f"{'群内':<8} {result['SSW']:>10.3f} {result['df_within']:>6} {result['MSW']:>10.3f}")
print(f"{'全体':<8} {result['SST']:>10.3f} {result['df_total']:>6}")
F分布と検定の可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左: F分布の形状
x = np.linspace(0, 6, 500)
for (d1, d2), color in zip([(2, 9), (3, 20), (5, 30), (10, 50)],
['red', 'blue', 'green', 'purple']):
pdf = stats.f.pdf(x, d1, d2)
axes[0].plot(x, pdf, color=color, linewidth=2,
label=f'$F_{{{d1},{d2}}}$')
axes[0].set_xlabel('$x$', fontsize=12)
axes[0].set_ylabel('Probability Density', fontsize=12)
axes[0].set_title('F-distribution', fontsize=13)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(0, 1.0)
# 右: ANOVAの検定結果
f_obs = 15.2
df1, df2 = 2, 9
x = np.linspace(0, 20, 500)
pdf = stats.f.pdf(x, df1, df2)
axes[1].plot(x, pdf, 'k-', linewidth=2, label=f'$F_{{{df1},{df2}}}$')
# 棄却域
f_crit = stats.f.ppf(0.95, df1, df2)
x_reject = x[x >= f_crit]
axes[1].fill_between(x_reject, stats.f.pdf(x_reject, df1, df2),
color='red', alpha=0.3, label=f'Rejection ($\\alpha$ = 0.05)')
axes[1].axvline(f_crit, color='red', linestyle='--', linewidth=1.5,
label=f'$F_{{crit}}$ = {f_crit:.3f}')
# 観測されたF値
p_value = 1 - stats.f.cdf(f_obs, df1, df2)
axes[1].axvline(f_obs, color='blue', linewidth=2.5,
label=f'$F_{{obs}}$ = {f_obs:.1f} (p = {p_value:.5f})')
axes[1].set_xlabel('$F$', fontsize=12)
axes[1].set_ylabel('Probability Density', fontsize=12)
axes[1].set_title('One-way ANOVA: Fertilizer Example', 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()
群ごとのデータ分布と箱ひげ図
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
# より多いデータで ANOVA を実施
np.random.seed(42)
group_a = np.random.normal(50, 5, 30)
group_b = np.random.normal(55, 5, 30)
group_c = np.random.normal(52, 5, 30)
group_d = np.random.normal(48, 5, 30)
f_stat, p_value = stats.f_oneway(group_a, group_b, group_c, group_d)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左: 箱ひげ図
data = [group_a, group_b, group_c, group_d]
labels = ['Group A', 'Group B', 'Group C', 'Group D']
bp = axes[0].boxplot(data, labels=labels, patch_artist=True,
boxprops=dict(facecolor='lightblue', edgecolor='black'))
axes[0].set_ylabel('Value', fontsize=12)
axes[0].set_title(f'Box Plot (F = {f_stat:.2f}, p = {p_value:.4f})', fontsize=13)
axes[0].grid(True, alpha=0.3, axis='y')
# 各群の平均をプロット
means = [np.mean(g) for g in data]
axes[0].scatter(range(1, 5), means, color='red', zorder=5, s=80,
label='Mean', marker='D')
axes[0].legend(fontsize=11)
# 右: 各群の分布
x_range = np.linspace(30, 70, 200)
colors = ['blue', 'red', 'green', 'orange']
for g, label, color in zip(data, labels, colors):
mu, sigma = np.mean(g), np.std(g, ddof=1)
axes[1].plot(x_range, stats.norm.pdf(x_range, mu, sigma),
color=color, linewidth=2, label=f'{label} ($\\bar{{x}}$={mu:.1f})')
axes[1].set_xlabel('Value', fontsize=12)
axes[1].set_ylabel('Density', fontsize=12)
axes[1].set_title('Estimated Distributions', fontsize=13)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"F = {f_stat:.4f}, p = {p_value:.6f}")
print(f"α = 0.05 での判定: {'棄却(群間に差あり)' if p_value < 0.05 else '棄却しない'}")
まとめ
本記事では、F検定と分散分析(ANOVA)について解説しました。
- F分布: 2つの独立なカイ二乗変量の比 $F = (U/d_1)/(V/d_2)$ が従う分布
- 等分散のF検定: $F = s_1^2/s_2^2 \sim F_{n_1-1, n_2-1}$。2群の分散の等しさを検定
- 一元配置ANOVA: 全変動を群間(SSB)と群内(SSW)に分解し、$F = \text{MSB}/\text{MSW}$ で検定
- t検定の繰り返しは不可: 多重比較により第1種の過誤が膨らむ。ANOVAはこの問題を回避
- F統計量の意味: $F \approx 1$ なら群間差なし、$F \gg 1$ なら群間差あり
次のステップとして、以下の記事も参考にしてください。