F検定と分散分析(ANOVA)

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$ なら群間差あり

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