Pandas & NumPy Academy · レッスン

ANOVAと事後検定

一元配置ANOVAで3つ以上のグループ間の平均値を比較し、Tukey HSDで差のある組み合わせを特定します。

レッスン 4/413 ステップ

「ANOVAと事後検定」はCoddyKit上の無料Pandas & NumPy Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPandas & NumPy Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

複数のt検定ではいけない理由

3つ以上のグループ間で平均値を比較する場合、複数のt検定を実行すると第1種過誤率(偽陽性率)が高くなります。A対B、A対C、B対Cをそれぞれα=0.05で検定すると、全体の偽陽性確率は5%ではなく約14%になります。分散分析(ANOVA)は、すべてのグループを1回の検定で同時に検定することでこの問題を解決し、偽陽性率を正確にαに保ちます。包括的な問い「いずれかのグループに差があるか」に対しては、1回のANOVAで全ペアのt検定を置き換えられます。

一元配置ANOVAの全体像

一元配置ANOVAは、3つ以上のグループの平均値に有意な差があるかを検定します。全体の分散を、グループ間分散(グループへの所属によって説明される分散)とグループ内分散(ランダムなノイズ)に分解します。F統計量は両者の比で、F = (グループ間分散)/(グループ内分散)です。Fが大きいほど、ノイズに対してグループ間の差が大きく、p値は小さくなります。帰無仮説はH₀: すべてのグループの平均値が等しい、です。

import numpy as np
from scipy import stats

np.random.seed(0)
# Three product variants with different conversion rates
group_a = np.random.normal(10.0, 2.0, 50)  # baseline
group_b = np.random.normal(11.5, 2.0, 50)  # slightly better
group_c = np.random.normal(13.0, 2.0, 50)  # clearly better

f_stat, p = stats.f_oneway(group_a, group_b, group_c)
print(f'F-statistic: {f_stat:.3f}')
print(f'p-value: {p:.6f}')
print('At least one group differs?', 'Yes' if p < 0.05 else 'No')

Pandas DataFrameでのANOVA

実務では、データはロング形式のDataFrameに格納されます。1つの列にグループラベルを、別の列に測定値を格納します。各グループをSeriesとして抽出し、stats.f_oneway()に渡します。あるいは、ワイド形式のDataFrame(グループごとに1列)がある場合は、各列を直接渡します。この関数は任意の数の位置引数を受け取れるため、4つ、5つ、それ以上のグループにも簡単に拡張できます。

import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(1)
df = pd.DataFrame({
    'group': ['A']*40 + ['B']*40 + ['C']*40 + ['D']*40,
    'score': np.concatenate([
        np.random.normal(70, 10, 40),
        np.random.normal(75, 10, 40),
        np.random.normal(80, 10, 40),
        np.random.normal(72, 10, 40)
    ])
})

groups = [group['score'].values
          for _, group in df.groupby('group')]

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(df.groupby('group')['score'].mean().round(2))

ANOVAの前提条件

一元配置ANOVAには、次の前提があります。(1) 独立性 — 各観測値が互いに独立していること、(2) 正規性 — 各グループ内の残差が近似的に正規分布に従うこと(大標本では中心極限定理により頑健です)、(3) 等分散性 — グループ間で分散が等しいこと。分散の前提はLevene検定(stats.levene(*groups))で検定します。分散が等しくない場合は、WelchのANOVAを使用します。これはpingouinライブラリで利用できるほか、statsmodelsを使って計算することもできます。

import numpy as np
from scipy import stats

np.random.seed(0)
g1 = np.random.normal(10, 2, 40)
g2 = np.random.normal(12, 2, 40)
g3 = np.random.normal(11, 8, 40)   # Much higher variance

# Test equal variances
stat_l, p_l = stats.levene(g1, g2, g3)
print(f'Levene\'s test: stat={stat_l:.3f}, p={p_l:.4f}')
if p_l < 0.05:
    print('Warning: Unequal variances! Standard ANOVA may be unreliable.')
    print('Consider Welch\'s ANOVA or Kruskal-Wallis.')

Kruskal-Wallis検定:ノンパラメトリックANOVA

ANOVAの前提(正規分布、小標本、等分散性)が満たされない場合は、Kruskal-Wallis検定がノンパラメトリックな代替手法になります。データを順位に変換し、順位和を比較することで、グループの分布に差があるかを検定します。scipy.stats.kruskal(*groups)を使用します。帰無仮説は、すべてのグループが同じ分布に従うというものです(分布の形状が同じ場合は、中央値が等しいことの検定と同等です)。常に有効な手法ですが、前提条件が満たされている場合はANOVAより検出力が低くなります。

import numpy as np
from scipy import stats

np.random.seed(0)
# Non-normal data: customer satisfaction scores (1-10 scale)
g1 = np.random.choice(range(1, 11), 50, p=[0.05]*10)
g2 = np.random.choice(range(1, 11), 50, p=[0.02, 0.03, 0.05, 0.08, 0.12, 0.15, 0.20, 0.15, 0.12, 0.08])
g3 = np.random.choice(range(1, 11), 50, p=[0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.08, 0.05, 0.04, 0.03])

stat, p = stats.kruskal(g1, g2, g3)
print(f'Kruskal-Wallis: H={stat:.3f}, p={p:.4f}')
print('Groups differ?', 'Yes' if p < 0.05 else 'No')

事後検定:必要な理由

ANOVAで有意な結果が得られると、少なくとも1つのグループの平均値が異なることは分かりますが、どのペアに差があるかは分かりません。事後検定では、多重比較を補正しながら、すべてのペアを比較します。最もよく使われるのはTukeyの正直な有意差検定(Tukey HSD)で、ファミリーワイズエラー率を正確にαに制御します。statsmodels.stats.multicomp.pairwise_tukeyhsd()で利用できます。事後検定は、有意なANOVAの後にのみ実行してください。有意でない結果に対して、手当たり次第に検定してはいけません。

import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(0)
data = np.concatenate([
    np.random.normal(70, 10, 40),
    np.random.normal(80, 10, 40),
    np.random.normal(75, 10, 40)
])
groups = ['A']*40 + ['B']*40 + ['C']*40

# First confirm ANOVA is significant
f, p = stats.f_oneway(*[data[i*40:(i+1)*40] for i in range(3)])
print(f'ANOVA p={p:.4f}')
if p < 0.05:
    print('Post-hoc Tukey HSD:')
    print(pairwise_tukeyhsd(data, groups, alpha=0.05))

Tukey HSDの出力を読む

Tukey HSDの表には、グループの各ペアにつき1行が表示されます。注目すべき列は、meandiff(ペア間の平均値の差)、lowerとupper(差の95%信頼区間)、reject(αにおいてペア間に有意差がある場合にTrue)です。信頼区間に0が含まれていなければ、そのペアには有意差があります。信頼区間が重なるグループ同士には、有意な差がありません。

import numpy as np
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(42)
data = np.concatenate([
    np.random.normal(10, 2, 60),  # Group A
    np.random.normal(13, 2, 60),  # Group B (clearly higher)
    np.random.normal(10.5, 2, 60) # Group C (barely different from A)
])
groups = ['A']*60 + ['B']*60 + ['C']*60

result = pairwise_tukeyhsd(data, groups, alpha=0.05)
print(result)
# A-B: reject=True (B is higher)
# A-C: reject=False (barely different)
# B-C: reject=True (B is higher)

Bonferroni補正とBenjamini-Hochberg補正

Tukey HSDの代わりに、ペアごとのt検定へBonferroni補正を適用することもできます。すべてのt検定を実行した後、比較の数をkとして、判定のしきい値をα/kに調整します。この方法は保守的で、実際に存在する差を見逃す可能性があります。Benjamini-Hochberg偽発見率(FDR)補正はそれほど保守的ではなく、偽発見の期待割合を制御します。偽陽性がやや増える代わりに、真陽性をより多く検出できます。statsmodels.stats.multitest.multipletests(p_values, method='fdr_bh')を使用します。

import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests

np.random.seed(0)
groups = [np.random.normal(10 + i*1.5, 2, 40) for i in range(4)]
names = ['A', 'B', 'C', 'D']

# All pairwise t-tests
p_values = []
pairs = []
for i in range(len(groups)):
    for j in range(i+1, len(groups)):
        _, p = stats.ttest_ind(groups[i], groups[j])
        p_values.append(p)
        pairs.append(f'{names[i]}-{names[j]}')

# Benjamini-Hochberg correction
reject, adj_p, _, _ = multipletests(p_values, method='fdr_bh')
for pair, p, ap, r in zip(pairs, p_values, adj_p, reject):
    print(f'{pair}: raw_p={p:.4f}, adj_p={ap:.4f}, significant={r}')

ANOVAの効果量:イータ二乗

ANOVAでは、Cohenのdに相当する指標としてイータ二乗(η²)を使用します。これは、グループへの所属によって説明される全分散の割合です。η² = SS_between / SS_totalです。目安として、0.01未満は小、0.06は中、0.14超は大と解釈します。偏イータ二乗(η²_p)は、研究論文でより一般的です。全分散ではなく、グループ分散と誤差分散の合計だけで割ります。効果が統計的に検出できるだけでなく、実務上意味のある大きさかどうかを示すため、ANOVAのp値とともにη²も必ず計算してください。

import numpy as np
from scipy import stats

np.random.seed(0)
groups = [
    np.random.normal(10, 2, 50),
    np.random.normal(12, 2, 50),
    np.random.normal(14, 2, 50)
]

all_data = np.concatenate(groups)
grand_mean = all_data.mean()

ss_between = sum(len(g) * (g.mean() - grand_mean)**2 for g in groups)
ss_total = sum((x - grand_mean)**2 for g in groups for x in g)
eta_sq = ss_between / ss_total

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(f'Eta-squared: {eta_sq:.4f} ({eta_sq:.1%} of variance explained)')

二元配置ANOVAの概要

二元配置ANOVAは、一元配置ANOVAを2つのカテゴリ要因に同時に拡張したものです。たとえば、試験の得点が指導方法と学生の経験レベルの両方によって異なるかを同時に検定できます。また、交互作用効果、つまり指導方法の効果が経験レベルによって変わるかどうかも検定します。statsmodelsでは、ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit()とanova_lm(model)を使って実装します。これは、より高度な実験計画の基礎となります。

import pandas as pd
import numpy as np
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm

np.random.seed(0)
df = pd.DataFrame({
    'method': ['trad']*60 + ['active']*60,
    'level': ['beginner']*30 + ['advanced']*30 + ['beginner']*30 + ['advanced']*30,
    'score': (np.random.normal(70, 8, 30).tolist() +
              np.random.normal(80, 8, 30).tolist() +
              np.random.normal(75, 8, 30).tolist() +
              np.random.normal(88, 8, 30).tolist())
})

model = ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit()
print(anova_lm(model, typ=2)[['F', 'PR(>F)']].round(4))

反復測定ANOVA

反復測定ANOVAは、同じ被験者を複数回測定する場合に使用します。たとえば、処置後0分、30分、60分の反応時間を検定する場合です。これは、対応のあるt検定を複数グループに一般化したものです。個人差を考慮するため、一元配置ANOVAよりも検出力が高くなります。Pythonでは、pingouin.rm_anova()を使用し、事後分析にはpg.pairwise_tests()を使用します。球面性が成り立たない場合(Mauchly検定で検定します)は、Greenhouse-Geisser補正済みのp値を使用します。

import pandas as pd
import numpy as np

# Example structure for repeated measures (pingouin library)
np.random.seed(0)
subjects = list(range(20)) * 3   # 20 subjects, 3 time points
timepoints = ['T0']*20 + ['T1']*20 + ['T2']*20
scores = np.concatenate([
    np.random.normal(50, 10, 20),   # baseline
    np.random.normal(55, 10, 20),   # improved
    np.random.normal(60, 10, 20)    # further improved
])

df_rm = pd.DataFrame({'subject': subjects, 'time': timepoints, 'score': scores})
print(df_rm.groupby('time')['score'].mean().round(2))
# Use pingouin.rm_anova(data=df_rm, dv='score', within='time', subject='subject')

理解度チェック

このレッスンで学んだデータ分析の概念について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、scipy.stats.f_oneway()が全体の第1種過誤率を制御しながら、いずれかのグループの平均値に差があるかを検定すること、事後検定(Tukey HSD)がANOVAで有意な結果が得られた後に、具体的にどのペアに差があるかを特定すること、そしてKruskal-Wallis検定が正規性または等分散性の前提が満たされない場合のノンパラメトリックな代替手法であることを学びました。次は、これまでのすべてのスキルをエンドツーエンドのデータパイプラインに統合する総合プロジェクトを開始します。

無料で開始

AI チューターと学ぶ Python — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「ANOVAと事後検定」レッスンは無料ですか?

はい。「ANOVAと事後検定」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Pandas & NumPy Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

「ANOVAと事後検定」で何を学びますか?

一元配置ANOVAで3つ以上のグループ間の平均値を比較し、Tukey HSDで差のある組み合わせを特定します。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Pandas & NumPy Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPandas & NumPy Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「ANOVAと事後検定」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPandas & NumPy Academyレッスンでコードを書いて実行できますか?

はい。すべてのPandas & NumPy Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 記述統計と正規性検定
  2. 平均値を比較するt検定
  3. 独立性のカイ二乗検定
  4. ANOVAと事後検定
← Pandas & NumPy Academyに戻る