t検定の理論と実装を完全解説(1標本・2標本・対応あり)

t検定は、母集団の平均に関する仮説を検定するための最も基本的で広く使われる統計手法です。z検定が母分散既知を前提とするのに対し、t検定は 母分散が未知 の場合に適用できるため、実際のデータ分析において圧倒的に使用頻度が高い手法です。

t検定の理論はウィリアム・シーリー・ゴセット(ペンネーム: Student)が1908年にギネスビール醸造所での品質管理のために開発しました。母分散を標本分散で置き換えたときに検定統計量がt分布に従うという発見が、現代統計学の発展に大きく貢献しました。本記事では、t分布の導出から3種類のt検定(1標本、対応あり、独立2標本)を網羅的に解説し、Pythonで実装します。

本記事の内容

  • t分布の導出と性質
  • 1標本t検定の理論と実装
  • 対応ありt検定
  • 独立2標本t検定(等分散・非等分散)
  • 具体的な計算例
  • Python scipy.stats による実装

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

t分布とは

z検定との違い

母分散 $\sigma^2$ が既知のとき、検定統計量 $Z = \frac{\bar{X} – \mu_0}{\sigma/\sqrt{n}}$ は $N(0,1)$ に従います(z検定)。しかし、実際には母分散は未知であることがほとんどです。母分散を標本分散 $S^2$ で置き換えた統計量、

$$ T = \frac{\bar{X} – \mu_0}{S/\sqrt{n}} $$

は、もはや正規分布には従わず、t分布 に従います。

t分布の導出

$X_1, \dots, X_n \sim N(\mu, \sigma^2)$ とします。以下の2つの統計量を考えます。

$$ Z = \frac{\bar{X} – \mu}{\sigma / \sqrt{n}} \sim N(0, 1) $$

$$ V = \frac{(n-1)S^2}{\sigma^2} = \frac{\sum_{i=1}^{n}(X_i – \bar{X})^2}{\sigma^2} \sim \chi^2(n-1) $$

ここで $S^2 = \frac{1}{n-1}\sum_{i=1}^{n}(X_i – \bar{X})^2$ は不偏分散です。

$Z$ と $V$ が独立(正規分布の性質から示される)であることを用いると、

$$ \begin{align} T &= \frac{\bar{X} – \mu}{S/\sqrt{n}} = \frac{(\bar{X} – \mu) / (\sigma/\sqrt{n})}{S/\sigma} \\ &= \frac{Z}{\sqrt{V/(n-1)}} \end{align} $$

これは自由度 $\nu = n – 1$ のt分布の定義そのものです。

t分布の定義

標準正規分布に従う確率変数 $Z$ と、自由度 $\nu$ のカイ二乗分布に従う確率変数 $V$ が独立であるとき、

$$ \begin{equation} T = \frac{Z}{\sqrt{V/\nu}} \sim t(\nu) \end{equation} $$

を自由度 $\nu$ の t分布 と呼びます。

t分布の確率密度関数

$$ \begin{equation} f(t) = \frac{\Gamma\left(\frac{\nu+1}{2}\right)}{\sqrt{\nu\pi} \, \Gamma\left(\frac{\nu}{2}\right)} \left(1 + \frac{t^2}{\nu}\right)^{-\frac{\nu+1}{2}} \end{equation} $$

ここで $\Gamma(\cdot)$ はガンマ関数です。

t分布の性質

性質 内容
対称性 $t = 0$ に関して対称
裾の重さ 正規分布より裾が重い(外れ値が出やすい)
自由度依存 $\nu$ が小さいほど裾が重い
正規分布への収束 $\nu \to \infty$ で $t(\nu) \to N(0,1)$
期待値 $E[T] = 0$($\nu > 1$)
分散 $\text{Var}(T) = \frac{\nu}{\nu – 2}$($\nu > 2$)

1標本t検定

問題設定

1つの母集団から抽出した標本に基づいて、母平均 $\mu$ が特定の値 $\mu_0$ であるかを検定します。

  • $H_0: \mu = \mu_0$
  • $H_1: \mu \neq \mu_0$(両側), $\mu > \mu_0$(右片側), $\mu < \mu_0$(左片側)

検定統計量

$$ \begin{equation} T = \frac{\bar{X} – \mu_0}{S / \sqrt{n}} \end{equation} $$

$H_0$ のもとで $T \sim t(n-1)$ に従います。

検定の手順

  1. 有意水準 $\alpha$ を設定する
  2. 検定統計量 $T$ を計算する
  3. 自由度 $n-1$ のt分布からp値を計算する
  4. $p \leq \alpha$ なら $H_0$ を棄却する

具体例

ある工場で生産される部品の直径が仕様値 $\mu_0 = 10.0$ mm であるか検定します。16個の標本を測定したところ、$\bar{x} = 10.3$ mm, $s = 0.8$ mm でした。

$$ \begin{align} T &= \frac{10.3 – 10.0}{0.8 / \sqrt{16}} = \frac{0.3}{0.2} = 1.5 \end{align} $$

自由度 $\nu = 15$ のt分布で両側p値を計算すると、

$$ p = 2 \times P(T \geq 1.5) = 2 \times (1 – F_{t(15)}(1.5)) \approx 0.155 $$

$p = 0.155 > 0.05$ なので $H_0$ を棄却できません。仕様値からの有意なずれは検出されませんでした。

対応ありt検定(Paired t-test)

問題設定

同じ対象に対して2回の測定を行い(処理前後、左右、など)、差に系統的な変化があるかを検定します。

  • 測定ペア: $(X_{1,1}, X_{1,2}), (X_{2,1}, X_{2,2}), \dots, (X_{n,1}, X_{n,2})$
  • 差: $D_i = X_{i,2} – X_{i,1}$
  • $H_0: \mu_D = 0$(差の母平均がゼロ)
  • $H_1: \mu_D \neq 0$

検定統計量

差 $D_i$ に対する1標本t検定に帰着します。

$$ \begin{equation} T = \frac{\bar{D}}{S_D / \sqrt{n}} \end{equation} $$

ここで $\bar{D} = \frac{1}{n}\sum_{i=1}^{n} D_i$、$S_D = \sqrt{\frac{1}{n-1}\sum_{i=1}^{n}(D_i – \bar{D})^2}$ です。

$H_0$ のもとで $T \sim t(n-1)$ です。

対応ありt検定の利点

対応ありt検定は、個体差を差し引いた上で処理効果を評価するため、独立2標本t検定よりも一般に 検出力が高い です。これは、差 $D_i$ の分散が個体差の変動を除去した分だけ小さくなるためです。

独立2標本t検定

問題設定

2つの独立な母集団から抽出した標本に基づいて、2つの母平均に差があるかを検定します。

  • 群1: $X_{1,1}, \dots, X_{1,n_1} \sim N(\mu_1, \sigma_1^2)$
  • 群2: $X_{2,1}, \dots, X_{2,n_2} \sim N(\mu_2, \sigma_2^2)$
  • $H_0: \mu_1 = \mu_2$
  • $H_1: \mu_1 \neq \mu_2$

ケース1: 等分散を仮定(Student’s t-test)

$\sigma_1^2 = \sigma_2^2 = \sigma^2$ を仮定します。プールした分散を用います。

$$ \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} $$

検定統計量は、

$$ \begin{equation} T = \frac{\bar{X}_1 – \bar{X}_2}{S_p \sqrt{\frac{1}{n_1} + \frac{1}{n_2}}} \end{equation} $$

$H_0$ のもとで $T \sim t(n_1 + n_2 – 2)$ です。

ケース2: 等分散を仮定しない(Welch’s t-test)

$\sigma_1^2 \neq \sigma_2^2$ の場合(ウェルチのt検定)。実用上はこちらが推奨されます。

$$ \begin{equation} T = \frac{\bar{X}_1 – \bar{X}_2}{\sqrt{\frac{S_1^2}{n_1} + \frac{S_2^2}{n_2}}} \end{equation} $$

自由度は ウェルチ-サタスウェイトの近似 を用います。

$$ \begin{equation} \nu = \frac{\left(\frac{S_1^2}{n_1} + \frac{S_2^2}{n_2}\right)^2}{\frac{(S_1^2/n_1)^2}{n_1 – 1} + \frac{(S_2^2/n_2)^2}{n_2 – 1}} \end{equation} $$

一般に $\nu$ は整数にはなりませんが、t分布の自由度パラメータは実数値でも定義されます。

等分散の検定

等分散を仮定するかどうかの判断のために、F検定やレーベン検定が用いられますが、実用上は 常にウェルチのt検定を使う ことが多くの教科書で推奨されています。等分散が成り立つ場合でも、ウェルチの検定は Student の検定とほぼ同じ結果を与えるためです。

具体例: 独立2標本t検定

新薬群と対照群で血圧の低下量を比較します。

新薬群 対照群
$n$ 20 18
$\bar{x}$ 12.5 9.8
$s$ 3.2 2.9

ウェルチのt検定を適用します。

$$ \begin{align} T &= \frac{12.5 – 9.8}{\sqrt{\frac{3.2^2}{20} + \frac{2.9^2}{18}}} = \frac{2.7}{\sqrt{\frac{10.24}{20} + \frac{8.41}{18}}} = \frac{2.7}{\sqrt{0.512 + 0.467}} = \frac{2.7}{\sqrt{0.979}} = \frac{2.7}{0.989} \approx 2.73 \end{align} $$

自由度:

$$ \begin{align} \nu &= \frac{(0.512 + 0.467)^2}{\frac{0.512^2}{19} + \frac{0.467^2}{17}} = \frac{0.979^2}{\frac{0.2621}{19} + \frac{0.2181}{17}} = \frac{0.958}{0.01380 + 0.01283} = \frac{0.958}{0.02663} \approx 35.97 \end{align} $$

自由度約36のt分布で $T = 2.73$ の両側p値は $p \approx 0.010$ です。$p < 0.05$ なので、新薬群と対照群で血圧の低下量に統計的に有意な差があると結論づけます。

Pythonでの実装

t分布の可視化

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

x = np.linspace(-5, 5, 1000)

fig, ax = plt.subplots(figsize=(10, 6))

# 異なる自由度のt分布
for nu in [1, 3, 5, 10, 30]:
    ax.plot(x, stats.t.pdf(x, df=nu), linewidth=2, label=f'$t({nu})$')

# 標準正規分布
ax.plot(x, stats.norm.pdf(x), 'k--', linewidth=2, label='$N(0,1)$')

ax.set_xlabel('t', fontsize=12)
ax.set_ylabel('Density', fontsize=12)
ax.set_title("Student's t-Distribution for Various Degrees of Freedom", fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 0.42)
plt.savefig("t_distribution.png", dpi=150, bbox_inches="tight")
plt.show()

自由度が小さいほど裾が重く(外れ値が出やすく)、自由度が大きくなるにつれて標準正規分布(黒い破線)に近づいていく様子が確認できます。

1標本t検定の実装

import numpy as np
from scipy import stats

np.random.seed(42)

# データの生成(母平均 = 10.3 の正規分布)
data = np.random.normal(loc=10.3, scale=0.8, size=16)

# 仕様値
mu0 = 10.0

# スクラッチ実装
n = len(data)
x_bar = data.mean()
s = data.std(ddof=1)  # 不偏標準偏差
t_stat = (x_bar - mu0) / (s / np.sqrt(n))
df = n - 1
p_value = 2 * (1 - stats.t.cdf(abs(t_stat), df))  # 両側検定

print("=== 1-Sample t-test (from scratch) ===")
print(f"Sample mean: {x_bar:.4f}")
print(f"Sample std: {s:.4f}")
print(f"t-statistic: {t_stat:.4f}")
print(f"Degrees of freedom: {df}")
print(f"p-value (two-sided): {p_value:.4f}")

# scipy による検定
t_scipy, p_scipy = stats.ttest_1samp(data, mu0)
print(f"\n=== scipy.stats.ttest_1samp ===")
print(f"t-statistic: {t_scipy:.4f}")
print(f"p-value: {p_scipy:.4f}")

# 95%信頼区間
t_crit = stats.t.ppf(0.975, df)
margin = t_crit * s / np.sqrt(n)
ci = (x_bar - margin, x_bar + margin)
print(f"\n95% Confidence Interval: ({ci[0]:.4f}, {ci[1]:.4f})")

対応ありt検定の実装

import numpy as np
from scipy import stats

np.random.seed(42)

# データの生成: 処理前後の測定値
n = 12
before = np.random.normal(loc=120, scale=10, size=n)  # 処理前の血圧
effect = np.random.normal(loc=5, scale=3, size=n)      # 処理効果
after = before - effect                                  # 処理後の血圧

# 差の計算
diff = after - before  # 負の値 = 血圧低下

print("Before:", np.round(before, 1))
print("After:", np.round(after, 1))
print("Diff:", np.round(diff, 1))

# スクラッチ実装
d_bar = diff.mean()
s_d = diff.std(ddof=1)
t_stat = d_bar / (s_d / np.sqrt(n))
df = n - 1
p_value = 2 * (1 - stats.t.cdf(abs(t_stat), df))

print(f"\n=== Paired t-test (from scratch) ===")
print(f"Mean difference: {d_bar:.4f}")
print(f"Std of differences: {s_d:.4f}")
print(f"t-statistic: {t_stat:.4f}")
print(f"Degrees of freedom: {df}")
print(f"p-value: {p_value:.4f}")

# scipy による検定
t_scipy, p_scipy = stats.ttest_rel(after, before)
print(f"\n=== scipy.stats.ttest_rel ===")
print(f"t-statistic: {t_scipy:.4f}")
print(f"p-value: {p_scipy:.4f}")

独立2標本t検定の実装

import numpy as np
from scipy import stats

np.random.seed(42)

# 2群のデータ生成
group1 = np.random.normal(loc=12.5, scale=3.2, size=20)  # 新薬群
group2 = np.random.normal(loc=9.8, scale=2.9, size=18)   # 対照群

print(f"Group 1: n={len(group1)}, mean={group1.mean():.3f}, std={group1.std(ddof=1):.3f}")
print(f"Group 2: n={len(group2)}, mean={group2.mean():.3f}, std={group2.std(ddof=1):.3f}")

# --- Student's t-test(等分散仮定)---
n1, n2 = len(group1), len(group2)
s1, s2 = group1.std(ddof=1), group2.std(ddof=1)

# プールした分散
sp2 = ((n1 - 1) * s1**2 + (n2 - 1) * s2**2) / (n1 + n2 - 2)
sp = np.sqrt(sp2)
t_student = (group1.mean() - group2.mean()) / (sp * np.sqrt(1/n1 + 1/n2))
df_student = n1 + n2 - 2
p_student = 2 * (1 - stats.t.cdf(abs(t_student), df_student))

print(f"\n=== Student's t-test (equal variance) ===")
print(f"Pooled std: {sp:.4f}")
print(f"t-statistic: {t_student:.4f}")
print(f"df: {df_student}")
print(f"p-value: {p_student:.4f}")

# --- Welch's t-test(等分散仮定なし)---
se = np.sqrt(s1**2/n1 + s2**2/n2)
t_welch = (group1.mean() - group2.mean()) / se

# ウェルチ-サタスウェイトの自由度
df_welch = (s1**2/n1 + s2**2/n2)**2 / ((s1**2/n1)**2/(n1-1) + (s2**2/n2)**2/(n2-1))
p_welch = 2 * (1 - stats.t.cdf(abs(t_welch), df_welch))

print(f"\n=== Welch's t-test (unequal variance) ===")
print(f"t-statistic: {t_welch:.4f}")
print(f"df: {df_welch:.2f}")
print(f"p-value: {p_welch:.4f}")

# scipy による検定
t_scipy_eq, p_scipy_eq = stats.ttest_ind(group1, group2, equal_var=True)
t_scipy_welch, p_scipy_welch = stats.ttest_ind(group1, group2, equal_var=False)

print(f"\n=== scipy verification ===")
print(f"Student: t={t_scipy_eq:.4f}, p={p_scipy_eq:.4f}")
print(f"Welch:   t={t_scipy_welch:.4f}, p={p_scipy_welch:.4f}")

3種類のt検定の可視化

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# (a) 1標本t検定
data = np.random.normal(10.3, 0.8, 16)
mu0 = 10.0
t_stat, p_val = stats.ttest_1samp(data, mu0)

ax = axes[0]
ax.hist(data, bins=8, density=True, alpha=0.7, color='skyblue', edgecolor='white')
ax.axvline(x=mu0, color='red', linewidth=2, linestyle='--', label=f'$\\mu_0$ = {mu0}')
ax.axvline(x=data.mean(), color='blue', linewidth=2, label=f'$\\bar{{x}}$ = {data.mean():.2f}')
ax.set_title(f'One-sample t-test\nt = {t_stat:.3f}, p = {p_val:.3f}', fontsize=12)
ax.set_xlabel('Value')
ax.legend()
ax.grid(True, alpha=0.3)

# (b) 対応ありt検定
before = np.random.normal(120, 10, 15)
after = before - np.random.normal(5, 3, 15)
t_stat, p_val = stats.ttest_rel(before, after)

ax = axes[1]
for i in range(len(before)):
    ax.plot([0, 1], [before[i], after[i]], 'gray', alpha=0.5, linewidth=0.8)
ax.plot([0] * len(before), before, 'ro', markersize=6, label='Before')
ax.plot([1] * len(after), after, 'bo', markersize=6, label='After')
ax.plot(0, before.mean(), 'r^', markersize=15)
ax.plot(1, after.mean(), 'b^', markersize=15)
ax.set_xticks([0, 1])
ax.set_xticklabels(['Before', 'After'])
ax.set_title(f'Paired t-test\nt = {t_stat:.3f}, p = {p_val:.3f}', fontsize=12)
ax.legend()
ax.grid(True, alpha=0.3)

# (c) 独立2標本t検定
g1 = np.random.normal(12.5, 3.2, 20)
g2 = np.random.normal(9.8, 2.9, 18)
t_stat, p_val = stats.ttest_ind(g1, g2, equal_var=False)

ax = axes[2]
bp = ax.boxplot([g1, g2], labels=['Drug', 'Control'], patch_artist=True,
                boxprops=dict(alpha=0.7))
bp['boxes'][0].set_facecolor('lightcoral')
bp['boxes'][1].set_facecolor('lightblue')
# 個々のデータ点をプロット
np.random.seed(0)
jitter1 = 1 + 0.05 * np.random.randn(len(g1))
jitter2 = 2 + 0.05 * np.random.randn(len(g2))
ax.scatter(jitter1, g1, alpha=0.5, color='red', s=20)
ax.scatter(jitter2, g2, alpha=0.5, color='blue', s=20)
ax.set_title(f"Welch's t-test\nt = {t_stat:.3f}, p = {p_val:.3f}", fontsize=12)
ax.set_ylabel('Value')
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("three_t_tests.png", dpi=150, bbox_inches="tight")
plt.show()

3種類のt検定を視覚的に比較しています。1標本t検定は仕様値(赤い破線)と標本平均の比較、対応ありt検定はペアのデータの変化、独立2標本t検定は2群の分布の比較を行います。

検定のシミュレーションと第1種の過誤率の検証

import numpy as np
from scipy import stats

np.random.seed(42)

# H0が真の場合のシミュレーション
n_simulations = 10000
alpha = 0.05
sample_sizes = [5, 10, 20, 30, 50, 100]

print("=== Type I Error Rate Verification ===")
print(f"{'n':>5s} | {'1-sample':>10s} | {'Paired':>10s} | {'2-sample':>10s}")
print("-" * 45)

for n in sample_sizes:
    reject_1samp = 0
    reject_paired = 0
    reject_2samp = 0

    for _ in range(n_simulations):
        # 1標本t検定: H0: mu = 0, 真のmu = 0
        x = np.random.normal(0, 1, n)
        _, p = stats.ttest_1samp(x, 0)
        if p < alpha:
            reject_1samp += 1

        # 対応ありt検定: H0: mu_D = 0, 真の差 = 0
        before = np.random.normal(0, 1, n)
        after = before + np.random.normal(0, 1, n)  # 差の期待値 = 0
        _, p = stats.ttest_rel(before, after)
        if p < alpha:
            reject_paired += 1

        # 独立2標本t検定: H0: mu1 = mu2, 真の差 = 0
        g1 = np.random.normal(0, 1, n)
        g2 = np.random.normal(0, 1, n)
        _, p = stats.ttest_ind(g1, g2, equal_var=False)
        if p < alpha:
            reject_2samp += 1

    print(f"{n:>5d} | {reject_1samp/n_simulations:>10.4f} | "
          f"{reject_paired/n_simulations:>10.4f} | {reject_2samp/n_simulations:>10.4f}")

print(f"\nExpected Type I Error Rate: {alpha}")

すべてのサンプルサイズにおいて、第1種の過誤率が設定した有意水準 $\alpha = 0.05$ にほぼ等しいことが確認できます。これはt検定が正しく第1種の過誤を制御していることを意味します。

効果量とサンプルサイズの関係

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# 必要サンプルサイズの計算(1標本t検定、両側、power = 0.8)
alpha = 0.05
power_target = 0.8

effect_sizes = np.linspace(0.1, 2.0, 100)
required_n = []

for d in effect_sizes:
    # 二分探索でnを求める
    n_low, n_high = 3, 10000
    while n_high - n_low > 1:
        n_mid = (n_low + n_high) // 2
        # 非心t分布によるpower計算
        t_crit = stats.t.ppf(1 - alpha / 2, n_mid - 1)
        ncp = d * np.sqrt(n_mid)  # 非心度パラメータ
        power = 1 - stats.nct.cdf(t_crit, n_mid - 1, ncp) + stats.nct.cdf(-t_crit, n_mid - 1, ncp)
        if power >= power_target:
            n_high = n_mid
        else:
            n_low = n_mid
    required_n.append(n_high)

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(effect_sizes, required_n, 'b-', linewidth=2)
ax.set_xlabel("Cohen's d (Effect Size)", fontsize=12)
ax.set_ylabel('Required Sample Size (n)', fontsize=12)
ax.set_title(f'Required Sample Size for Power = {power_target} ($\\alpha$ = {alpha})', fontsize=14)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 500)

# 代表的な効果量をアノテーション
for d, label in [(0.2, 'Small'), (0.5, 'Medium'), (0.8, 'Large')]:
    idx = np.argmin(np.abs(np.array(effect_sizes) - d))
    n_req = required_n[idx]
    ax.annotate(f'{label}\nd={d}, n={n_req}',
                xy=(d, n_req), xytext=(d + 0.3, n_req + 50),
                arrowprops=dict(arrowstyle='->', color='black'),
                fontsize=10, ha='center')

plt.savefig("sample_size_vs_effect_size.png", dpi=150, bbox_inches="tight")
plt.show()

効果量が小さいほど、目標の検出力を達成するために必要なサンプルサイズが急激に増えます。Cohen’s d = 0.2(小さな効果)では約200サンプル、d = 0.5(中程度の効果)では約34サンプル、d = 0.8(大きな効果)では約15サンプルが必要であることがわかります。

まとめ

本記事では、t検定の理論と実装について解説しました。

  • t分布は、母分散を標本分散で置き換えたときに検定統計量が従う分布で、正規分布より裾が重いです
  • 1標本t検定: 1つの母集団の平均と指定値の比較に使います
  • 対応ありt検定: ペアデータの差に対する1標本t検定に帰着し、個体差を除去できるため検出力が高いです
  • 独立2標本t検定: 2つの独立な群の平均を比較します。等分散を仮定しないウェルチのt検定が推奨されます
  • ウェルチの自由度はサタスウェイト近似により計算されます
  • 検出力は効果量とサンプルサイズに依存し、研究計画段階でのサンプルサイズ設計が重要です

次のステップとして、以下の記事も参考にしてください。