分散分析(Analysis of Variance, ANOVA)は、3つ以上の群の平均に差があるかどうかを検定する手法です。t検定が2群の比較に使われるのに対し、ANOVAは複数群を一度に比較できます。
例えば、3種類の肥料を使った植物の成長差を比較する、4つの異なる教授法の効果を比較するなど、実験研究で頻繁に使われます。
本記事の内容
- 一元配置分散分析の原理
- 群間変動と群内変動の分解
- F検定の仕組み
- 多重比較法
- Pythonでの実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
なぜt検定を繰り返してはいけないのか
3群 A, B, C の平均を比較したいとき、t検定を3回(A対B、A対C、B対C)行うことが考えられます。しかし、これには多重検定問題があります。
各検定の有意水準を $\alpha = 0.05$ とすると、すべてで正しく帰無仮説を棄却しない確率は $(1-0.05)^3 = 0.857$ であり、少なくとも1回偽陽性が出る確率は $1 – 0.857 = 0.143$ に膨れ上がります。群の数が増えるとこの問題はさらに深刻になります。
ANOVAは、1回の検定で複数群の平均を同時に比較することで、この問題を回避します。
一元配置分散分析
設定
$k$ 個の群があり、第 $i$ 群から $n_i$ 個のデータを観測します。第 $i$ 群の $j$ 番目の観測値を $X_{ij}$ とします。
モデル:
$$ X_{ij} = \mu_i + \varepsilon_{ij}, \quad \varepsilon_{ij} \sim N(0, \sigma^2) $$
帰無仮説: $H_0: \mu_1 = \mu_2 = \cdots = \mu_k$
対立仮説: $H_1:$ 少なくとも1組の $\mu_i \neq \mu_j$ が存在する
仮定
- 各群が正規分布に従う
- 各群の分散が等しい(等分散性)
- 観測が独立
記号の定義
- 全データ数: $N = \sum_{i=1}^{k} n_i$
- 第 $i$ 群の平均: $\bar{X}_{i\cdot} = \frac{1}{n_i}\sum_{j=1}^{n_i} X_{ij}$
- 全体平均: $\bar{X}_{\cdot\cdot} = \frac{1}{N}\sum_{i=1}^{k}\sum_{j=1}^{n_i} X_{ij}$
変動の分解
ANOVAの核心は、データの全変動を群間変動と群内変動に分解することです。
全平方和(SST)
$$ \text{SST} = \sum_{i=1}^{k}\sum_{j=1}^{n_i} (X_{ij} – \bar{X}_{\cdot\cdot})^2 $$
群間平方和(SSB)
$$ \text{SSB} = \sum_{i=1}^{k} n_i (\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot})^2 $$
群間変動は「各群の平均が全体平均からどれだけずれているか」を測ります。
群内平方和(SSW)
$$ \text{SSW} = \sum_{i=1}^{k}\sum_{j=1}^{n_i} (X_{ij} – \bar{X}_{i\cdot})^2 $$
群内変動は「各群内のデータのばらつき」を測ります。
分解公式
$$ \boxed{\text{SST} = \text{SSB} + \text{SSW}} $$
全変動 = 群間変動 + 群内変動 です。
導出
$$ \begin{align} X_{ij} – \bar{X}_{\cdot\cdot} &= (X_{ij} – \bar{X}_{i\cdot}) + (\bar{X}_{i\cdot} – \bar{X}_{\cdot\cdot}) \end{align} $$
両辺を2乗して総和をとり、交差項が0になることを利用すると分解公式が得られます。
F検定
平均平方
$$ \text{MSB} = \frac{\text{SSB}}{k – 1}, \quad \text{MSW} = \frac{\text{SSW}}{N – k} $$
F統計量
$$ F = \frac{\text{MSB}}{\text{MSW}} $$
$H_0$ のもとで $F \sim F(k-1, N-k)$(F分布)に従います。
直感的な理解
F統計量は「群間の変動と群内の変動の比」です。
- 群の平均が大きく異なる → SSBが大きい → Fが大きい → $H_0$ を棄却
- 群の平均がほぼ同じ → SSBが小さい → Fが1に近い → $H_0$ を棄却できない
ANOVA表
| 変動要因 | 平方和 | 自由度 | 平均平方 | F値 |
|---|---|---|---|---|
| 群間 | SSB | $k-1$ | MSB | $F = \text{MSB}/\text{MSW}$ |
| 群内 | SSW | $N-k$ | MSW | |
| 全体 | SST | $N-1$ |
多重比較法
ANOVAで帰無仮説が棄却された場合、どの群間に差があるかを特定するために多重比較(post-hoc test)を行います。
テューキーのHSD法
すべてのペアワイズ比較を同時に行い、ファミリーワイズエラー率を制御します。
$$ q = \frac{\bar{X}_{i\cdot} – \bar{X}_{j\cdot}}{\sqrt{\text{MSW}/n}} $$
ここでスチューデント化範囲分布の臨界値と比較します。
ボンフェローニ法
$k(k-1)/2$ 組のt検定を行い、有意水準を $\alpha / [k(k-1)/2]$ に補正します。
Pythonでの実装
一元配置ANOVAの手計算
import numpy as np
from scipy import stats
np.random.seed(42)
# 3群のデータ(肥料A, B, Cの効果)
group_a = np.array([23, 25, 27, 22, 26, 28])
group_b = np.array([30, 32, 29, 31, 33, 28])
group_c = np.array([25, 27, 26, 24, 28, 23])
groups = [group_a, group_b, group_c]
k = len(groups)
N = sum(len(g) for g in groups)
# 各群の平均
means = [np.mean(g) for g in groups]
grand_mean = np.mean(np.concatenate(groups))
print(f"Group A mean: {means[0]:.2f}")
print(f"Group B mean: {means[1]:.2f}")
print(f"Group C mean: {means[2]:.2f}")
print(f"Grand mean: {grand_mean:.2f}")
# 平方和の計算
SSB = sum(len(g) * (m - grand_mean)**2 for g, m in zip(groups, means))
SSW = sum(np.sum((g - m)**2) for g, m in zip(groups, means))
SST = np.sum((np.concatenate(groups) - grand_mean)**2)
print(f"\nSSB = {SSB:.2f}")
print(f"SSW = {SSW:.2f}")
print(f"SST = {SST:.2f}")
print(f"SSB + SSW = {SSB + SSW:.2f} (= SST)")
# F統計量
df_between = k - 1
df_within = N - k
MSB = SSB / df_between
MSW = SSW / df_within
F_stat = MSB / MSW
p_value = 1 - stats.f.cdf(F_stat, df_between, df_within)
print(f"\nMSB = {MSB:.2f}")
print(f"MSW = {MSW:.2f}")
print(f"F = {F_stat:.4f}")
print(f"p-value = {p_value:.4f}")
# SciPyによる検証
F_scipy, p_scipy = stats.f_oneway(group_a, group_b, group_c)
print(f"\nSciPy F = {F_scipy:.4f}")
print(f"SciPy p = {p_scipy:.4f}")
変動の分解の可視化
import numpy as np
import matplotlib.pyplot as plt
group_a = np.array([23, 25, 27, 22, 26, 28])
group_b = np.array([30, 32, 29, 31, 33, 28])
group_c = np.array([25, 27, 26, 24, 28, 23])
groups = [group_a, group_b, group_c]
labels = ['A', 'B', 'C']
colors = ['steelblue', 'orange', 'green']
grand_mean = np.mean(np.concatenate(groups))
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# (1) データのプロット
ax = axes[0]
for i, (g, label, color) in enumerate(zip(groups, labels, colors)):
ax.scatter([i] * len(g), g, color=color, s=80, zorder=5, label=f'Group {label}')
ax.hlines(np.mean(g), i - 0.3, i + 0.3, colors=color, linewidth=2)
ax.axhline(y=grand_mean, color='red', linestyle='--', linewidth=2, label='Grand mean')
ax.set_xticks([0, 1, 2])
ax.set_xticklabels(labels)
ax.set_ylabel('Value')
ax.set_title('Data and group means')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
# (2) 群間変動
ax = axes[1]
for i, (g, label, color) in enumerate(zip(groups, labels, colors)):
group_mean = np.mean(g)
ax.bar(i, len(g) * (group_mean - grand_mean)**2, color=color, alpha=0.7)
ax.set_xticks([0, 1, 2])
ax.set_xticklabels(labels)
ax.set_ylabel('Contribution to SSB')
ax.set_title(f'Between-group variation (SSB = {sum(len(g)*(np.mean(g)-grand_mean)**2 for g in groups):.1f})')
ax.grid(True, alpha=0.3)
# (3) 群内変動
ax = axes[2]
for i, (g, label, color) in enumerate(zip(groups, labels, colors)):
ax.bar(i, np.sum((g - np.mean(g))**2), color=color, alpha=0.7)
ax.set_xticks([0, 1, 2])
ax.set_xticklabels(labels)
ax.set_ylabel('Contribution to SSW')
ax.set_title(f'Within-group variation (SSW = {sum(np.sum((g-np.mean(g))**2) for g in groups):.1f})')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('anova_decomposition.png', dpi=150, bbox_inches='tight')
plt.show()
F分布の可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 6, 500)
params = [(2, 10), (5, 10), (10, 10), (5, 30)]
for df1, df2 in params:
ax.plot(x, stats.f.pdf(x, df1, df2), linewidth=2, label=f'$F({df1}, {df2})$')
ax.set_xlabel('$x$')
ax.set_ylabel('Density')
ax.set_title('F-distribution')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1)
plt.tight_layout()
plt.savefig('f_distribution.png', dpi=150, bbox_inches='tight')
plt.show()
テューキーのHSD法
import numpy as np
from scipy import stats
from itertools import combinations
def tukey_hsd(groups, labels, alpha=0.05):
"""テューキーのHSD法による多重比較"""
k = len(groups)
N = sum(len(g) for g in groups)
SSW = sum(np.sum((g - np.mean(g))**2) for g in groups)
MSW = SSW / (N - k)
print("Tukey's HSD pairwise comparisons:")
print("-" * 50)
for (i, gi), (j, gj) in combinations(enumerate(groups), 2):
mean_diff = abs(np.mean(gi) - np.mean(gj))
se = np.sqrt(MSW * (1/len(gi) + 1/len(gj)) / 2)
q = mean_diff / se
# statsmodelsを使わない場合の近似
# スチューデント化範囲分布の代わりにt分布で近似
df = N - k
t_val = mean_diff / np.sqrt(MSW * (1/len(gi) + 1/len(gj)))
p_val = 2 * (1 - stats.t.cdf(abs(t_val), df)) * k * (k-1) / 2 # ボンフェローニ補正
p_val = min(p_val, 1.0)
sig = "*" if p_val < alpha else "n.s."
print(f"{labels[i]} vs {labels[j]}: diff = {mean_diff:.2f}, p = {p_val:.4f} {sig}")
group_a = np.array([23, 25, 27, 22, 26, 28])
group_b = np.array([30, 32, 29, 31, 33, 28])
group_c = np.array([25, 27, 26, 24, 28, 23])
tukey_hsd([group_a, group_b, group_c], ['A', 'B', 'C'])
まとめ
本記事では、分散分析(ANOVA)の理論と実装を解説しました。
- ANOVA: 3群以上の平均の差を1回の検定で判定する手法
- 変動の分解: $\text{SST} = \text{SSB} + \text{SSW}$(全変動 = 群間変動 + 群内変動)
- F検定: $F = \text{MSB}/\text{MSW}$ でF分布に基づく検定を行う
- 多重比較: ANOVAで有意差が出た後、テューキーのHSD法などでペアワイズ比較
- t検定の繰り返しではなくANOVAを使う理由は多重検定問題の回避
ANOVAは実験計画法や品質管理の基礎となる重要な手法です。二元配置分散分析(交互作用の分析)へと発展することもできます。