有意水準とp値の正しい理解と使い方を解説

有意水準とp値は、仮説検定を行う上で最も頻繁に使われる概念です。しかし、p値は統計学の中でも特に誤解されやすい概念であり、「p値が小さいほど効果が大きい」「p < 0.05 なら真実」といった誤った解釈がしばしば見受けられます。

正しい統計的推論を行うためには、p値が何を意味し何を意味しないのかを厳密に理解する必要があります。本記事では、有意水準 $\alpha$ の設定の意味、p値の正確な定義、正しい解釈と誤った解釈、片側検定と両側検定の違い、そして複数の検定を同時に行う際の多重検定問題まで体系的に解説します。

本記事の内容

  • 有意水準 $\alpha$ の定義と設定
  • p値の数学的定義
  • p値の正しい解釈と誤った解釈
  • 片側検定と両側検定
  • 多重検定問題と補正法
  • Pythonでの可視化

前提知識

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

有意水準とは

定義

有意水準 $\alpha$ は、仮説検定において 帰無仮説 $H_0$ が真であるときに $H_0$ を誤って棄却する確率の上限 です。

$$ \begin{equation} \alpha = P(\text{$H_0$ を棄却} \mid H_0 \text{ が真}) \end{equation} $$

これは第1種の過誤の許容確率です。一般的には $\alpha = 0.05$(5%)が用いられ、他に $\alpha = 0.01$(1%)や $\alpha = 0.10$(10%)が使われることもあります。

有意水準の設定

有意水準はデータを見る前に設定しなければなりません。データを見た後に $\alpha$ を調整すると、検定の客観性が失われます。

$\alpha$ の値の選択は、第1種の過誤のコスト(重大さ)に依存します。

分野 典型的な $\alpha$ 理由
社会科学 0.05 標準的な慣行
新薬の承認 0.01 or 0.005 偽の効果を承認するコストが高い
素粒子物理学 $5.7 \times 10^{-7}$(5σ) 新粒子の発見には極めて高い確信が必要
探索的研究 0.10 見逃しを減らしたい

p値とは

直感的な理解

p値とは、「帰無仮説が正しいと仮定したとき、実際に観測されたデータ以上に極端な結果が得られる確率」です。

イメージとしては、「$H_0$ が正しい世界で、今回のデータがどれほど珍しいか」を測る指標です。p値が小さいほど、「$H_0$ の世界では今回のデータは極めて珍しい。$H_0$ は正しくないのではないか」と判断する根拠になります。

数学的定義

検定統計量 $T$ に対するp値は、以下で定義されます。

片側検定($H_1: \mu > \mu_0$)の場合:

$$ \begin{equation} p = P(T \geq t_{\text{obs}} \mid H_0) \end{equation} $$

両側検定($H_1: \mu \neq \mu_0$)の場合:

$$ \begin{equation} p = P(|T| \geq |t_{\text{obs}}| \mid H_0) = 2 P(T \geq |t_{\text{obs}}| \mid H_0) \end{equation} $$

ここで $t_{\text{obs}}$ は観測された検定統計量の値です。

判定基準

$$ \begin{cases} p \leq \alpha & \implies H_0 \text{ を棄却(統計的に有意)} \\ p > \alpha & \implies H_0 \text{ を棄却しない(有意でない)} \end{cases} $$

具体的な計算例

$X_1, \dots, X_{25} \sim N(\mu, 4)$($\sigma = 2$ 既知)で、$\bar{x} = 0.85$ が観測された場合の $H_0: \mu = 0$ に対する片側検定($H_1: \mu > 0$)のp値を求めます。

$$ \begin{align} z_{\text{obs}} &= \frac{\bar{x} – \mu_0}{\sigma / \sqrt{n}} = \frac{0.85 – 0}{2 / \sqrt{25}} = \frac{0.85}{0.4} = 2.125 \\ p &= P(Z \geq 2.125) = 1 – \Phi(2.125) \approx 0.0168 \end{align} $$

$p = 0.0168 < 0.05 = \alpha$ なので、$H_0$ を棄却します。

p値の正しい解釈と誤った解釈

正しい解釈

  • p値は「$H_0$ のもとで、観測データ以上に極端な結果が生じる確率」です
  • p値は $H_0$ に対する 証拠の強さ の尺度です
  • p値が小さいほど、$H_0$ に反する証拠が強いことを意味します

よくある誤った解釈

誤った解釈 なぜ誤りか
「p = 0.03 は $H_0$ が正しい確率が3%」 p値は $H_0$ が真であるときの条件付き確率であり、$H_0$ の事後確率ではない
「p < 0.05 なので効果は大きい」 p値は効果の大きさを示さない。サンプルサイズが大きければ小さな効果でもp値は小さくなる
「p > 0.05 なので $H_0$ は真」 $H_0$ を棄却できないことと、$H_0$ が正しいことは異なる(「差がない」の証明にはならない)
「p = 0.04 の方がp = 0.03 より信頼性が低い」 p値の連続的な差に大きな意味はない。0.049と0.051の差に質的な違いはない

効果量との区別

p値だけでなく、効果量(effect size)を報告することが推奨されます。効果量はサンプルサイズに依存しない効果の大きさの指標です。

代表的な効果量:

  • Cohen’s d: 2群の平均差を標準偏差で割ったもの $d = (\bar{x}_1 – \bar{x}_2) / s_p$
  • 相関係数 $r$
  • $\eta^2$(分散分析における効果量)

片側検定と両側検定

片側検定

対立仮説が特定の方向を主張する場合に使います。

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

棄却域は片方の裾にのみ設定されます。

両側検定

対立仮説が方向を指定しない場合に使います。

  • $H_0: \mu = \mu_0$ vs $H_1: \mu \neq \mu_0$

棄却域は両方の裾に $\alpha/2$ ずつ設定されます。

使い分けの基準

基準 片側検定 両側検定
事前知識 効果の方向が明確にわかっている 方向が不明
一般性 より特殊 より一般的
検出力 特定方向に対して高い やや低い
推奨度 理論的根拠がある場合のみ デフォルトとして推奨

数学的な関係

同じ検定統計量の値 $t_{\text{obs}}$ に対して、

$$ p_{\text{both}} = 2 \times p_{\text{one}} $$

両側検定のp値は片側検定のp値の2倍です。したがって、両側検定の方が棄却しにくくなります。

多重検定問題

問題の本質

複数の検定を同時に行うと、少なくとも1つで偽陽性が生じる確率(族有意水準: family-wise error rate, FWER)が増大します。

$m$ 個の独立な検定を有意水準 $\alpha$ で行った場合、少なくとも1つで第1種の過誤が生じる確率は、

$$ \text{FWER} = 1 – (1 – \alpha)^m $$

検定数 $m$ FWER($\alpha = 0.05$)
1 0.050
5 0.226
10 0.401
20 0.642
100 0.994

20個の検定を行うと、約64%の確率で少なくとも1つは偽陽性になります。

ボンフェローニ補正

最も保守的な補正法です。各検定の有意水準を $\alpha / m$ に修正します。

$$ \alpha_{\text{adj}} = \frac{\alpha}{m} $$

あるいは等価的に、p値を $m$ 倍して $\alpha$ と比較します。

$$ p_{\text{adj}} = \min(m \cdot p, 1) $$

利点: 計算が簡単、FWERを厳密に制御 欠点: 非常に保守的(検出力が大きく低下)

Benjamini-Hochberg法(BH法)

FWERの代わりに 偽発見率(False Discovery Rate, FDR)を制御する方法です。

  1. $m$ 個のp値を小さい順に並べる: $p_{(1)} \leq p_{(2)} \leq \cdots \leq p_{(m)}$
  2. $p_{(k)} \leq \frac{k}{m} \alpha$ を満たす最大の $k$ を見つける
  3. $p_{(1)}, \dots, p_{(k)}$ に対応する検定を有意とする

BH法はボンフェローニ補正よりも検出力が高く、ゲノム解析など大規模な多重検定で広く使われています。

Pythonでの実装

p値の計算と可視化

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

# 正規分布での片側・両側検定のp値の可視化
z_obs = 2.0  # 観測された検定統計量

z = np.linspace(-4, 4, 1000)
pdf = stats.norm.pdf(z)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 片側検定(右側)
ax = axes[0]
ax.plot(z, pdf, 'k-', linewidth=2)
z_fill = z[z >= z_obs]
ax.fill_between(z_fill, stats.norm.pdf(z_fill), alpha=0.4, color='red',
                label=f'p = {1 - stats.norm.cdf(z_obs):.4f}')
ax.axvline(x=z_obs, color='red', linestyle='--', linewidth=1.5)
ax.set_title(f'One-sided test ($z_{{obs}}$ = {z_obs})', fontsize=12)
ax.set_xlabel('z')
ax.set_ylabel('Density')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

# 両側検定
ax = axes[1]
ax.plot(z, pdf, 'k-', linewidth=2)
z_fill_right = z[z >= z_obs]
z_fill_left = z[z <= -z_obs]
ax.fill_between(z_fill_right, stats.norm.pdf(z_fill_right), alpha=0.4, color='blue')
ax.fill_between(z_fill_left, stats.norm.pdf(z_fill_left), alpha=0.4, color='blue',
                label=f'p = {2 * (1 - stats.norm.cdf(z_obs)):.4f}')
ax.axvline(x=z_obs, color='blue', linestyle='--', linewidth=1.5)
ax.axvline(x=-z_obs, color='blue', linestyle='--', linewidth=1.5)
ax.set_title(f'Two-sided test ($|z_{{obs}}|$ = {z_obs})', fontsize=12)
ax.set_xlabel('z')
ax.set_ylabel('Density')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

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

片側検定のp値(赤い領域)は片方の裾の面積であり、両側検定のp値(青い領域)は両裾の面積の合計です。同じ検定統計量に対して、両側検定のp値は片側検定の2倍になります。

p値の分布(H0が真の場合とH1が真の場合)

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

np.random.seed(42)
n = 30
sigma = 1
mu0 = 0
n_simulations = 10000

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# H0が真の場合のp値の分布
p_values_h0 = []
for _ in range(n_simulations):
    sample = np.random.normal(mu0, sigma, n)
    z = sample.mean() / (sigma / np.sqrt(n))
    p = 2 * (1 - stats.norm.cdf(abs(z)))  # 両側検定
    p_values_h0.append(p)

axes[0].hist(p_values_h0, bins=50, density=True, alpha=0.7, color='skyblue', edgecolor='white')
axes[0].axhline(y=1, color='red', linewidth=2, linestyle='--', label='Uniform(0,1)')
axes[0].set_xlabel('p-value', fontsize=12)
axes[0].set_ylabel('Density', fontsize=12)
axes[0].set_title('p-value Distribution under $H_0$ (should be Uniform)', fontsize=12)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)

# H1が真の場合のp値の分布
mu1 = 0.5
p_values_h1 = []
for _ in range(n_simulations):
    sample = np.random.normal(mu1, sigma, n)
    z = sample.mean() / (sigma / np.sqrt(n))
    p = 2 * (1 - stats.norm.cdf(abs(z)))
    p_values_h1.append(p)

axes[1].hist(p_values_h1, bins=50, density=True, alpha=0.7, color='lightcoral', edgecolor='white')
axes[1].axvline(x=0.05, color='green', linewidth=2, linestyle='--', label='$\\alpha$ = 0.05')
axes[1].set_xlabel('p-value', fontsize=12)
axes[1].set_ylabel('Density', fontsize=12)
axes[1].set_title(f'p-value Distribution under $H_1$ ($\\mu$ = {mu1})', fontsize=12)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)

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

# 第1種・第2種の過誤率
alpha = 0.05
type1 = np.mean(np.array(p_values_h0) < alpha)
power = np.mean(np.array(p_values_h1) < alpha)
print(f"Type I Error Rate: {type1:.4f} (expected: {alpha})")
print(f"Power: {power:.4f}")

$H_0$ が真のとき、p値は $[0, 1]$ 上の一様分布に従います(左図)。これはp値の重要な性質で、有意水準 $\alpha$ を設定したとき、ちょうど $\alpha$ の割合で $p < \alpha$ となることを意味します。$H_1$ が真のとき、p値は0に偏った分布になります(右図)。

多重検定問題の可視化

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

np.random.seed(42)

# 多重検定のシミュレーション
n_tests_list = [1, 5, 10, 20, 50, 100]
alpha = 0.05
n_simulations = 10000

# FWERの理論値と実験値
fwer_theory = [1 - (1 - alpha) ** m for m in n_tests_list]
fwer_empirical = []

for m in n_tests_list:
    false_positives = 0
    for _ in range(n_simulations):
        # m個の独立なz検定(すべてH0が真)
        z_values = np.random.normal(0, 1, m)
        p_values = 2 * (1 - stats.norm.cdf(np.abs(z_values)))
        if np.any(p_values < alpha):
            false_positives += 1
    fwer_empirical.append(false_positives / n_simulations)

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(n_tests_list, fwer_theory, 'r-o', linewidth=2, markersize=8, label='Theoretical FWER')
ax.plot(n_tests_list, fwer_empirical, 'b--s', linewidth=2, markersize=8, label='Empirical FWER')
ax.axhline(y=alpha, color='green', linestyle=':', linewidth=2, label=f'$\\alpha$ = {alpha}')
ax.set_xlabel('Number of tests (m)', fontsize=12)
ax.set_ylabel('Family-Wise Error Rate', fontsize=12)
ax.set_title('Multiple Testing Problem: FWER increases with m', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)
plt.savefig("multiple_testing_fwer.png", dpi=150, bbox_inches="tight")
plt.show()

ボンフェローニ補正とBH法の比較

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

np.random.seed(42)

# シナリオ: 100個の検定のうち10個が真に有意
m = 100
m_true = 10  # 本当に効果があるもの
alpha = 0.05

# p値の生成
# 真にH0のもの: 一様分布
p_null = np.random.uniform(0, 1, m - m_true)
# 真にH1のもの: 小さなp値(効果あり)
p_alt = np.random.beta(1, 20, m_true)  # 0に偏った分布

p_values = np.concatenate([p_null, p_alt])
is_true_alt = np.concatenate([np.zeros(m - m_true), np.ones(m_true)]).astype(bool)

# ソートのインデックス
sort_idx = np.argsort(p_values)
p_sorted = p_values[sort_idx]
true_alt_sorted = is_true_alt[sort_idx]

# 補正なし
reject_none = p_values < alpha

# ボンフェローニ補正
p_bonf = np.minimum(p_values * m, 1.0)
reject_bonf = p_bonf < alpha

# BH法(Benjamini-Hochberg)
rank = np.arange(1, m + 1)
bh_threshold = rank / m * alpha
reject_bh_sorted = p_sorted <= bh_threshold
# 最大のkを見つける
if np.any(reject_bh_sorted):
    max_k = np.max(np.where(reject_bh_sorted)[0])
    reject_bh_sorted_final = np.zeros(m, dtype=bool)
    reject_bh_sorted_final[:max_k + 1] = True
else:
    reject_bh_sorted_final = np.zeros(m, dtype=bool)
# 元の順序に戻す
reject_bh = np.zeros(m, dtype=bool)
reject_bh[sort_idx] = reject_bh_sorted_final

# 結果の表示
def calc_metrics(reject, is_true_alt):
    TP = np.sum(reject & is_true_alt)
    FP = np.sum(reject & ~is_true_alt)
    FN = np.sum(~reject & is_true_alt)
    TN = np.sum(~reject & ~is_true_alt)
    return TP, FP, FN, TN

print(f"{'Method':<20s} {'TP':>4s} {'FP':>4s} {'FN':>4s} {'TN':>4s} {'FDR':>8s} {'Power':>8s}")
print("-" * 60)
for name, reject in [('No correction', reject_none),
                      ('Bonferroni', reject_bonf),
                      ('BH (FDR)', reject_bh)]:
    TP, FP, FN, TN = calc_metrics(reject, is_true_alt)
    fdr = FP / max(FP + TP, 1)
    power = TP / max(TP + FN, 1)
    print(f"{name:<20s} {TP:>4d} {FP:>4d} {FN:>4d} {TN:>4d} {fdr:>8.3f} {power:>8.3f}")

# 可視化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for ax, (name, reject) in zip(axes, [('No Correction', reject_none),
                                       ('Bonferroni', reject_bonf),
                                       ('BH (FDR)', reject_bh)]):
    colors = []
    for r, t in zip(reject, is_true_alt):
        if r and t:
            colors.append('green')    # 真陽性
        elif r and not t:
            colors.append('red')      # 偽陽性
        elif not r and t:
            colors.append('orange')   # 偽陰性
        else:
            colors.append('gray')     # 真陰性

    ax.scatter(range(m), -np.log10(p_values), c=colors, s=30, alpha=0.7)
    ax.axhline(y=-np.log10(alpha), color='blue', linestyle='--', label=f'$\\alpha$ = {alpha}')
    ax.set_xlabel('Test index', fontsize=10)
    ax.set_ylabel('$-\\log_{10}(p)$', fontsize=10)
    ax.set_title(name, fontsize=12)
    ax.grid(True, alpha=0.3)

    # カスタム凡例
    from matplotlib.patches import Patch
    legend_elements = [Patch(facecolor='green', label='True Positive'),
                       Patch(facecolor='red', label='False Positive'),
                       Patch(facecolor='orange', label='False Negative'),
                       Patch(facecolor='gray', label='True Negative')]
    ax.legend(handles=legend_elements, fontsize=8, loc='upper right')

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

補正なしでは多くの偽陽性(赤)が生じていますが、ボンフェローニ補正は保守的すぎて真の効果も見逃しがちです(偽陰性、オレンジ)。BH法はバランスのとれた結果を示しています。

p値の直感的理解のためのインタラクティブな可視化

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

# 異なる検定統計量の値に対するp値の変化
z_values = np.arange(0, 4.1, 0.5)

fig, axes = plt.subplots(2, 4, figsize=(18, 8))
axes = axes.ravel()

z_grid = np.linspace(-4, 4, 1000)
pdf = stats.norm.pdf(z_grid)

for i, z_obs in enumerate(z_values):
    if i >= 8:
        break
    ax = axes[i]
    ax.plot(z_grid, pdf, 'k-', linewidth=1.5)

    # p値の領域を塗る(両側検定)
    if z_obs > 0:
        mask_right = z_grid >= z_obs
        mask_left = z_grid <= -z_obs
        ax.fill_between(z_grid[mask_right], pdf[mask_right], alpha=0.4, color='red')
        ax.fill_between(z_grid[mask_left], pdf[mask_left], alpha=0.4, color='red')

    p_val = 2 * (1 - stats.norm.cdf(z_obs))
    ax.set_title(f'z = {z_obs:.1f}, p = {p_val:.4f}', fontsize=10)
    ax.set_xlim(-4, 4)
    ax.set_ylim(0, 0.45)
    ax.grid(True, alpha=0.3)

    # 有意かどうか
    if p_val < 0.05:
        ax.set_facecolor('#fff0f0')
    else:
        ax.set_facecolor('#f0fff0')

plt.suptitle('p-value as area under the curve (Two-sided test)', fontsize=14, y=1.01)
plt.tight_layout()
plt.savefig("pvalue_intuition.png", dpi=150, bbox_inches="tight")
plt.show()

検定統計量 $z$ が大きくなるにつれて、p値(赤い領域の面積)が小さくなっていく様子が視覚的に理解できます。$z = 1.96$ 付近で $p \approx 0.05$ となり、これが $\alpha = 0.05$ の臨界値に対応します。背景が赤みがかっているパネルは $p < 0.05$ で有意、緑がかっているパネルは $p > 0.05$ で有意でないことを示しています。

まとめ

本記事では、有意水準とp値の正しい理解と使い方について解説しました。

  • 有意水準 $\alpha$ は第1種の過誤の許容確率であり、データを見る前に設定します
  • p値は「$H_0$ のもとで、観測データ以上に極端な結果が得られる確率」です
  • p値は $H_0$ が正しい確率ではありません。また、効果の大きさも示しません
  • 片側検定のp値は両側検定の半分です。デフォルトでは両側検定が推奨されます
  • 多重検定を行うとFWERが増大するため、ボンフェローニ補正やBH法による調整が必要です
  • p値とともに効果量と信頼区間を報告することが推奨されます

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