第7回 分散分析
はじめに
本シリーズ「Pythonで学ぶデータ分析実践」の第7回では、分散分析(ANOVA)について解説します。
前回の統計的検定では、2群の平均値の差をt検定で判断する方法を学びました。しかし実務では、3つ以上のグループを同時に比較したい場面が頻繁にあります。例えば「3つの広告パターンのうちどれが最も効果的か」「複数の支店の売上に差があるか」などです。
このような場合にt検定を繰り返し行うと多重検定の問題が発生するため、**分散分析(ANOVA: Analysis of Variance)**という手法を使います。本記事では一元分散分析・二元分散分析の考え方と、多重比較法について実践的に解説します。
シリーズ全体の構成(全12回)
| 回 | テーマ |
|---|---|
| 第1回 | データ分析とは |
| 第2回 | データの前処理 |
| 第3回 | データの把握 |
| 第4回 | 相関分析 |
| 第5回 | 統計的推定 |
| 第6回 | 統計的検定 |
| 第7回 | 分散分析(本記事) |
| 第8回 | 回帰分析 |
| 第9回 | 時系列データ分析 |
| 第10回 | クラス分類 |
| 第11回 | クラスタリング |
| 第12回 | 次元削減 |
7-1. 分散分析とは
**分散分析(ANOVA: Analysis of Variance)**は、3つ以上の群の平均値に統計的に有意な差があるかどうかを検定する手法です。
なぜt検定の繰り返しではダメなのか
3群(A, B, C)の比較をt検定で行うと、A-B、A-C、B-Cの3回検定が必要です。検定回数が増えると第1種の誤り(偽陽性)の確率が膨らみます。
検定1回: 偽陽性率 = 5%
検定3回: 少なくとも1回偽陽性が出る確率 = 1-(0.95)³ ≈ 14.3%
検定10回: 少なくとも1回偽陽性が出る確率 = 1-(0.95)¹⁰ ≈ 40.1%
分散分析は1回の検定で「すべての群の平均が等しいか」を判定するため、この問題を回避できます。
分散分析の考え方
分散分析では、データのばらつき(変動)を以下の2つの要因に分解して考えます。
| 要因 | 説明 | 例 |
|---|---|---|
| 効果(群間変動) | グループの違いによるばらつき | 学習方法の違いが成績に与える影響 |
| 誤差(群内変動) | 同じグループ内の個人差によるばらつき | 同じ学習方法でも個人で成績が異なる |
分散分析の核心: 群間変動が群内変動に比べて十分大きければ、「グループ間に差がある」と判断します。
平方和の分解
データの総変動(ばらつき)は以下のように分解できます。
$$SST = SSB + SSE$$
| 平方和 | 正式名称 | 意味 |
|---|---|---|
| SST | 総平方和(Total Sum of Squares) | データ全体のばらつき |
| SSB | 群間平方和(Between-group Sum of Squares) | グループの違いによるばらつき |
| SSE | 群内平方和(Within-group Sum of Squares / Error) | グループ内の個人差(誤差) |
各平方和の計算式
$$SST = \sum_{i=1}^{k}\sum_{j=1}^{n_i}(X_{ij} - \bar{X})^2$$
$$SSB = \sum_{i=1}^{k} n_i(\bar{X}_i - \bar{X})^2$$
$$SSE = \sum_{i=1}^{k}\sum_{j=1}^{n_i}(X_{ij} - \bar{X}_i)^2$$
- $X_{ij}$: i番目のグループのj番目のデータ
- $\bar{X}$: 全体の平均
- $\bar{X}_i$: i番目のグループの平均
- $k$: グループ数
- $n_i$: i番目のグループのデータ数
F値の計算
F値は「効果の大きさ」と「誤差の大きさ」を比較する指標です。
$$F = \frac{群間分散(MSB)}{群内分散(MSE)} = \frac{SSB / (k-1)}{SSE / (N-k)}$$
- MSB(群間平均平方)= SSB / (k-1) ※k-1: 群間の自由度
- MSE(群内平均平方)= SSE / (N-k) ※N-k: 群内の自由度
- N: 全データ数
| F値の大きさ | 意味 |
|---|---|
| F値が大きい | 群間変動 >> 群内変動 → グループの違いが大きい |
| F値が小さい(≈1) | 群間変動 ≈ 群内変動 → グループの違いは偶然レベル |
F分布
F値はF分布に従います。F分布の形状は2つの自由度(群間の自由度、群内の自由度)によって決まります。
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import japanize_matplotlib
x = np.linspace(0, 5, 200)
plt.figure(figsize=(10, 6))
for dfn, dfd in [(2, 27), (3, 36), (5, 50), (10, 100)]:
y = stats.f.pdf(x, dfn, dfd)
plt.plot(x, y, linewidth=2, label=f'F({dfn}, {dfd})')
plt.xlabel("F値", fontsize=12)
plt.ylabel("確率密度", fontsize=12)
plt.title("F分布(自由度別)", fontsize=14)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.xlim(0, 5)
plt.show()
分散分析の手順まとめ
ステップ1: 仮説を設定する
H₀: すべてのグループの平均は等しい(μ₁ = μ₂ = ... = μₖ)
H₁: 少なくとも1つのグループの平均が異なる
ステップ2: 平方和を計算する(SST, SSB, SSE)
ステップ3: F値を計算する
ステップ4: p値を求め、有意水準と比較して判定する
p < α → 帰無仮説を棄却(グループ間に有意差あり)
p ≥ α → 帰無仮説を棄却できない
ステップ5: 有意差がある場合、多重比較でどのペアに差があるか特定する
7-2. 一元分散分析
**一元分散分析(One-way ANOVA)**は、1つの因子(要因)によるグループ分けでの平均値の差を分析する方法です。
例題
学習方法(通常講義・オンライン学習・グループ学習)が学生の試験成績に与える影響を調べます。各学習方法から無作為に選ばれた学生の試験成績をデータとして使用します。
仮説の設定
- $H_0$: すべての学習方法グループで試験成績の平均は等しい($\mu_1 = \mu_2 = \mu_3$)
- $H_1$: 少なくとも1つの学習方法グループで試験成績の平均が他と異なる
Pythonでの実装
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
import japanize_matplotlib
# データの準備
np.random.seed(42)
lecture = np.random.normal(loc=65, scale=10, size=30) # 通常講義
online = np.random.normal(loc=70, scale=12, size=30) # オンライン学習
group_study = np.random.normal(loc=75, scale=9, size=30) # グループ学習
# DataFrameにまとめる
df = pd.DataFrame({
"score": np.concatenate([lecture, online, group_study]),
"method": ["通常講義"]*30 + ["オンライン"]*30 + ["グループ学習"]*30
})
# 基本統計量の確認
print("=== グループ別基本統計量 ===")
print(df.groupby("method")["score"].agg(["count", "mean", "std"]).round(2))
print(f"\n全体平均: {df['score'].mean():.2f}")
手動計算で理解を深める
# 各グループの平均
mean_all = df["score"].mean() # 全体平均
mean_lecture = lecture.mean() # 通常講義の平均
mean_online = online.mean() # オンラインの平均
mean_group = group_study.mean() # グループ学習の平均
# 平方和の計算
# 総平方和(SST)
sst = np.sum((df["score"] - mean_all) ** 2)
# 群間平方和(SSB)
ssb = (30 * (mean_lecture - mean_all)**2 +
30 * (mean_online - mean_all)**2 +
30 * (mean_group - mean_all)**2)
# 群内平方和(SSE)
sse = (np.sum((lecture - mean_lecture)**2) +
np.sum((online - mean_online)**2) +
np.sum((group_study - mean_group)**2))
# 自由度
df_between = 3 - 1 # k - 1 = 2
df_within = 90 - 3 # N - k = 87
# 平均平方(分散)
msb = ssb / df_between # 群間平均平方
mse = sse / df_within # 群内平均平方
# F値
f_value = msb / mse
# p値
p_value = 1 - stats.f.cdf(f_value, df_between, df_within)
print(f"\n=== 分散分析表(手動計算) ===")
print(f"{'要因':<10} {'平方和':>10} {'自由度':>6} {'平均平方':>10} {'F値':>8} {'p値':>10}")
print("-" * 60)
print(f"{'群間':<10} {ssb:>10.2f} {df_between:>6} {msb:>10.2f} {f_value:>8.4f} {p_value:>10.6f}")
print(f"{'群内':<10} {sse:>10.2f} {df_within:>6} {mse:>10.2f}")
print(f"{'全体':<10} {sst:>10.2f} {89:>6}")
scipy.stats.f_oneway による分析
from scipy import stats
# 一元分散分析の実行
f_stat, p_value = stats.f_oneway(lecture, online, group_study)
print(f"\n=== scipy.stats.f_oneway による一元分散分析 ===")
print(f"F値: {f_stat:.4f}")
print(f"p値: {p_value:.6f}")
alpha = 0.05
if p_value < alpha:
print(f"\n→ p値({p_value:.6f}) < α({alpha})")
print(" 帰無仮説を棄却。学習方法によって試験成績に有意な差がある。")
print(" → 次のステップ: 多重比較でどのペアに差があるか特定する")
else:
print(f"\n→ p値({p_value:.6f}) ≥ α({alpha})")
print(" 帰無仮説を棄却できない。学習方法による差は認められない。")
結果の可視化
import seaborn as sns
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 箱ひげ図
sns.boxplot(x="method", y="score", data=df, ax=axes[0], palette="Set2",
order=["通常講義", "オンライン", "グループ学習"])
axes[0].set_title(f"学習方法別スコア分布\nF={f_stat:.2f}, p={p_value:.4f}", fontsize=12)
axes[0].set_xlabel("学習方法")
axes[0].set_ylabel("試験成績")
axes[0].axhline(df["score"].mean(), color='red', linestyle='--', alpha=0.5, label='全体平均')
axes[0].legend()
# ストリッププロット + 平均値
sns.stripplot(x="method", y="score", data=df, ax=axes[1], palette="Set2",
order=["通常講義", "オンライン", "グループ学習"], alpha=0.6, jitter=True)
# 各グループの平均をプロット
means = df.groupby("method")["score"].mean()
for i, method in enumerate(["通常講義", "オンライン", "グループ学習"]):
axes[1].hlines(means[method], i-0.3, i+0.3, color='red', linewidth=3)
axes[1].set_title("個別データ点と各グループの平均(赤線)", fontsize=12)
axes[1].set_xlabel("学習方法")
axes[1].set_ylabel("試験成績")
plt.tight_layout()
plt.show()
分散分析の前提条件
一元分散分析を正しく適用するためには、以下の前提条件を確認する必要があります。
| 前提条件 | 確認方法 | 違反時の対処 |
|---|---|---|
| 各群が正規分布に従う | シャピロ・ウィルク検定 | ノンパラメトリック検定(クラスカル・ウォリス検定) |
| 等分散性(各群の分散が等しい) | Levene検定、Bartlett検定 | Welchの分散分析 |
| 各観測が独立 | 実験デザインで担保 | 反復測定分散分析 |
from scipy import stats
# 正規性の検定(シャピロ・ウィルク検定)
print("=== 正規性の検定(シャピロ・ウィルク検定) ===")
for name, data in [("通常講義", lecture), ("オンライン", online), ("グループ学習", group_study)]:
stat, p = stats.shapiro(data)
result = "正規分布" if p > 0.05 else "非正規分布"
print(f" {name}: 統計量={stat:.4f}, p値={p:.4f} → {result}")
# 等分散性の検定(Levene検定)
print("\n=== 等分散性の検定(Levene検定) ===")
stat, p = stats.levene(lecture, online, group_study)
print(f" 統計量={stat:.4f}, p値={p:.4f}")
if p > 0.05:
print(" → 等分散を仮定できる(通常のANOVAが適用可能)")
else:
print(" → 等分散を仮定できない(Welchの分散分析を使用)")
ノンパラメトリック代替:クラスカル・ウォリス検定
正規性の前提が満たされない場合は、クラスカル・ウォリス検定を使用します。
# クラスカル・ウォリス検定(正規性を仮定しない)
stat, p = stats.kruskal(lecture, online, group_study)
print(f"\n=== クラスカル・ウォリス検定 ===")
print(f"統計量: {stat:.4f}, p値: {p:.6f}")
7-3. 二元分散分析
**二元分散分析(Two-way ANOVA)**は、2つの因子とその交互作用がデータに与える影響を同時に分析する手法です。
一元分散分析との違い
| 項目 | 一元分散分析 | 二元分散分析 |
|---|---|---|
| 因子の数 | 1つ | 2つ |
| 分析できる効果 | 1つの主効果 | 2つの主効果 + 交互作用 |
| 例 | 学習方法が成績に影響するか | 学習方法と性別が成績に影響するか |
交互作用とは
交互作用とは、ある因子の効果が別の因子の水準によって異なることを意味します。
例:
- 交互作用なし:「グループ学習は男女ともに成績を上げる」
- 交互作用あり:「グループ学習は女性の成績を上げるが、男性には効果がない」
例題
因子として「学習方法」(通常講義・オンライン・グループ学習)と「性別」(男性・女性)を導入し、これらが試験成績にどのような影響を与えるかを検証します。
仮説の設定
二元分散分析では、3つの仮説を同時に検定します。
| # | 効果 | 帰無仮説 ($H_0$) | 対立仮説 ($H_1$) |
|---|---|---|---|
| 1 | 主効果①(学習方法) | 学習方法は成績に影響しない | 学習方法は成績に影響する |
| 2 | 主効果②(性別) | 性別は成績に影響しない | 性別は成績に影響する |
| 3 | 交互作用(学習方法×性別) | 交互作用はない | 交互作用がある |
平方和の分解(二元分散分析)
$$SST = SS_{学習方法} + SS_{性別} + SS_{交互作用} + SSE$$
Pythonでの実装
二元分散分析にはstatsmodelsライブラリを使用します。
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.formula.api import ols
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
# データの準備
np.random.seed(42)
# 各条件のデータ生成(学習方法 × 性別)
data = []
methods = ["通常講義", "オンライン", "グループ学習"]
genders = ["男性", "女性"]
# 効果の設定
method_effects = {"通常講義": 0, "オンライン": 5, "グループ学習": 10}
gender_effects = {"男性": 0, "女性": 3}
# 交互作用:グループ学習は女性により効果的
interaction = {("グループ学習", "女性"): 5}
for method in methods:
for gender in genders:
base = 60
effect = base + method_effects[method] + gender_effects[gender]
effect += interaction.get((method, gender), 0)
scores = np.random.normal(loc=effect, scale=8, size=15)
for score in scores:
data.append({"method": method, "gender": gender, "score": score})
df = pd.DataFrame(data)
# 基本統計量
print("=== グループ別基本統計量 ===")
summary = df.groupby(["method", "gender"])["score"].agg(["count", "mean", "std"])
print(summary.round(2))
statsmodelsによる二元分散分析
# 二元分散分析の実行
# ols: 線形モデル(最小二乗法)
# C(): カテゴリ変数であることを明示
model = ols('score ~ C(method) + C(gender) + C(method):C(gender)', data=df).fit()
# 分散分析表の作成(Type II)
anova_table = sm.stats.anova_lm(model, typ=2)
print("\n=== 二元分散分析表 ===")
print(anova_table.round(4))
# 結果の解釈
alpha = 0.05
print("\n=== 結果の解釈(有意水準 α=0.05) ===")
effects = {
"C(method)": "学習方法の主効果",
"C(gender)": "性別の主効果",
"C(method):C(gender)": "交互作用効果"
}
for source, name in effects.items():
p = anova_table.loc[source, "PR(>F)"]
f_val = anova_table.loc[source, "F"]
if p < alpha:
print(f" {name}: F={f_val:.4f}, p={p:.6f} → 有意(p < 0.05)")
else:
print(f" {name}: F={f_val:.4f}, p={p:.6f} → 有意でない")
交互作用の可視化
# 交互作用プロット
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# プロット1: 学習方法×性別の平均スコア
means = df.groupby(["method", "gender"])["score"].mean().unstack()
means = means.reindex(["通常講義", "オンライン", "グループ学習"])
means.plot(marker='o', linewidth=2, markersize=8, ax=axes[0])
axes[0].set_title("交互作用プロット(学習方法×性別)", fontsize=12)
axes[0].set_xlabel("学習方法")
axes[0].set_ylabel("平均スコア")
axes[0].legend(title="性別")
axes[0].grid(alpha=0.3)
# プロット2: 箱ひげ図
sns.boxplot(x="method", y="score", hue="gender", data=df, ax=axes[1],
palette="Set2", order=["通常講義", "オンライン", "グループ学習"])
axes[1].set_title("学習方法×性別のスコア分布", fontsize=12)
axes[1].set_xlabel("学習方法")
axes[1].set_ylabel("スコア")
axes[1].legend(title="性別")
plt.tight_layout()
plt.show()
交互作用の解釈
交互作用プロットで線が平行でない場合、交互作用が存在する可能性があります。
| 線の形状 | 交互作用 | 解釈 |
|---|---|---|
| 平行 | なし | 学習方法の効果は性別によらず一定 |
| 交差する | 強い交互作用 | 学習方法の効果が性別によって逆転する |
| 平行でないが交差しない | 弱い交互作用 | 学習方法の効果の大きさが性別によって異なる |
7-4. 多重比較
分散分析で「少なくとも1つのグループに差がある」とわかっても、どのグループ間に差があるのかはわかりません。それを特定するために**多重比較(Post-hoc test)**を行います。
なぜ多重比較が必要か
分散分析の結果は「全グループの平均が等しい」か「少なくとも1つ異なる」かしか教えてくれません。具体的にどのペア(A-B、A-C、B-Cなど)に差があるかを特定するには、追加の検定が必要です。
ただし単純にt検定を繰り返すと多重検定問題が生じるため、それを補正する多重比較法を使用します。
主な多重比較法
| 方法 | 特徴 | 使用場面 |
|---|---|---|
| Tukey HSD | 全ペアを比較。最もよく使われる | 全グループの比較を網羅的に行いたい |
| Bonferroni | 有意水準を検定回数で割る。保守的 | 比較回数が少ない場合 |
| Scheffé | 最も保守的。あらゆる対比に対応 | 事後的に任意の対比を行う場合 |
| Dunnett | 対照群との比較のみ | 対照群(コントロール)と他群を比較 |
| Holm | Bonferroniの改良版。検出力が高い | 一般的な多重比較 |
Tukey HSD法の実装
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
import matplotlib.pyplot as plt
import japanize_matplotlib
# 先ほどの一元分散分析のデータを使用
np.random.seed(42)
lecture = np.random.normal(loc=65, scale=10, size=30)
online = np.random.normal(loc=70, scale=12, size=30)
group_study = np.random.normal(loc=75, scale=9, size=30)
scores = np.concatenate([lecture, online, group_study])
methods = ["通常講義"]*30 + ["オンライン"]*30 + ["グループ学習"]*30
# まず分散分析で有意差を確認
f_stat, p_value = stats.f_oneway(lecture, online, group_study)
print(f"=== 一元分散分析 ===")
print(f"F値: {f_stat:.4f}, p値: {p_value:.6f}")
if p_value < 0.05:
print("→ 有意差あり。多重比較を実施します。\n")
# Tukey HSD法による多重比較
tukey_result = pairwise_tukeyhsd(scores, methods, alpha=0.05)
print("=== Tukey HSD 多重比較結果 ===")
print(tukey_result)
# 結果の可視化
fig = tukey_result.plot_simultaneous()
plt.title("Tukey HSD: グループ間の平均差と信頼区間", fontsize=12)
plt.xlabel("スコア")
plt.tight_layout()
plt.show()
Tukey HSD結果の読み方
出力の各行が1つのペア比較を表します。
| 列名 | 意味 |
|---|---|
| group1, group2 | 比較するグループのペア |
| meandiff | 平均値の差(group2 - group1) |
| p-adj | 補正されたp値 |
| lower, upper | 平均差の95%信頼区間 |
| reject | True → 有意差あり、False → 有意差なし |
Bonferroni補正
from itertools import combinations
# 全ペアのt検定 + Bonferroni補正
groups = {"通常講義": lecture, "オンライン": online, "グループ学習": group_study}
pairs = list(combinations(groups.keys(), 2))
n_comparisons = len(pairs)
alpha_corrected = 0.05 / n_comparisons # Bonferroni補正
print(f"=== Bonferroni補正による多重比較 ===")
print(f"比較回数: {n_comparisons}")
print(f"補正後の有意水準: {alpha_corrected:.4f}\n")
print(f"{'ペア':<25} {'t値':>8} {'p値':>10} {'判定':<10}")
print("-" * 60)
for g1, g2 in pairs:
t_stat, p_val = stats.ttest_ind(groups[g1], groups[g2])
significant = "有意差あり" if p_val < alpha_corrected else "有意差なし"
print(f"{g1} vs {g2:<12} {t_stat:>8.4f} {p_val:>10.6f} {significant}")
実践例:マーケティング施策の効果比較
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib
# 4つの広告パターンのクリック率データ
np.random.seed(42)
ad_A = np.random.normal(loc=3.2, scale=0.8, size=50) # パターンA
ad_B = np.random.normal(loc=3.5, scale=0.9, size=50) # パターンB
ad_C = np.random.normal(loc=4.1, scale=0.7, size=50) # パターンC
ad_D = np.random.normal(loc=3.3, scale=1.0, size=50) # パターンD
df_ad = pd.DataFrame({
"ctr": np.concatenate([ad_A, ad_B, ad_C, ad_D]),
"pattern": ["A"]*50 + ["B"]*50 + ["C"]*50 + ["D"]*50
})
# 1. 分散分析
f_stat, p_value = stats.f_oneway(ad_A, ad_B, ad_C, ad_D)
print(f"=== 広告パターン別CTRの分散分析 ===")
print(f"F値: {f_stat:.4f}, p値: {p_value:.6f}")
# 2. 多重比較
if p_value < 0.05:
tukey = pairwise_tukeyhsd(df_ad["ctr"], df_ad["pattern"], alpha=0.05)
print(f"\n=== Tukey HSD 多重比較 ===")
print(tukey)
# 3. 可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sns.boxplot(x="pattern", y="ctr", data=df_ad, ax=axes[0], palette="Set3")
axes[0].set_title(f"広告パターン別CTR\nF={f_stat:.2f}, p={p_value:.4f}", fontsize=12)
axes[0].set_xlabel("広告パターン")
axes[0].set_ylabel("CTR (%)")
# 平均値の棒グラフ + 信頼区間
means = df_ad.groupby("pattern")["ctr"].mean()
sems = df_ad.groupby("pattern")["ctr"].sem()
axes[1].bar(means.index, means.values, yerr=sems.values*1.96,
capsize=5, color=['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3'], alpha=0.8)
axes[1].set_title("平均CTRと95%信頼区間", fontsize=12)
axes[1].set_xlabel("広告パターン")
axes[1].set_ylabel("平均CTR (%)")
axes[1].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
効果量(η²: イータ二乗)
分散分析でも効果量を報告することが推奨されます。
$$\eta^2 = \frac{SSB}{SST}$$
# 効果量の計算
all_scores = np.concatenate([ad_A, ad_B, ad_C, ad_D])
grand_mean = all_scores.mean()
ssb = sum(50 * (g.mean() - grand_mean)**2 for g in [ad_A, ad_B, ad_C, ad_D])
sst = np.sum((all_scores - grand_mean)**2)
eta_squared = ssb / sst
print(f"\n=== 効果量 ===")
print(f"η² = {eta_squared:.4f}")
if eta_squared < 0.01:
print("→ 効果量: ほとんどなし")
elif eta_squared < 0.06:
print("→ 効果量: 小")
elif eta_squared < 0.14:
print("→ 効果量: 中")
else:
print("→ 効果量: 大")
補足:分散分析の実践的なポイント
分散分析の選択ガイド
分析したい因子はいくつ?
├── 1つ
│ ├── 正規分布 + 等分散 → 一元分散分析(f_oneway)
│ ├── 正規分布 + 等分散でない → Welchの分散分析
│ └── 非正規分布 → クラスカル・ウォリス検定
│
├── 2つ
│ └── 二元分散分析(statsmodels ols + anova_lm)
│
└── 対応あり(繰り返し測定)
└── 反復測定分散分析(statsmodels AnovaRM)
反復測定分散分析
同じ被験者に対して複数条件を測定した場合(対応あり)は、反復測定分散分析を使用します。
import pandas as pd
from statsmodels.stats.anova import AnovaRM
# 例: 同じ10人の被験者が3つの条件でテストを受けた
np.random.seed(42)
n_subjects = 10
df_repeated = pd.DataFrame({
"subject": list(range(n_subjects)) * 3,
"condition": ["条件A"]*n_subjects + ["条件B"]*n_subjects + ["条件C"]*n_subjects,
"score": np.concatenate([
np.random.normal(60, 8, n_subjects), # 条件A
np.random.normal(65, 8, n_subjects), # 条件B
np.random.normal(70, 8, n_subjects), # 条件C
])
})
# 反復測定分散分析
rm_anova = AnovaRM(df_repeated, depvar='score', subject='subject', within=['condition'])
result = rm_anova.fit()
print("=== 反復測定分散分析 ===")
print(result)
分散分析の結果報告の書き方
学術論文やレポートでの報告形式:
一元分散分析の結果、学習方法の主効果が有意であった
(F(2, 87) = 8.45, p < .001, η² = .16)。
Tukey HSD法による多重比較の結果、グループ学習は通常講義
(p = .002)およびオンライン学習(p = .034)よりも有意に
高い成績を示した。通常講義とオンライン学習の間には
有意な差は認められなかった(p = .215)。
まとめ
本記事では、分散分析として以下の内容を解説しました。
| 項目 | ポイント |
|---|---|
| 分散分析とは | 3群以上の平均値の差を1回の検定で判断する手法 |
| 平方和の分解 | SST = SSB + SSE(全体 = 群間 + 群内) |
| F値 | 群間分散/群内分散。F値が大きいほど群間の差が大きい |
| 一元分散分析 | 1つの因子の効果を分析(scipy.stats.f_oneway) |
| 二元分散分析 | 2つの因子と交互作用を同時に分析(statsmodels) |
| 多重比較 | どのペアに差があるかを特定(Tukey HSD等) |
分散分析のチェックリスト
- 帰無仮説と対立仮説を設定したか
- 前提条件(正規性・等分散性・独立性)を確認したか
- 適切な分散分析手法を選択したか
- p値と効果量の両方を確認したか
- 有意差がある場合、多重比較を実施したか
- 交互作用を可視化して解釈したか(二元分散分析の場合)
- 結果を正しく報告しているか
シリーズ記事一覧
- 【第1回】Pythonで学ぶデータ分析実践 〜データ分析とは〜
- 【第2回】データの前処理(欠損値・外れ値・ダミー変数・データスケーリングなど)
- 【第3回】データの把握(基本統計量・ヒストグラム・箱ひげ図・散布図など)
- 【第4回】相関分析(共分散・相関係数・相関比・連関係数など)
- 【第5回】統計的推定(母集団と標本・点推定・区間推定・信頼区間)
- 【第6回】統計的検定(仮説検定・p値・t検定・カイ二乗検定など)
- 【第7回】分散分析(一元分散分析・二元分散分析・多重比較)
- 【第8回】回帰分析(単回帰・重回帰・一般化線形モデル・モデル評価)
- 【第9回】時系列データ分析(トレンド・季節性・ARIMA・SARIMAなど)
- 【第10回】クラス分類(決定木・ランダムフォレスト・ロジスティック回帰など)
- 【第11回】クラスタリング(k-means・階層クラスタリング・エルボー法など)
- 【第12回】次元削減(主成分分析・寄与率・可視化)
- 【発展編】分析精度の向上(特徴量エンジニアリング・ハイパーパラメータ調整・アンサンブル学習など)
- 【発展編】ニューラルネットワーク入門(パーセプトロン・誤差逆伝播法・TensorFlow・Kerasなど)
次回の第8回では、変数間の関係をモデル化する回帰分析について解説します。単回帰分析、重回帰分析、正規線形モデル、一般化線形モデル、そしてR²やRMSEによるモデル評価まで、予測モデルの構築方法を学びます。