3種類の肥料A, B, Cの効果を比較したいとします。各肥料を10区画の畑に施し、収穫量を測定しました。A群の平均は95kg、B群は102kg、C群は98kgです。
t検定を使えば2群ずつ比較できますが、A vs B, A vs C, B vs Cの3回の検定が必要になり、全体の第一種の過誤率が $1 – (1-0.05)^3 = 0.143$ に膨張してしまいます。
3群以上の平均を一度に比較する方法が分散分析(Analysis of Variance, ANOVA)です。ANOVAはフィッシャーが1920年代に農業実験のために開発した手法で、「群間のばらつき」と「群内のばらつき」の比較に基づいています。
ANOVAを理解すると、以下のような場面で適切な比較ができます。
- 農学・工学: 複数の処理条件の効果の比較
- 医学: 3種類以上の治療法の有効性の比較
- 心理学: 複数の実験条件間の反応の比較
- 品質管理: 複数の製造ラインの品質の比較
本記事の内容
- 一元配置ANOVAの数理モデル
- 変動の分解(群間変動と群内変動)
- F統計量の導出と分布
- ANOVAの仮定と診断
- 多重比較法(事後検定)
- Pythonでの実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
一元配置ANOVAのモデル
問題設定
$k$ 個のグループがあり、第 $i$ 群に $n_i$ 個の観測値があります($i = 1, \ldots, k$)。全観測数は $N = \sum_{i=1}^k n_i$ です。
効果モデル:
$$ \begin{equation} X_{ij} = \mu + \alpha_i + \varepsilon_{ij}, \quad \varepsilon_{ij} \sim N(0, \sigma^2) \end{equation} $$
ここで $\mu$ は全体平均、$\alpha_i$ は第 $i$ 群の効果($\sum n_i \alpha_i = 0$ の制約)、$\varepsilon_{ij}$ は誤差項です。
仮説
$$ H_0: \alpha_1 = \alpha_2 = \cdots = \alpha_k = 0 \quad (\text{全群の平均が等しい}) $$
$$ H_1: \text{少なくとも1つの } \alpha_i \neq 0 $$
$H_0$ は $\mu_1 = \mu_2 = \cdots = \mu_k$ と等価です。
「3群以上の平均が等しいか」を検定するために、なぜ「分散」を分析するのでしょうか。次にその直感を説明します。
ANOVAの直感
群間の平均に差があるかどうかを、以下の2つの変動の比較で判断します。
- 群間変動(between-group variation): 各群の平均が全体平均からどれだけ散らばっているか → 群の効果を反映
- 群内変動(within-group variation): 各観測値が各群の平均からどれだけ散らばっているか → 純粋な誤差を反映
群間変動が群内変動に比べて十分大きければ、群間の差は偶然では説明できないと結論します。
この直感を数式で表現しましょう。
変動の分解
平方和の分解
全変動(Total Sum of Squares, SST)は群間変動(Between-group SS, SSB)と群内変動(Within-group SS, SSW)に分解されます。
$$ \begin{equation} \underbrace{\sum_{i=1}^k \sum_{j=1}^{n_i} (X_{ij} – \bar{X}_{\cdot\cdot})^2}_{SST} = \underbrace{\sum_{i=1}^k n_i(\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot})^2}_{SSB} + \underbrace{\sum_{i=1}^k \sum_{j=1}^{n_i} (X_{ij} – \bar{X}_{i\cdot})^2}_{SSW} \end{equation} $$
ここで $\bar{X}_{i\cdot}$ は第 $i$ 群の平均、$\bar{X}_{\cdot\cdot}$ は全体平均です。
この分解を導出します。$X_{ij} – \bar{X}_{\cdot\cdot} = (X_{ij} – \bar{X}_{i\cdot}) + (\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot})$ として二乗し和を取ると、交差項は
$$ 2\sum_i \sum_j (X_{ij} – \bar{X}_{i\cdot})(\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot}) = 2\sum_i (\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot})\underbrace{\sum_j (X_{ij} – \bar{X}_{i\cdot})}_{= 0} = 0 $$
で消えるため、$SST = SSB + SSW$ が成り立ちます。
自由度
| 変動源 | 平方和 | 自由度 | 平均平方 |
|---|---|---|---|
| 群間 | SSB | $k – 1$ | MSB = SSB / $(k-1)$ |
| 群内 | SSW | $N – k$ | MSW = SSW / $(N-k)$ |
| 全体 | SST | $N – 1$ |
平均平方(Mean Square)は平方和を自由度で割ったものです。
F統計量
$$ \begin{equation} F = \frac{MSB}{MSW} = \frac{SSB/(k-1)}{SSW/(N-k)} \end{equation} $$
$H_0$ のもとで $F \sim F_{k-1, N-k}$(F分布)に従います。
$H_0$ のもとでは MSB も MSW も $\sigma^2$ の不偏推定量であり、$F \approx 1$ になります。$H_1$ のもとでは MSB が $\sigma^2$ より大きくなる(群間の差が加わる)ため、$F > 1$ になります。
ANOVAの仮定
- 正規性: 各群の母集団が正規分布に従う
- 等分散性: 各群の母分散が等しい($\sigma_1^2 = \cdots = \sigma_k^2$)
- 独立性: 観測値が互いに独立
等分散性の仮定が破れている場合はウェルチのANOVAを使います。
F検定の後、「どの群間に差があるか」を特定するために多重比較法が必要です。
多重比較法
なぜ多重比較が必要か
ANOVAで $H_0$ が棄却された場合、「少なくとも1組の群間に差がある」ことはわかりますが、「どの群間に差があるか」はわかりません。
t検定を全ペアに行うと多重検定問題が生じます。$k$ 群の場合、$\binom{k}{2}$ 回の検定が必要であり、実験全体の第一種の過誤率が膨張します。
テューキーのHSD法
最もよく使われる事後検定がテューキーのHSD(Honestly Significant Difference)法です。
全ペアの平均差 $|\bar{X}_{i\cdot} – \bar{X}_{j\cdot}|$ に対して、次の閾値を超えるかどうかで判定します。
$$ \text{HSD} = q_{\alpha, k, N-k} \cdot \sqrt{\frac{MSW}{n}} $$
ここで $q$ はスチューデント化された範囲分布(studentized range distribution)の分位点です。
テューキーのHSD法は実験全体の第一種の過誤率を $\alpha$ に制御します。
Pythonで一元配置ANOVAと多重比較を実装しましょう。
Pythonでの実装と可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
# 3群のデータ生成
n_per_group = 20
mu_A, mu_B, mu_C = 95, 102, 98
sigma = 8
group_A = np.random.normal(mu_A, sigma, n_per_group)
group_B = np.random.normal(mu_B, sigma, n_per_group)
group_C = np.random.normal(mu_C, sigma, n_per_group)
# 一元配置ANOVA
F_stat, p_value = stats.f_oneway(group_A, group_B, group_C)
# 手動計算
all_data = np.concatenate([group_A, group_B, group_C])
grand_mean = np.mean(all_data)
group_means = [np.mean(group_A), np.mean(group_B), np.mean(group_C)]
N = len(all_data)
k = 3
SSB = sum(n_per_group * (m - grand_mean)**2 for m in group_means)
SSW = sum(np.sum((g - m)**2) for g, m in zip([group_A, group_B, group_C], group_means))
SST = np.sum((all_data - grand_mean)**2)
MSB = SSB / (k - 1)
MSW = SSW / (N - k)
F_manual = MSB / MSW
p_manual = 1 - stats.f.cdf(F_manual, k-1, N-k)
print(f"ANOVA結果:")
print(f" SSB = {SSB:.2f}, SSW = {SSW:.2f}, SST = {SST:.2f}")
print(f" MSB = {MSB:.2f}, MSW = {MSW:.2f}")
print(f" F = {F_manual:.4f} (scipy: {F_stat:.4f})")
print(f" p = {p_manual:.4f} (scipy: {p_value:.4f})")
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# (a) 箱ひげ図とANOVA結果
ax = axes[0]
bp = ax.boxplot([group_A, group_B, group_C],
labels=['Fertilizer A', 'Fertilizer B', 'Fertilizer C'],
patch_artist=True)
colors_bp = ['#4CAF50', '#2196F3', '#FF9800']
for patch, color in zip(bp['boxes'], colors_bp):
patch.set_facecolor(color)
patch.set_alpha(0.5)
# 群平均を表示
for i, (label, mean) in enumerate(zip(['A', 'B', 'C'], group_means)):
ax.plot(i+1, mean, 'D', color='red', markersize=8)
ax.axhline(grand_mean, color='gray', linewidth=1.5, linestyle='--',
label=f'Grand mean = {grand_mean:.1f}')
ax.set_ylabel('Yield (kg)', fontsize=12)
sig = '***' if p_value < 0.001 else '**' if p_value < 0.01 else '*' if p_value < 0.05 else 'n.s.'
ax.set_title(f'One-way ANOVA\nF({k-1},{N-k}) = {F_stat:.2f}, p = {p_value:.4f} ({sig})', fontsize=12)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3, axis='y')
# (b) 変動の分解
ax = axes[1]
categories = ['SSB\n(Between)', 'SSW\n(Within)', 'SST\n(Total)']
values = [SSB, SSW, SST]
colors_bar = ['#E91E63', '#03A9F4', '#9E9E9E']
bars = ax.bar(categories, values, color=colors_bar, alpha=0.7, edgecolor='gray')
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 50,
f'{val:.0f}', ha='center', fontsize=11, fontweight='bold')
ax.set_ylabel('Sum of Squares', fontsize=12)
ax.set_title(f'Decomposition: SST = SSB + SSW\n'
f'$\\eta^2 = SSB/SST = {SSB/SST:.3f}$', fontsize=12)
ax.grid(True, alpha=0.3, axis='y')
# (c) F分布上でのp値
ax = axes[2]
df1, df2 = k - 1, N - k
x_f = np.linspace(0, 8, 300)
f_pdf = stats.f.pdf(x_f, df1, df2)
ax.plot(x_f, f_pdf, 'b-', linewidth=2, label=f'$F_{{{df1},{df2}}}$')
ax.fill_between(x_f, f_pdf, where=(x_f >= F_stat), alpha=0.3,
color='red', label=f'p-value = {p_value:.4f}')
ax.axvline(F_stat, color='green', linewidth=2, linestyle='--',
label=f'$F_{{obs}} = {F_stat:.2f}$')
f_crit = stats.f.ppf(0.95, df1, df2)
ax.axvline(f_crit, color='black', linewidth=1.5, linestyle=':',
label=f'Critical value = {f_crit:.2f}')
ax.set_xlabel('F statistic', fontsize=12)
ax.set_ylabel('Density', fontsize=12)
ax.set_title(f'F Distribution (df1={df1}, df2={df2})', fontsize=13)
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('anova_one_way.png', dpi=150, bbox_inches='tight')
plt.show()
# 多重比較(テューキーのHSD)
from scipy.stats import tukey_hsd
result = tukey_hsd(group_A, group_B, group_C)
print(f"\nTukey HSD結果:")
print(result)
このグラフからANOVAの全体像がわかります。
-
左図: 3群の箱ひげ図と群平均(赤いダイヤ)です。肥料Bが最も平均収穫量が高く、肥料Aが最も低いことがわかります。F検定のp値は有意であり、3群の平均が等しいという帰無仮説は棄却されます
-
中央図: 全変動の分解を示しています。SSBは群間の差による変動、SSWは群内の偶然による変動です。$\eta^2 = SSB/SST$ は効果量(決定係数に相当)であり、全変動のうち群の違いで説明できる割合を表しています
-
右図: F分布上でのF統計量の位置とp値です。観測されたF値が臨界値を超えており、帰無仮説が棄却されることが視覚的にわかります
まとめ
本記事では、一元配置ANOVAの理論と実装を解説しました。
- ANOVAは全変動を群間変動(SSB)と群内変動(SSW)に分解し、F統計量 = MSB/MSW で検定する
- $H_0$(全群の平均が等しい)のもとで $F \sim F_{k-1, N-k}$ に従う
- ANOVAの仮定は正規性・等分散性・独立性であり、等分散性の仮定にはウェルチのANOVAで対処できる
- ANOVAで有意な結果が出た後は多重比較法(テューキーのHSDなど)で具体的な群間差を特定する
次のステップとして、以下の記事も参考にしてください。
- 二元配置分散分析 — 2つの要因の効果と交互作用
- 多重検定問題 — 多重比較の補正法
- ノンパラメトリック検定 — 正規性が仮定できない場合の代替