第6回 統計的検定
はじめに
本シリーズ「Pythonで学ぶデータ分析実践」の第6回では、統計的検定について解説します。
前回の統計的推定では、標本から母集団の特徴を「推定」する方法を学びました。今回は、データに基づいて「差がある」「差がない」を客観的に判断する仮説検定の考え方を解説します。
例えば「新しい施策は効果があるのか?」「2つのグループに差はあるのか?」といった問いに対して、統計的な根拠をもって回答するための手法です。ビジネスの意思決定やA/Bテストの評価など、実務で頻繁に使われます。
シリーズ全体の構成(全12回)
| 回 | テーマ |
|---|---|
| 第1回 | データ分析とは |
| 第2回 | データの前処理 |
| 第3回 | データの把握 |
| 第4回 | 相関分析 |
| 第5回 | 統計的推定 |
| 第6回 | 統計的検定(本記事) |
| 第7回 | 分散分析 |
| 第8回 | 回帰分析 |
| 第9回 | 時系列データ分析 |
| 第10回 | クラス分類 |
| 第11回 | クラスタリング |
| 第12回 | 次元削減 |
6-1. 仮説検定とは
**統計的検定(仮説検定)**とは、「ある仮説に対してそれが正しいのか否かを統計学的に検証する」推計統計学の手法です。検定を行うことで、標本データから母集団に関する仮説が正しいかどうかを客観的に判断します。
帰無仮説と対立仮説
検定では「ある仮説を立て、その仮説を棄却する」というアプローチで判断を行います。
| 用語 | 記号 | 説明 | 例 |
|---|---|---|---|
| 帰無仮説 | $H_0$ | 本来証明したい仮説とは反対の仮説(「差がない」「効果がない」) | 「牛肉1パックは100gである」 |
| 対立仮説 | $H_1$ | 本来証明したい仮説(「差がある」「効果がある」) | 「牛肉1パックは100gではない」 |
検定の目的: 帰無仮説が誤りであることを示して、対立仮説を証明すること
検定の流れ
ステップ1: 帰無仮説 H₀ と対立仮説 H₁ を立てる
ステップ2: 有意水準 α を設定する(通常 0.05)
ステップ3: 検定統計量(t値など)を計算する
ステップ4: p値を計算する
ステップ5: p値と有意水準を比較して判断する
- p < α → 帰無仮説を棄却(有意差あり)
- p ≥ α → 帰無仮説を棄却できない(有意差なし)
ステップ6: 結論を出す
具体例で理解する検定の考え方
例題: あるスーパーのチラシで牛肉が「1パック100g」と掲載されていた。この牛肉を30パック購入し計量したところ「平均97g」だった。「1パック100g」と言えるかどうか検定で明らかにしたい。
- $H_0$:「牛肉1パックあたりの平均重量は100gである」(μ = 100)
- $H_1$:「牛肉1パックあたりの平均重量は100gではない」(μ ≠ 100)
標本平均が100gから大きく離れていれば → 帰無仮説を棄却できる
標本平均が100gからあまり離れていなければ → 帰無仮説を棄却できない
6-2. p値
有意確率「p値」とは
p値は、帰無仮説が真であると仮定した場合に、観測されたデータ(またはそれ以上に極端なデータ)が得られる確率です。
- p値が小さい → 帰無仮説のもとで今回の結果は起こりにくい → 帰無仮説は疑わしい
- p値が大きい → 帰無仮説のもとで今回の結果は十分起こり得る → 帰無仮説を否定できない
t値とp値の関係
t値は「標本平均が母集団平均から標準誤差の何個分離れているか」を表す検定統計量です。
$$t = \frac{\bar{x} - \mu_0}{SE} = \frac{\bar{x} - \mu_0}{s / \sqrt{n}}$$
- $\bar{x}$: 標本平均
- $\mu_0$: 帰無仮説で想定する母平均
- $s$: 不偏標準偏差
- $n$: 標本サイズ
t値が大きい(標本平均が比較対象と大きく離れている)と、p値は小さくなります。
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import japanize_matplotlib
# t分布とp値の可視化
df = 29 # 自由度(n-1)
t_observed = -2.5 # 観測されたt値
x = np.linspace(-4, 4, 1000)
y = stats.t.pdf(x, df)
plt.figure(figsize=(10, 6))
plt.plot(x, y, 'k-', linewidth=2, label='t分布 (df=29)')
# 両側検定のp値(棄却域)を塗りつぶし
plt.fill_between(x, y, where=(x <= t_observed), alpha=0.4, color='red',
label=f'p値の領域 (t≤{t_observed})')
plt.fill_between(x, y, where=(x >= -t_observed), alpha=0.4, color='red',
label=f'p値の領域 (t≥{-t_observed})')
# p値を計算
p_value = 2 * stats.t.cdf(t_observed, df)
plt.axvline(t_observed, color='red', linestyle='--', linewidth=1.5)
plt.axvline(-t_observed, color='red', linestyle='--', linewidth=1.5)
plt.xlabel("t値", fontsize=12)
plt.ylabel("確率密度", fontsize=12)
plt.title(f"t分布とp値の関係(t={t_observed}, p={p_value:.4f})", fontsize=14)
plt.legend(fontsize=10)
plt.grid(alpha=0.3)
plt.show()
6-3. 有意水準
有意水準αとは
**有意水準(α)**は、帰無仮説を棄却するかどうかの判断基準となる確率のしきい値です。
| 有意水準 | 意味 | 使用場面 |
|---|---|---|
| α = 0.05(5%) | 「5%未満の確率でしか起きないことが起きた」と判断 | 一般的な研究・分析 |
| α = 0.01(1%) | より厳しい基準 | 医学研究など高い信頼性が必要な場合 |
| α = 0.10(10%) | 緩い基準 | 探索的な分析 |
判定ルール
$$p < \alpha \Rightarrow 帰無仮説を棄却(有意差あり)$$
$$p \geq \alpha \Rightarrow 帰無仮説を棄却できない(有意差なし)$$
両側検定と片側検定
| 検定の種類 | 対立仮説 | 棄却域 | 使用場面 |
|---|---|---|---|
| 両側検定 | $\mu \neq \mu_0$ | 両端にそれぞれα/2 | 「差がある」を調べたい(方向不明) |
| 片側検定(右側) | $\mu > \mu_0$ | 右端にα | 「大きい」を調べたい |
| 片側検定(左側) | $\mu < \mu_0$ | 左端にα | 「小さい」を調べたい |
一般的には、特定の方向だけを調べるのは恣意的になりやすいため、両側検定を行うことが多いです。
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import japanize_matplotlib
x = np.linspace(-4, 4, 1000)
y = stats.norm.pdf(x)
alpha = 0.05
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 両側検定
z_crit = stats.norm.ppf(1 - alpha/2)
axes[0].plot(x, y, 'k-', linewidth=2)
axes[0].fill_between(x, y, where=(x <= -z_crit), alpha=0.4, color='red')
axes[0].fill_between(x, y, where=(x >= z_crit), alpha=0.4, color='red')
axes[0].axvline(-z_crit, color='red', linestyle='--')
axes[0].axvline(z_crit, color='red', linestyle='--')
axes[0].set_title(f"両側検定(α=5%: 各端2.5%)\n棄却域: |z| > {z_crit:.3f}", fontsize=12)
axes[0].set_xlabel("検定統計量")
axes[0].annotate("2.5%", xy=(-3.2, 0.01), fontsize=11, color='red')
axes[0].annotate("2.5%", xy=(2.8, 0.01), fontsize=11, color='red')
axes[0].annotate("95%(採択域)", xy=(-0.8, 0.15), fontsize=11)
# 片側検定(右側)
z_crit_one = stats.norm.ppf(1 - alpha)
axes[1].plot(x, y, 'k-', linewidth=2)
axes[1].fill_between(x, y, where=(x >= z_crit_one), alpha=0.4, color='red')
axes[1].axvline(z_crit_one, color='red', linestyle='--')
axes[1].set_title(f"片側検定・右側(α=5%)\n棄却域: z > {z_crit_one:.3f}", fontsize=12)
axes[1].set_xlabel("検定統計量")
axes[1].annotate("5%", xy=(2.2, 0.01), fontsize=11, color='red')
axes[1].annotate("95%(採択域)", xy=(-1.5, 0.15), fontsize=11)
plt.tight_layout()
plt.show()
検定における2種類の誤り
| 誤りの種類 | 説明 | 確率 | 通称 |
|---|---|---|---|
| 第1種の誤り(α誤り) | 帰無仮説が正しいのに棄却してしまう | α(有意水準) | 偽陽性 |
| 第2種の誤り(β誤り) | 帰無仮説が誤りなのに棄却できない | β | 偽陰性 |
- 検出力(1-β): 対立仮説が正しい場合に正しく棄却できる確率
| 真実 \ 判断 | H₀を棄却しない | H₀を棄却する |
|---|---|---|
| H₀が真 | 正しい判断 ✓ | 第1種の誤り(α) |
| H₀が偽 | 第2種の誤り(β) | 正しい判断 ✓ |
6-4. t検定
t検定は仮説検定の中で最もよく用いられる手法の一つです。大きく1変量データに対するt検定と2群のデータに対するt検定に分けられます。
1変量データのt検定(1標本t検定)
平均値が「仮説値(母平均)」と異なると言えるかどうかを検定します。
例題
あるスーパーのチラシで牛肉が「1パック100g」と掲載されていた。この牛肉を30パック購入し計量したところ、以下の結果が得られた。「1パック100g」と言えるかどうか検定で明らかにしたい。
import numpy as np
from scipy import stats
# 牛肉30パックの重量データ(g)
weights = np.array([97, 99, 96, 98, 95, 101, 97, 94, 98, 96,
99, 97, 95, 98, 96, 100, 97, 94, 99, 96,
98, 97, 95, 99, 96, 98, 97, 94, 100, 96])
# 基本統計量
n = len(weights)
x_bar = weights.mean()
s = weights.std(ddof=1)
se = s / np.sqrt(n)
print(f"=== 基本統計量 ===")
print(f"標本サイズ n: {n}")
print(f"標本平均: {x_bar:.2f} g")
print(f"不偏標準偏差: {s:.2f} g")
print(f"標準誤差 SE: {se:.4f}")
# 仮説設定
mu_0 = 100 # 帰無仮説: μ = 100
print(f"\n=== 仮説 ===")
print(f"H₀: μ = {mu_0}g(牛肉1パックの平均重量は100gである)")
print(f"H₁: μ ≠ {mu_0}g(牛肉1パックの平均重量は100gではない)")
# t値の手動計算
t_manual = (x_bar - mu_0) / se
print(f"\n=== t値の計算 ===")
print(f"t = (x̄ - μ₀) / SE = ({x_bar:.2f} - {mu_0}) / {se:.4f} = {t_manual:.4f}")
# scipy.stats.ttest_1samp による1標本t検定
t_stat, p_value = stats.ttest_1samp(weights, mu_0)
print(f"\n=== 検定結果 ===")
print(f"t値: {t_stat:.4f}")
print(f"p値: {p_value:.6f}")
# 判定(有意水準 5%)
alpha = 0.05
print(f"\n=== 判定(有意水準 α = {alpha}) ===")
if p_value < alpha:
print(f"p値({p_value:.6f}) < α({alpha})")
print("→ 帰無仮説を棄却。牛肉1パックの平均重量は100gとは言えない。")
else:
print(f"p値({p_value:.6f}) ≥ α({alpha})")
print("→ 帰無仮説を棄却できない。100gでないとは断定できない。")
6-5. 対応のあるt検定
独立した2標本t検定(独立t検定)
2つの群が互いに独立していて、それぞれの群の平均値に有意な差があるかを検定します。
使用場面: 異なるグループ間の比較
- 理系と文系のテスト成績の比較
- 新薬群とプラセボ群の効果比較
- 男性と女性の購入額の比較
import numpy as np
from scipy import stats
# 例: 理系と文系の数学テスト成績
np.random.seed(42)
science_scores = np.array([78, 85, 92, 88, 76, 90, 84, 95, 82, 87,
91, 79, 86, 93, 80, 88, 84, 90, 77, 85])
liberal_arts_scores = np.array([65, 72, 58, 70, 68, 75, 62, 71, 66, 73,
60, 69, 74, 63, 67, 70, 64, 72, 61, 68])
print(f"=== 基本統計量 ===")
print(f"理系: 平均={science_scores.mean():.2f}, 標準偏差={science_scores.std(ddof=1):.2f}, n={len(science_scores)}")
print(f"文系: 平均={liberal_arts_scores.mean():.2f}, 標準偏差={liberal_arts_scores.std(ddof=1):.2f}, n={len(liberal_arts_scores)}")
# 仮説
print(f"\n=== 仮説 ===")
print("H₀: 理系と文系の数学の平均点に差はない(μ₁ = μ₂)")
print("H₁: 理系と文系の数学の平均点に差がある(μ₁ ≠ μ₂)")
# 等分散性の検定(Levene検定)
levene_stat, levene_p = stats.levene(science_scores, liberal_arts_scores)
print(f"\n=== 等分散性の検定(Levene検定) ===")
print(f"統計量: {levene_stat:.4f}, p値: {levene_p:.4f}")
if levene_p >= 0.05:
print("→ 等分散を仮定できる(Studentのt検定を使用)")
equal_var = True
else:
print("→ 等分散を仮定できない(Welchのt検定を使用)")
equal_var = False
# 独立t検定
t_stat, p_value = stats.ttest_ind(science_scores, liberal_arts_scores, equal_var=equal_var)
print(f"\n=== 独立t検定の結果 ===")
print(f"t値: {t_stat:.4f}")
print(f"p値: {p_value:.8f}")
alpha = 0.05
if p_value < alpha:
print(f"\n→ p値({p_value:.6f}) < α({alpha}): 帰無仮説を棄却")
print(" 理系と文系の数学の平均点には統計的に有意な差がある。")
else:
print(f"\n→ p値({p_value:.6f}) ≥ α({alpha}): 帰無仮説を棄却できない")
等分散性の確認
独立t検定を行う際は、2群の分散が等しいかどうかを事前に確認する必要があります。
| 等分散性 | 使用する検定 | scipy関数 |
|---|---|---|
| 等分散を仮定 | Studentのt検定 | ttest_ind(equal_var=True) |
| 等分散を仮定しない | Welchのt検定 | ttest_ind(equal_var=False) |
実務では、等分散の仮定が成り立つか不明な場合はWelchのt検定を使う方が安全です。Welchのt検定は等分散でない場合でも正しい結果を出し、等分散の場合でもほぼ同じ結果になります。
対応のあるt検定(対応t検定)
同じ対象に対して行った前後の測定や、対応するペアデータに対して使用します。
使用場面: 同一対象の前後比較
- 同じ生徒の前期試験と後期試験の比較
- ダイエット前とダイエット後の体重
- 研修前と研修後のスキルテスト
import numpy as np
from scipy import stats
# 例: 文系の生徒20名の前期・後期の数学テスト
np.random.seed(42)
before = np.array([55, 62, 48, 70, 58, 65, 52, 60, 45, 68,
57, 63, 50, 72, 55, 61, 49, 66, 53, 59])
after = np.array([60, 68, 55, 73, 65, 70, 58, 67, 52, 72,
62, 69, 56, 75, 60, 66, 55, 71, 58, 65])
# 差の計算
diff = after - before
print(f"=== 基本統計量 ===")
print(f"前期: 平均={before.mean():.2f}, 標準偏差={before.std(ddof=1):.2f}")
print(f"後期: 平均={after.mean():.2f}, 標準偏差={after.std(ddof=1):.2f}")
print(f"差(後期-前期): 平均={diff.mean():.2f}, 標準偏差={diff.std(ddof=1):.2f}")
# 仮説
print(f"\n=== 仮説 ===")
print("H₀: 前期と後期で数学の学力に差はない(μ_diff = 0)")
print("H₁: 前期と後期で数学の学力に差がある(μ_diff ≠ 0)")
# 対応のあるt検定
t_stat, p_value = stats.ttest_rel(before, after)
print(f"\n=== 対応のあるt検定の結果 ===")
print(f"t値: {t_stat:.4f}")
print(f"p値: {p_value:.8f}")
alpha = 0.05
if p_value < alpha:
print(f"\n→ p値({p_value:.6f}) < α({alpha}): 帰無仮説を棄却")
print(" 前期と後期で数学の成績に統計的に有意な差がある。")
if diff.mean() > 0:
print(f" 後期の方が平均{diff.mean():.1f}点高い。")
else:
print(f"\n→ p値({p_value:.6f}) ≥ α({alpha}): 帰無仮説を棄却できない")
独立t検定と対応t検定の使い分け
| 項目 | 独立t検定 | 対応t検定 |
|---|---|---|
| データの関係 | 2群が独立(別の対象) | 同一対象の前後比較 |
| 例 | クラスAとクラスBの比較 | 同じ生徒の前期と後期 |
| scipy関数 | ttest_ind() |
ttest_rel() |
| 検出力 | やや低い | 高い(個人差を排除できる) |
6-6. 比率の検定
母集団の比率(割合)が想定した値と異なるかどうかを検定する方法です。二項分布に基づいて検定を行います。
例題
ある商品の購入者に対し「全体の20%が抽選で全額キャッシュバック」というキャンペーンが行われていた。この商品を100名が購入したところ、キャッシュバックの対象となったのは12名だった。この結果は「たまたまの偶然」によるものか、「20%のキャッシュバック」という宣伝が実際と異なるのかを検証する。
検定の手順
(1) 仮説を立てる
- $H_0$: 購入者の20%が全額キャッシュバックを受ける(p = 0.20)
- $H_1$: キャッシュバック率は20%ではない(p ≠ 0.20)
(2) 統計量の計算
正規近似を使う場合、検定統計量Zは以下で計算されます。
$$Z = \frac{\hat{p} - p_0}{\sqrt{\frac{p_0(1-p_0)}{n}}}$$
- $\hat{p}$ = 12/100 = 0.12(標本比率)
- $p_0$ = 0.20(帰無仮説の比率)
- n = 100
$$Z = \frac{0.12 - 0.20}{\sqrt{\frac{0.20 \times 0.80}{100}}} = \frac{-0.08}{0.04} = -2.0$$
(3) 判定
Z = -2.0 は、有意水準5%の棄却域(|Z| > 1.96)に含まれるため、帰無仮説を棄却する。
Pythonでの実装
import numpy as np
from scipy import stats
# データ
n = 100 # 試行回数(購入者数)
k = 12 # 成功回数(キャッシュバック対象者数)
p_0 = 0.20 # 帰無仮説の比率
# 標本比率
p_hat = k / n
print(f"=== 比率の検定 ===")
print(f"標本サイズ n: {n}")
print(f"キャッシュバック対象者: {k}名")
print(f"標本比率 p̂: {p_hat:.2f} ({p_hat*100:.1f}%)")
print(f"帰無仮説の比率 p₀: {p_0:.2f} ({p_0*100:.1f}%)")
# 方法1: 正規近似による検定
z = (p_hat - p_0) / np.sqrt(p_0 * (1 - p_0) / n)
p_value_norm = 2 * stats.norm.cdf(-abs(z)) # 両側検定
print(f"\n=== 正規近似による検定 ===")
print(f"Z値: {z:.4f}")
print(f"p値: {p_value_norm:.6f}")
# 方法2: scipy.stats.binomtest(正確な二項検定)
result = stats.binomtest(k, n, p_0, alternative='two-sided')
print(f"\n=== 二項検定(正確法) ===")
print(f"p値: {result.pvalue:.6f}")
# 判定
alpha = 0.05
print(f"\n=== 判定(有意水準 α = {alpha}) ===")
if result.pvalue < alpha:
print(f"p値({result.pvalue:.6f}) < α({alpha})")
print("→ 帰無仮説を棄却。")
print(" 「購入者の20%がキャッシュバックを受ける」という主張は統計的に否定される。")
else:
print(f"p値({result.pvalue:.6f}) ≥ α({alpha})")
print("→ 帰無仮説を棄却できない。偶然の範囲内と判断される。")
比率の検定の実践例:コンバージョン率の検証
from scipy import stats
# Webサイトのコンバージョン率が業界平均(3%)と異なるか検定
n_visitors = 500 # 訪問者数
n_conversions = 22 # コンバージョン数
industry_rate = 0.03 # 業界平均 3%
result = stats.binomtest(n_conversions, n_visitors, industry_rate, alternative='two-sided')
print(f"訪問者数: {n_visitors}")
print(f"コンバージョン数: {n_conversions}")
print(f"コンバージョン率: {n_conversions/n_visitors*100:.1f}%")
print(f"業界平均: {industry_rate*100:.1f}%")
print(f"p値: {result.pvalue:.6f}")
if result.pvalue < 0.05:
print("→ 業界平均と統計的に有意な差がある")
else:
print("→ 業界平均と有意な差があるとは言えない")
6-7. カイ二乗検定
カイ二乗検定は、観測された頻度が期待される頻度と統計的に有意な差があるかを判断する検定です。主にカテゴリカルデータ(質的データ)に対して使用されます。
カイ二乗検定の種類
| 種類 | 説明 | 使用場面 |
|---|---|---|
| 適合度検定 | 観測データが理論的な分布に適合するか | サイコロが公正か検証 |
| 独立性の検定 | 2つのカテゴリ変数が独立か | 性別と購入商品の関連 |
カイ二乗検定とA/Bテスト
A/Bテストの結果を解析する際にカイ二乗検定がよく用いられます。2つのバージョン(AとB)のコンバージョン率に統計的に有意な差があるかどうかを判断できます。
A/Bテストとは: 2つの異なるバージョンをランダムに選ばれたグループに提供し、どちらがより良い結果をもたらすかを比較する実験的手法。マーケティングやWebサイト最適化で広く使用されています。
例題:A/Bテストの結果検証
Webサイトのランディングページで、デザインAとデザインBのコンバージョン率を比較するA/Bテストを実施した。
| コンバージョンあり | コンバージョンなし | 合計 | |
|---|---|---|---|
| デザインA | 45 | 455 | 500 |
| デザインB | 63 | 437 | 500 |
| 合計 | 108 | 892 | 1000 |
import numpy as np
from scipy import stats
import pandas as pd
# クロス集計表(観測値)
observed = np.array([[45, 455], # デザインA: CVあり, CVなし
[63, 437]]) # デザインB: CVあり, CVなし
# カイ二乗検定
chi2, p_value, dof, expected = stats.chi2_contingency(observed)
print(f"=== カイ二乗検定(A/Bテスト) ===")
print(f"\n観測値:")
print(f" デザインA: CVR = {45/500*100:.1f}% ({45}件/{500}件)")
print(f" デザインB: CVR = {63/500*100:.1f}% ({63}件/{500}件)")
print(f"\n期待値(差がないと仮定した場合):")
print(f" デザインA: CVあり={expected[0,0]:.1f}, CVなし={expected[0,1]:.1f}")
print(f" デザインB: CVあり={expected[1,0]:.1f}, CVなし={expected[1,1]:.1f}")
print(f"\n検定結果:")
print(f" カイ二乗値: {chi2:.4f}")
print(f" 自由度: {dof}")
print(f" p値: {p_value:.6f}")
alpha = 0.05
if p_value < alpha:
print(f"\n→ p値({p_value:.6f}) < α({alpha}): 帰無仮説を棄却")
print(" デザインAとBのコンバージョン率には統計的に有意な差がある。")
print(f" デザインBの方がCVRが高い({63/500*100:.1f}% vs {45/500*100:.1f}%)")
else:
print(f"\n→ p値({p_value:.6f}) ≥ α({alpha}): 帰無仮説を棄却できない")
print(" デザインAとBのCVRに有意な差があるとは言えない。")
適合度検定
データが想定した分布に従っているかを検定します。
from scipy import stats
import numpy as np
# 例: サイコロを600回振った結果(公正なら各面100回ずつ出るはず)
observed_freq = np.array([90, 105, 95, 110, 85, 115]) # 実際の結果
expected_freq = np.array([100, 100, 100, 100, 100, 100]) # 期待値
# カイ二乗適合度検定
chi2_stat, p_value = stats.chisquare(observed_freq, expected_freq)
print(f"=== カイ二乗適合度検定 ===")
print(f"観測値: {observed_freq}")
print(f"期待値: {expected_freq}")
print(f"カイ二乗値: {chi2_stat:.4f}")
print(f"p値: {p_value:.6f}")
if p_value < 0.05:
print("→ このサイコロは公正ではない可能性がある")
else:
print("→ このサイコロは公正であると言える")
実践例:複数パターンのA/Bテスト
import numpy as np
from scipy import stats
import pandas as pd
# 3パターン(A/B/C)のテスト結果
data = {
"デザイン": ["A", "B", "C"],
"CVあり": [45, 63, 58],
"CVなし": [455, 437, 442],
"合計": [500, 500, 500]
}
df = pd.DataFrame(data)
print("=== A/B/Cテスト結果 ===")
print(df.to_string(index=False))
# クロス集計表
observed = np.array([[45, 455],
[63, 437],
[58, 442]])
chi2, p_value, dof, expected = stats.chi2_contingency(observed)
print(f"\nカイ二乗値: {chi2:.4f}")
print(f"自由度: {dof}")
print(f"p値: {p_value:.6f}")
if p_value < 0.05:
print("→ 3つのデザイン間でCVRに有意な差がある")
# どのペアに差があるかは多重比較が必要(第7回で解説)
else:
print("→ 3つのデザイン間でCVRに有意な差があるとは言えない")
補足:検定に関する実践的な注意点
検定結果の正しい表現
| NG表現 | OK表現 |
|---|---|
| 「帰無仮説が正しいことが証明された」 | 「帰無仮説を棄却できなかった」 |
| 「差がないことが証明された」 | 「有意な差は認められなかった」 |
| 「100%差がある」 | 「有意水準5%で統計的に有意な差がある」 |
「帰無仮説を棄却できない」は「帰無仮説が正しい」こととは異なります。「差がないと断言できる」のではなく、「この標本データからは差があるとは言えない」というのが正確な解釈です。
効果量(Effect Size)
p値は標本サイズに大きく影響されます。大きなサンプルサイズがあれば、実質的に意味のない小さな差でも「有意」と判定されてしまいます。そのため、p値とあわせて効果量を報告することが推奨されています。
| 効果量指標 | 用途 | 小 | 中 | 大 |
|---|---|---|---|---|
| Cohen's d | 2群の平均差 | 0.2 | 0.5 | 0.8 |
| η²(イータ二乗) | 分散分析 | 0.01 | 0.06 | 0.14 |
| Cramér's V | カイ二乗検定 | 0.1 | 0.3 | 0.5 |
import numpy as np
def cohens_d(group1, group2):
"""Cohen's d(効果量)を計算"""
n1, n2 = len(group1), len(group2)
var1, var2 = group1.var(ddof=1), group2.var(ddof=1)
# プールされた標準偏差
pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
return (group1.mean() - group2.mean()) / pooled_std
# 先ほどの理系・文系のデータで効果量を計算
science_scores = np.array([78, 85, 92, 88, 76, 90, 84, 95, 82, 87,
91, 79, 86, 93, 80, 88, 84, 90, 77, 85])
liberal_arts_scores = np.array([65, 72, 58, 70, 68, 75, 62, 71, 66, 73,
60, 69, 74, 63, 67, 70, 64, 72, 61, 68])
d = cohens_d(science_scores, liberal_arts_scores)
print(f"Cohen's d: {d:.4f}")
if abs(d) < 0.2:
print("→ 効果量: ほとんどなし")
elif abs(d) < 0.5:
print("→ 効果量: 小")
elif abs(d) < 0.8:
print("→ 効果量: 中")
else:
print("→ 効果量: 大")
検定手法の選択ガイド
データの種類を確認
├── 量的データ
│ ├── 1群 → 1標本t検定
│ ├── 2群
│ │ ├── 対応あり → 対応のあるt検定
│ │ └── 対応なし
│ │ ├── 正規分布に従う → 独立t検定
│ │ └── 正規分布に従わない → Mann-Whitney U検定
│ └── 3群以上 → 分散分析(第7回で解説)
│
└── 質的データ(カテゴリ)
├── 1変数 → カイ二乗適合度検定
├── 2変数の独立性 → カイ二乗独立性検定
└── 比率の検定 → 二項検定(binomtest)
多重検定の問題
複数の検定を同時に行うと、第1種の誤り(偽陽性)の確率が増大します。
# 多重検定の問題の例
# 20回検定を行うと、全て帰無仮説が正しくても1回は「有意」と出る確率
alpha = 0.05
n_tests = 20
prob_at_least_one_false_positive = 1 - (1 - alpha) ** n_tests
print(f"{n_tests}回検定を行った場合:")
print(f"少なくとも1回偽陽性が出る確率: {prob_at_least_one_false_positive:.2%}")
# → 約64%!
# ボンフェローニ補正
alpha_corrected = alpha / n_tests
print(f"\nボンフェローニ補正後の有意水準: {alpha_corrected:.4f}")
print("→ 各検定のp値がこの値を下回った場合のみ有意と判断")
ノンパラメトリック検定
データが正規分布に従わない場合や、サンプルサイズが小さい場合は、t検定の代わりにノンパラメトリック検定を使用します。
| パラメトリック検定 | ノンパラメトリック検定 | scipy関数 |
|---|---|---|
| 1標本t検定 | ウィルコクソンの符号順位検定 | stats.wilcoxon() |
| 独立t検定 | マン・ホイットニーU検定 | stats.mannwhitneyu() |
| 対応t検定 | ウィルコクソンの符号順位検定 | stats.wilcoxon() |
from scipy import stats
import numpy as np
# マン・ホイットニーU検定(正規性を仮定しない)
group_a = np.array([3, 5, 7, 2, 8, 4, 6, 9, 1, 5])
group_b = np.array([6, 8, 10, 7, 9, 11, 5, 12, 8, 7])
stat, p_value = stats.mannwhitneyu(group_a, group_b, alternative='two-sided')
print(f"マン・ホイットニーU検定:")
print(f" 統計量: {stat:.4f}")
print(f" p値: {p_value:.6f}")
まとめ
本記事では、統計的検定として以下の内容を解説しました。
| 項目 | ポイント |
|---|---|
| 仮説検定とは | 帰無仮説を立て、データからそれを棄却できるか判断する |
| p値 | 帰無仮説が真の場合に観測データが得られる確率 |
| 有意水準 | 帰無仮説を棄却する基準(通常5%) |
| t検定(1標本) | 平均値が仮説値と異なるか検定 |
| 対応のあるt検定 | 同一対象の前後の差を検定 |
| 独立t検定 | 2つの独立した群の平均差を検定 |
| 比率の検定 | 母集団の比率が想定値と異なるか検定 |
| カイ二乗検定 | カテゴリカルデータの関連性・適合性を検定 |
検定のチェックリスト
- 帰無仮説と対立仮説を明確に設定したか
- 有意水準を事前に決めたか(通常5%)
- データの種類(量的/質的)を確認したか
- 適切な検定手法を選択したか
- 前提条件(正規性、等分散性)を確認したか
- p値だけでなく効果量も確認したか
- 多重検定の問題に対処したか
- 結果の解釈を正しく行っているか
シリーズ記事一覧
- 【第1回】Pythonで学ぶデータ分析実践 〜データ分析とは〜
- 【第2回】データの前処理(欠損値・外れ値・ダミー変数・データスケーリングなど)
- 【第3回】データの把握(基本統計量・ヒストグラム・箱ひげ図・散布図など)
- 【第4回】相関分析(共分散・相関係数・相関比・連関係数など)
- 【第5回】統計的推定(母集団と標本・点推定・区間推定・信頼区間)
- 【第6回】統計的検定(仮説検定・p値・t検定・カイ二乗検定など)
- 【第7回】分散分析(一元分散分析・二元分散分析・多重比較)
- 【第8回】回帰分析(単回帰・重回帰・一般化線形モデル・モデル評価)
- 【第9回】時系列データ分析(トレンド・季節性・ARIMA・SARIMAなど)
- 【第10回】クラス分類(決定木・ランダムフォレスト・ロジスティック回帰など)
- 【第11回】クラスタリング(k-means・階層クラスタリング・エルボー法など)
- 【第12回】次元削減(主成分分析・寄与率・可視化)
- 【発展編】分析精度の向上(特徴量エンジニアリング・ハイパーパラメータ調整・アンサンブル学習など)
- 【発展編】ニューラルネットワーク入門(パーセプトロン・誤差逆伝播法・TensorFlow・Kerasなど)
次回の第7回では、3群以上の平均値の差を検定する分散分析について解説します。一元分散分析、二元分散分析、多重比較法(どのグループ間に差があるか特定する方法)を、Pythonのコードとともに学びます。