ある植物の成長に影響する要因として「肥料の種類(A, B, C)」と「日照条件(多い/少ない)」の2つを調べたいとします。一元配置ANOVAでは1つの要因しか扱えませんが、2つの要因を同時に調べたい場合はどうすればよいでしょうか。
さらに重要なのは、肥料の効果が日照条件によって変わる可能性があることです。たとえば、肥料Aは日照が多いときに効果的だが、日照が少ないときは効果がない — このような「要因間の相乗効果(または打ち消し効果)」を交互作用(interaction)と呼びます。
2つの要因の主効果と交互作用を同時に検定する手法が二元配置分散分析(two-way ANOVA)です。
二元配置ANOVAを理解すると、以下のような分析が可能になります。
- 実験計画法: 複数の要因の効果を効率的に調べる
- 医学: 薬剤の種類と投与量の組み合わせ効果
- 工学: 温度と圧力の組み合わせが品質に与える影響
- マーケティング: 広告媒体とメッセージの組み合わせ効果
本記事の内容
- 二元配置ANOVAのモデル
- 変動の分解(主効果 + 交互作用)
- 交互作用の解釈とプロット
- 繰り返しあり/なしの違い
- Pythonでの実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
二元配置ANOVAのモデル
問題設定
2つの要因A($a$ 水準)とB($b$ 水準)があり、各セル $(i, j)$ に $n$ 個の繰り返し観測があります。
$$ \begin{equation} X_{ijk} = \mu + \alpha_i + \beta_j + (\alpha\beta)_{ij} + \varepsilon_{ijk} \end{equation} $$
ここで – $\mu$: 全体平均 – $\alpha_i$: 要因Aの第 $i$ 水準の主効果($\sum \alpha_i = 0$) – $\beta_j$: 要因Bの第 $j$ 水準の主効果($\sum \beta_j = 0$) – $(\alpha\beta)_{ij}$: AとBの交互作用効果 – $\varepsilon_{ijk} \sim N(0, \sigma^2)$: 誤差項
3つの帰無仮説
二元配置ANOVAでは同時に3つの仮説を検定します。
- Aの主効果: $H_0^A: \alpha_1 = \cdots = \alpha_a = 0$
- Bの主効果: $H_0^B: \beta_1 = \cdots = \beta_b = 0$
- 交互作用: $H_0^{AB}: (\alpha\beta)_{ij} = 0 \text{ for all } i, j$
交互作用とは
交互作用は「一方の要因の効果が、もう一方の要因の水準によって変わること」です。
交互作用がない場合、要因Aの効果は要因Bの水準によらず一定です。
$$ \mu_{ij} – \mu_{ij’} = \beta_j – \beta_{j’} \quad \text{(Aの水準によらない)} $$
交互作用がある場合、この差がAの水準によって変わります。
日常的な例: 「薬Aはアルコールなしでは有効だが、アルコールありでは逆効果」— これは薬とアルコールの交互作用です。
この直感を理解した上で、変動の分解を見ていきましょう。
変動の分解
平方和の分解
全変動は4つの成分に分解されます。
$$ \begin{equation} SST = SS_A + SS_B + SS_{AB} + SS_E \end{equation} $$
$$ SS_A = bn\sum_{i=1}^a (\bar{X}_{i\cdot\cdot} – \bar{X}_{\cdot\cdot\cdot})^2 $$
$$ SS_B = an\sum_{j=1}^b (\bar{X}_{\cdot j\cdot} – \bar{X}_{\cdot\cdot\cdot})^2 $$
$$ SS_{AB} = n\sum_{i=1}^a\sum_{j=1}^b (\bar{X}_{ij\cdot} – \bar{X}_{i\cdot\cdot} – \bar{X}_{\cdot j\cdot} + \bar{X}_{\cdot\cdot\cdot})^2 $$
$$ SS_E = \sum_{i,j,k}(X_{ijk} – \bar{X}_{ij\cdot})^2 $$
ANOVA表
| 変動源 | SS | df | MS | F |
|---|---|---|---|---|
| 要因A | $SS_A$ | $a-1$ | $MS_A$ | $F_A = MS_A/MS_E$ |
| 要因B | $SS_B$ | $b-1$ | $MS_B$ | $F_B = MS_B/MS_E$ |
| 交互作用AB | $SS_{AB}$ | $(a-1)(b-1)$ | $MS_{AB}$ | $F_{AB} = MS_{AB}/MS_E$ |
| 誤差 | $SS_E$ | $ab(n-1)$ | $MS_E$ | |
| 全体 | $SST$ | $abn-1$ |
各F統計量はそれぞれ $F$ 分布に従い、独立に検定できます。
解釈の順序
結果の解釈は交互作用から始めるのが原則です。
- まず交互作用 $F_{AB}$ を見る
- 交互作用が有意なら: 主効果の解釈は注意が必要(単純主効果の分析を行う)
- 交互作用が有意でなければ: 各主効果を独立に解釈できる
交互作用が有意なとき、「肥料Bが全体的に良い」という主効果の解釈は誤解を招く可能性があります。日照条件によって肥料の順位が入れ替わるかもしれないからです。
次に、交互作用の有無を視覚的に判断する方法を見ていきましょう。
交互作用プロットの解釈
交互作用プロット(interaction plot)は、一方の要因を横軸、セル平均を縦軸、もう一方の要因を線の色で区別したグラフです。
- 線が平行 → 交互作用がない(各要因の効果は独立)
- 線が交差または非平行 → 交互作用がある(要因間の相乗/打消し効果)
Pythonで二元配置ANOVAを実装し、交互作用プロットを作成しましょう。
Pythonでの実装と可視化
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
# 二元配置のデータ生成
# 要因A: 肥料(3水準)、要因B: 日照(2水準)
a_levels = ['Fertilizer A', 'Fertilizer B', 'Fertilizer C']
b_levels = ['Low light', 'High light']
n_rep = 10 # 各セルの繰り返し数
# セル平均(交互作用あり)
cell_means = {
('Fertilizer A', 'Low light'): 85,
('Fertilizer A', 'High light'): 95,
('Fertilizer B', 'Low light'): 100,
('Fertilizer B', 'High light'): 105,
('Fertilizer C', 'Low light'): 90,
('Fertilizer C', 'High light'): 110, # Cは高日照で特に効果的
}
sigma = 6
# データ生成
data = {}
all_values = []
all_a = []
all_b = []
for a in a_levels:
for b in b_levels:
values = np.random.normal(cell_means[(a, b)], sigma, n_rep)
data[(a, b)] = values
all_values.extend(values)
all_a.extend([a] * n_rep)
all_b.extend([b] * n_rep)
all_values = np.array(all_values)
# 手動で二元配置ANOVA
grand_mean = np.mean(all_values)
N = len(all_values)
a, b, n = len(a_levels), len(b_levels), n_rep
# 各平均
a_means = {level: np.mean([data[(level, bl)] for bl in b_levels]) for level in a_levels}
b_means = {level: np.mean([data[(al, level)] for al in a_levels]) for level in b_levels}
cell_mean_obs = {(al, bl): np.mean(data[(al, bl)]) for al in a_levels for bl in b_levels}
# 平方和
SS_A = b * n * sum((a_means[al] - grand_mean)**2 for al in a_levels)
SS_B = a * n * sum((b_means[bl] - grand_mean)**2 for bl in b_levels)
SS_AB = n * sum((cell_mean_obs[(al,bl)] - a_means[al] - b_means[bl] + grand_mean)**2
for al in a_levels for bl in b_levels)
SS_E = sum(np.sum((data[(al,bl)] - cell_mean_obs[(al,bl)])**2)
for al in a_levels for bl in b_levels)
SST = np.sum((all_values - grand_mean)**2)
# 自由度
df_A = a - 1
df_B = b - 1
df_AB = (a-1)*(b-1)
df_E = a*b*(n-1)
MS_A = SS_A / df_A
MS_B = SS_B / df_B
MS_AB = SS_AB / df_AB
MS_E = SS_E / df_E
F_A = MS_A / MS_E
F_B = MS_B / MS_E
F_AB = MS_AB / MS_E
p_A = 1 - stats.f.cdf(F_A, df_A, df_E)
p_B = 1 - stats.f.cdf(F_B, df_B, df_E)
p_AB = 1 - stats.f.cdf(F_AB, df_AB, df_E)
print("Two-way ANOVA Table:")
print(f"{'Source':<15s} {'SS':>10s} {'df':>5s} {'MS':>10s} {'F':>8s} {'p':>8s}")
print("-" * 60)
print(f"{'Factor A':<15s} {SS_A:10.2f} {df_A:5d} {MS_A:10.2f} {F_A:8.3f} {p_A:8.4f}")
print(f"{'Factor B':<15s} {SS_B:10.2f} {df_B:5d} {MS_B:10.2f} {F_B:8.3f} {p_B:8.4f}")
print(f"{'A x B':<15s} {SS_AB:10.2f} {df_AB:5d} {MS_AB:10.2f} {F_AB:8.3f} {p_AB:8.4f}")
print(f"{'Error':<15s} {SS_E:10.2f} {df_E:5d} {MS_E:10.2f}")
print(f"{'Total':<15s} {SST:10.2f} {N-1:5d}")
fig, axes = plt.subplots(1, 3, figsize=(16, 5.5))
# (a) 交互作用プロット
ax = axes[0]
x_pos = np.arange(len(a_levels))
for j, bl in enumerate(b_levels):
means = [cell_mean_obs[(al, bl)] for al in a_levels]
sems = [np.std(data[(al, bl)], ddof=1) / np.sqrt(n) for al in a_levels]
color = ['#2196F3', '#FF9800'][j]
ax.errorbar(x_pos, means, yerr=sems, marker='o', linewidth=2,
markersize=8, capsize=5, color=color, label=bl)
ax.set_xticks(x_pos)
ax.set_xticklabels(a_levels, fontsize=10)
ax.set_ylabel('Yield (kg)', fontsize=12)
ax.set_title('Interaction Plot', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# (b) 変動の分解
ax = axes[1]
ss_values = [SS_A, SS_B, SS_AB, SS_E]
ss_labels = ['$SS_A$\n(Fertilizer)', '$SS_B$\n(Light)', '$SS_{AB}$\n(Interaction)', '$SS_E$\n(Error)']
colors_bar = ['#4CAF50', '#2196F3', '#FF5722', '#9E9E9E']
bars = ax.bar(ss_labels, ss_values, color=colors_bar, alpha=0.7, edgecolor='gray')
for bar, val, p in zip(bars, ss_values, [p_A, p_B, p_AB, None]):
sig = ''
if p is not None:
sig = ' ***' if p < 0.001 else ' **' if p < 0.01 else ' *' if p < 0.05 else ' n.s.'
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 30,
f'{val:.0f}{sig}', ha='center', fontsize=9)
ax.set_ylabel('Sum of Squares', fontsize=12)
ax.set_title('Decomposition of Variance', fontsize=13)
ax.grid(True, alpha=0.3, axis='y')
# (c) 交互作用ありvs なしの比較
ax = axes[2]
# 交互作用なしのデータ
cell_means_no_int = {
('A', 'Low'): 85, ('A', 'High'): 95,
('B', 'Low'): 95, ('B', 'High'): 105,
('C', 'Low'): 90, ('C', 'High'): 100,
}
# 交互作用ありのデータ(元のまま)
cell_means_int = {
('A', 'Low'): 85, ('A', 'High'): 95,
('B', 'Low'): 100, ('B', 'High'): 105,
('C', 'Low'): 90, ('C', 'High'): 110,
}
for data_dict, title, style in [(cell_means_no_int, 'No interaction', '--'),
(cell_means_int, 'With interaction', '-')]:
for j, (bl, color) in enumerate(zip(['Low', 'High'], ['#2196F3', '#FF9800'])):
means = [data_dict[(al, bl)] for al in ['A', 'B', 'C']]
ax.plot([0, 1, 2], means, style, color=color, linewidth=2, alpha=0.7,
marker='o', markersize=6,
label=f'{bl} light ({title})' if j == 0 or style == '-' else None)
ax.set_xticks([0, 1, 2])
ax.set_xticklabels(['A', 'B', 'C'])
ax.set_xlabel('Fertilizer', fontsize=12)
ax.set_ylabel('Mean Yield', fontsize=12)
ax.set_title('Parallel (no int.) vs Non-parallel (int.)', fontsize=12)
ax.legend(fontsize=7, loc='upper left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('anova_two_way.png', dpi=150, bbox_inches='tight')
plt.show()
このグラフから二元配置ANOVAの結果と交互作用が読み取れます。
-
左図(交互作用プロット): 2つの線が平行でなく、特に肥料Cで日照条件による差が大きいことがわかります。肥料Cは高日照で特に効果的であり、これが交互作用の原因です。エラーバーは標準誤差を表しています
-
中央図(変動の分解): 各変動源の大きさとその統計的有意性を示しています。要因A(肥料)と要因B(日照)の主効果はともに有意であり、交互作用も有意です
-
右図(平行vs非平行): 交互作用がない場合(破線)は2つの線が平行であり、交互作用がある場合(実線)は非平行です。非平行パターンは「ある肥料の効果が日照条件に依存する」ことを示しています
まとめ
本記事では、二元配置ANOVAの理論と実装を解説しました。
- 二元配置ANOVAは全変動を主効果A、主効果B、交互作用AB、誤差の4つに分解する
- 交互作用は「一方の要因の効果が他方の要因の水準によって変わること」を表し、交互作用プロットで視覚的に判断できる
- 結果の解釈は交互作用から始めるのが原則。交互作用が有意なら、主効果の解釈には注意が必要
- 繰り返しなしの場合は交互作用を検定できず、加法的モデルを仮定する必要がある
次のステップとして、以下の記事も参考にしてください。
- ノンパラメトリック検定 — 正規性が仮定できない場合
- 多重検定問題 — 複数の比較の補正
- 効果量と検出力 — 実験計画のためのサンプルサイズ設計