AとBの2つの教授法のどちらが効果的かを調べたいとします。30人ずつのクラスにそれぞれの教授法を適用し、期末テストの成績を比較します。もしAクラスの平均点が75点、Bクラスが70点だったとき、この5点の差は「教授法の違いによる本当の差」なのか、「クラスの学生のばらつきによるもの」なのかを区別する必要があります。
2つのグループの平均を比較するための検定が2標本t検定(two-sample t-test)です。しかし、ここで重要な判断が必要になります。2つのグループのデータは独立なのか、それとも対応関係があるのか。そして、2つのグループの分散は等しいと仮定できるのか。
2標本t検定を理解すると、以下の場面で適切な比較ができます。
- 臨床試験: 治療群とプラセボ群の効果の比較
- 教育: 2つの教授法の効果の比較
- 工学: 2つの製造方法による品質の比較
- ビジネス: A/Bテストにおける2群の比較
本記事の内容
- 独立2標本t検定(等分散の仮定あり/なし)
- 対応のある2標本t検定
- 3つの検定の使い分け
- 等分散性の検定(F検定、ルヴェーン検定)
- Pythonでの実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
独立2標本t検定
問題設定
2つの独立な正規母集団から標本が得られたとします。
$$ X_1, \ldots, X_{n_1} \sim N(\mu_1, \sigma_1^2), \quad Y_1, \ldots, Y_{n_2} \sim N(\mu_2, \sigma_2^2) $$
検定したい仮説は
$$ H_0: \mu_1 = \mu_2 \quad \text{vs} \quad H_1: \mu_1 \neq \mu_2 $$
平均の差 $\Delta = \mu_1 – \mu_2$ がゼロかどうかを検定します。「独立」とは、2つのグループのデータが互いに影響し合わないことを意味します。
ここで、$\sigma_1^2$ と $\sigma_2^2$ が等しいかどうかで検定の方法が分かれます。
等分散を仮定する場合(Student’s t検定)
$\sigma_1^2 = \sigma_2^2 = \sigma^2$ と仮定するとき、プールされた分散(pooled variance)を使います。
$$ \begin{equation} S_p^2 = \frac{(n_1 – 1)S_1^2 + (n_2 – 1)S_2^2}{n_1 + n_2 – 2} \end{equation} $$
2つのグループの分散推定量を自由度で重み付けして合算しています。直感的には、同じ母分散を持つ2つのグループから得られた情報を一つにまとめることで、分散の推定精度が向上します。
検定統計量は
$$ \begin{equation} T = \frac{\bar{X} – \bar{Y}}{S_p\sqrt{1/n_1 + 1/n_2}} \sim t_{n_1 + n_2 – 2} \end{equation} $$
自由度は $n_1 + n_2 – 2$ です。
等分散を仮定しない場合(ウェルチのt検定)
$\sigma_1^2 \neq \sigma_2^2$ のとき、ウェルチのt検定(Welch’s t-test)を使います。
$$ \begin{equation} T = \frac{\bar{X} – \bar{Y}}{\sqrt{S_1^2/n_1 + S_2^2/n_2}} \end{equation} $$
この統計量は厳密にはt分布に従いませんが、サターウェイトの近似により近似的な自由度
$$ \nu = \frac{(S_1^2/n_1 + S_2^2/n_2)^2}{\frac{(S_1^2/n_1)^2}{n_1-1} + \frac{(S_2^2/n_2)^2}{n_2-1}} \end{equation} $$
を使って $T \approx t_\nu$ と近似します。$\nu$ は一般に整数にはならず、$\min(n_1, n_2) – 1$ と $n_1 + n_2 – 2$ の間の値を取ります。
どちらを使うべきか
実用上の推奨: 等分散が確信できない限り、ウェルチのt検定を使うのが安全です。
- 等分散のとき: ウェルチのt検定はStudent’s t検定とほぼ同じ結果を与える(検出力の損失はわずか)
- 等分散でないとき: Student’s t検定は第一種の過誤率が公称値からずれる(危険)
つまり、ウェルチのt検定は等分散・非等分散のどちらでも適切に動作する「安全な」選択です。
独立な2群の比較を見てきましたが、データに対応関係がある場合は別の方法が必要です。
対応のある2標本t検定
問題設定
同じ被験者から「処理前」と「処理後」の2つの測定値が得られる場合、データには対応関係があります。
例: 同じ学生の「講義前テスト」と「講義後テスト」のスコア
この場合、差 $D_i = X_i – Y_i$ を計算し、1標本t検定を適用します。
$$ \begin{equation} T = \frac{\bar{D}}{S_D/\sqrt{n}} \sim t_{n-1} \end{equation} $$
ここで $\bar{D} = \frac{1}{n}\sum D_i$、$S_D = \sqrt{\frac{1}{n-1}\sum(D_i – \bar{D})^2}$ です。
対応のある検定の利点
対応のある検定は、被験者間の個人差を除去するため、検出力が高くなります。
$\text{Var}[\bar{X} – \bar{Y}]_{\text{独立}} = \sigma_X^2/n + \sigma_Y^2/n$
$\text{Var}[\bar{D}]_{\text{対応}} = (\sigma_X^2 + \sigma_Y^2 – 2\rho\sigma_X\sigma_Y)/n$
$X$ と $Y$ の相関 $\rho$ が正のとき(通常はそう)、対応のある検定の分散の方が小さく、検出力が向上します。
3つの検定方法を整理したところで、Pythonで実装と比較をしましょう。
Pythonでの実装と可視化
3種類のt検定の比較
同じデータに対して3種類のt検定を適用し、結果の違いを確認します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
# データ生成
n = 25
mu1, mu2 = 105, 100
sigma1, sigma2 = 12, 8 # 分散が異なる
group1 = np.random.normal(mu1, sigma1, n)
group2 = np.random.normal(mu2, sigma2, n)
# 対応のあるデータ生成
# 各被験者にベースラインの個人差がある
baseline = np.random.normal(100, 15, n)
pre = baseline + np.random.normal(0, 3, n)
post = baseline + 5 + np.random.normal(0, 3, n) # 処理効果 = 5
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# (a) 独立2標本: Student vs Welch
ax = axes[0]
t_student, p_student = stats.ttest_ind(group1, group2, equal_var=True)
t_welch, p_welch = stats.ttest_ind(group1, group2, equal_var=False)
ax.boxplot([group1, group2], labels=['Group 1', 'Group 2'],
patch_artist=True,
boxprops=[dict(facecolor='lightblue'), dict(facecolor='lightsalmon')])
ax.set_ylabel('Value', fontsize=12)
ax.set_title(f"Independent t-tests\nStudent: t={t_student:.2f}, p={p_student:.4f}\n"
f"Welch: t={t_welch:.2f}, p={p_welch:.4f}", fontsize=11)
ax.grid(True, alpha=0.3, axis='y')
# 分散の情報
ax.text(0.02, 0.02, f'$s_1^2={np.var(group1, ddof=1):.1f}$, $s_2^2={np.var(group2, ddof=1):.1f}$\n'
f'Ratio = {np.var(group1, ddof=1)/np.var(group2, ddof=1):.2f}',
transform=ax.transAxes, fontsize=9,
bbox=dict(facecolor='lightyellow', alpha=0.8))
# (b) 対応ありt検定
ax = axes[1]
t_paired, p_paired = stats.ttest_rel(post, pre)
t_unpaired, p_unpaired = stats.ttest_ind(post, pre, equal_var=False)
for i in range(n):
ax.plot([0, 1], [pre[i], post[i]], 'gray', alpha=0.3, linewidth=1)
ax.scatter(np.zeros(n), pre, color='blue', s=30, zorder=5, label='Pre')
ax.scatter(np.ones(n), post, color='red', s=30, zorder=5, label='Post')
ax.set_xticks([0, 1])
ax.set_xticklabels(['Pre', 'Post'], fontsize=12)
ax.set_ylabel('Score', fontsize=12)
ax.set_title(f"Paired vs Independent\nPaired: t={t_paired:.2f}, p={p_paired:.4f}\n"
f"Indep.: t={t_unpaired:.2f}, p={p_unpaired:.4f}", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3, axis='y')
# (c) 分散比とStudent vs Welchの第一種の過誤率
ax = axes[2]
variance_ratios = [1, 2, 4, 8, 16]
n1, n2 = 15, 15
n_sim = 10000
error_student = []
error_welch = []
for ratio in variance_ratios:
s1, s2 = 1.0, np.sqrt(ratio)
rej_student = 0
rej_welch = 0
for _ in range(n_sim):
x = np.random.normal(0, s1, n1)
y = np.random.normal(0, s2, n2)
_, p_s = stats.ttest_ind(x, y, equal_var=True)
_, p_w = stats.ttest_ind(x, y, equal_var=False)
if p_s < 0.05:
rej_student += 1
if p_w < 0.05:
rej_welch += 1
error_student.append(rej_student / n_sim)
error_welch.append(rej_welch / n_sim)
x_pos = np.arange(len(variance_ratios))
width = 0.35
ax.bar(x_pos - width/2, error_student, width, color='blue', alpha=0.7,
label="Student's t")
ax.bar(x_pos + width/2, error_welch, width, color='red', alpha=0.7,
label="Welch's t")
ax.axhline(0.05, color='black', linewidth=1.5, linestyle='--',
label='Nominal $\\alpha = 0.05$')
ax.set_xlabel('Variance ratio $\\sigma_2^2/\\sigma_1^2$', fontsize=12)
ax.set_ylabel('Type I error rate', fontsize=12)
ax.set_title('Student vs Welch: Type I Error', fontsize=13)
ax.set_xticks(x_pos)
ax.set_xticklabels(variance_ratios)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig('two_sample_t_test.png', dpi=150, bbox_inches='tight')
plt.show()
このグラフから3種類のt検定の違いが明確にわかります。
-
左図: 2つのグループの箱ひげ図です。Student’s t検定とウェルチのt検定でp値がわずかに異なります。分散比が大きいため、ウェルチのt検定の方がより正確な推論を行っています
-
中央図: 対応のあるデータでは、各被験者の前後の値を線で結んでいます。対応のあるt検定($p = 0.0001$ 程度)は独立t検定($p$ がかなり大きい)よりずっと小さなp値を示しています。個人差を除去することで、わずかな処理効果も検出できるようになります
-
右図: 分散比が大きくなるとStudent’s t検定の第一種の過誤率が公称値0.05から大きく膨張しますが、ウェルチのt検定は一貫して0.05を維持しています。分散比が4倍以上のとき、Student’s t検定は信頼できません
検定の選択フローチャート
検定選択の実践的なガイドをPythonで実装します。
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
# 検定選択の自動判断
def choose_and_perform_t_test(group1, group2, paired=False, alpha=0.05):
"""2標本の比較を自動的に行う"""
results = {}
if paired:
if len(group1) != len(group2):
raise ValueError("対応ありの場合、2群のサイズは同じ必要があります")
t_stat, p_val = stats.ttest_rel(group1, group2)
results['test'] = 'Paired t-test'
results['t_stat'] = t_stat
results['p_value'] = p_val
diff = group1 - group2
results['effect_size'] = np.mean(diff) / np.std(diff, ddof=1) # Cohen's d
else:
# 等分散性の検定(ルヴェーン検定)
lev_stat, lev_p = stats.levene(group1, group2)
results['levene_p'] = lev_p
if lev_p > alpha:
# 等分散を仮定
t_stat, p_val = stats.ttest_ind(group1, group2, equal_var=True)
results['test'] = "Student's t-test (equal variance)"
else:
# 等分散を仮定しない
t_stat, p_val = stats.ttest_ind(group1, group2, equal_var=False)
results['test'] = "Welch's t-test (unequal variance)"
results['t_stat'] = t_stat
results['p_value'] = p_val
# Cohen's d
pooled_std = np.sqrt(((len(group1)-1)*np.var(group1, ddof=1) +
(len(group2)-1)*np.var(group2, ddof=1)) /
(len(group1) + len(group2) - 2))
results['effect_size'] = (np.mean(group1) - np.mean(group2)) / pooled_std
return results
# 3つのシナリオで比較
scenarios = [
{
'name': 'Scenario 1:\nEqual variance',
'g1': np.random.normal(105, 10, 30),
'g2': np.random.normal(100, 10, 30),
'paired': False
},
{
'name': 'Scenario 2:\nUnequal variance',
'g1': np.random.normal(105, 15, 30),
'g2': np.random.normal(100, 5, 30),
'paired': False
},
{
'name': 'Scenario 3:\nPaired data',
'g1': None,
'g2': None,
'paired': True
}
]
# Scenario 3のデータ生成
base = np.random.normal(100, 20, 30)
scenarios[2]['g1'] = base + 5 + np.random.normal(0, 3, 30)
scenarios[2]['g2'] = base + np.random.normal(0, 3, 30)
fig, axes = plt.subplots(1, 3, figsize=(16, 5.5))
for idx, scenario in enumerate(scenarios):
ax = axes[idx]
g1, g2 = scenario['g1'], scenario['g2']
result = choose_and_perform_t_test(g1, g2, paired=scenario['paired'])
# 可視化
if scenario['paired']:
for i in range(len(g1)):
ax.plot([1, 2], [g2[i], g1[i]], 'gray', alpha=0.3)
ax.scatter(np.ones(len(g2)), g2, s=30, color='blue', zorder=5)
ax.scatter(np.full(len(g1), 2), g1, s=30, color='red', zorder=5)
ax.set_xticks([1, 2])
ax.set_xticklabels(['Control', 'Treatment'])
else:
parts = ax.violinplot([g1, g2], positions=[1, 2], showmeans=True, showmedians=True)
for pc, color in zip(parts['bodies'], ['lightblue', 'lightsalmon']):
pc.set_facecolor(color)
pc.set_alpha(0.7)
ax.set_xticks([1, 2])
ax.set_xticklabels(['Group 1', 'Group 2'])
significance = '***' if result['p_value'] < 0.001 else '**' if result['p_value'] < 0.01 else '*' if result['p_value'] < 0.05 else 'n.s.'
ax.set_title(f"{scenario['name']}\n{result['test']}\n"
f"t={result['t_stat']:.2f}, p={result['p_value']:.4f} ({significance})\n"
f"Cohen's d = {result['effect_size']:.2f}",
fontsize=10)
ax.set_ylabel('Value', fontsize=11)
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig('two_sample_t_test_scenarios.png', dpi=150, bbox_inches='tight')
plt.show()
このグラフから、各シナリオに適した検定の選択がわかります。
-
シナリオ1(等分散): ルヴェーン検定で等分散が棄却されないため、Student’s t検定が選択されます。効果量 Cohen’s d と統計的有意性が報告されています
-
シナリオ2(不等分散): 分散比が大きいためウェルチのt検定が自動的に選択されます。不等分散のもとでも正しい第一種の過誤率を保証します
-
シナリオ3(対応あり): 対応のあるデータでは、個人差が除去されるため、同じ効果量でもp値が非常に小さくなり、検出力が向上しています
まとめ
本記事では、2標本t検定の3つの変種を解説しました。
- Student’s t検定は等分散を仮定し、プールされた分散を使う。等分散のとき最も検出力が高い
- ウェルチのt検定は等分散を仮定せず、サターウェイトの近似自由度を使う。等分散でも非等分散でも頑健で、デフォルトの選択として推奨される
- 対応のあるt検定は同一被験者の前後比較に使い、個人差を除去することで検出力が大幅に向上する
- 等分散の仮定が崩れると Student’s t検定の第一種の過誤率が膨張するが、ウェルチのt検定は影響を受けない
次のステップとして、以下の記事も参考にしてください。
- カイ二乗検定 — カテゴリカルデータの検定
- 一元配置分散分析 — 3群以上の比較
- ノンパラメトリック検定 — 正規性が仮定できない場合