統計的仮説検定を実施する前に、「どのくらいのサンプル数があれば、検出したい効果を見逃さずに済むか」を事前に設計することは極めて重要です。この設計の中核にあるのが 検出力(power) と 標本サイズ(sample size) の関係です。
検出力分析(power analysis)は、臨床試験の計画、工業実験の設計、社会科学研究のプロポーザル作成など、研究の初期段階で必ず行うべきプロセスです。適切な標本サイズを決めなければ、効果があるのに検出できない(第2種の過誤)リスクが高まり、時間とコストが無駄になります。
本記事の内容
- 検出力(power)の厳密な定義と意味
- 検出力に影響する4つの要素の数学的関係
- 標本サイズ決定公式の導出
- 効果量(effect size)の概念と計算法
- Pythonによる検出力曲線のプロットと statsmodels を使った検出力分析
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
検出力とは
定義
検出力(statistical power)とは、対立仮説 $H_1$ が真であるときに、正しく帰無仮説 $H_0$ を棄却できる確率です。
$$ \text{Power} = 1 – \beta = P(\text{$H_0$ を棄却} \mid H_1 \text{ が真}) $$
ここで $\beta$ は第2種の過誤(偽陰性)の確率です。
直感的理解
検出力は「検定の感度」とも言えます。検出力が高い検定ほど、実際に存在する効果を見逃しにくくなります。
- 検出力 $= 0.80$: 真の効果がある場合、80%の確率で検出できる(20%は見逃す)
- 検出力 $= 0.50$: 真の効果がある場合、50%の確率でしか検出できない(コイン投げと同じ)
- 検出力 $= 0.95$: 真の効果がある場合、95%の確率で検出できる
一般に、検出力は 0.80以上 を目標とすることが推奨されています(Cohen, 1988)。
検出力に影響する4つの要素
数学的な枠組み
母平均の片側 $z$ 検定を考えます。
$$ H_0: \mu = \mu_0, \quad H_1: \mu > \mu_0 $$
前記事で導出したように、検出力は次の式で表されます。
$$ 1 – \beta = \Phi(\delta – z_\alpha) $$
ここで $\delta$ は非心度(noncentrality parameter)であり、次のように定義されます。
$$ \delta = \frac{(\mu_1 – \mu_0)\sqrt{n}}{\sigma} $$
この式から、検出力に影響する4つの要素が読み取れます。
要素1: 効果量(Effect Size)
効果量 $d$ は、検出したい差を母標準偏差で標準化した量です。
$$ d = \frac{\mu_1 – \mu_0}{\sigma} $$
非心度は $\delta = d\sqrt{n}$ と書き直せます。効果量が大きいほど非心度が大きくなり、検出力が高くなります。
効果量が大きいということは、帰無仮説のもとでの分布と対立仮説のもとでの分布が大きく離れていることを意味します。離れていれば区別しやすいので、検出力が高くなるのは直感的にも理解できます。
Cohenの基準による効果量の目安を示します。
| 効果量 $d$ | 大きさ |
|---|---|
| 0.2 | 小さい(small) |
| 0.5 | 中程度(medium) |
| 0.8 | 大きい(large) |
要素2: 有意水準 $\alpha$
有意水準 $\alpha$ は棄却域の広さを決めます。$\alpha$ が大きいほど $z_\alpha$ が小さくなり、$\delta – z_\alpha$ が大きくなって検出力が増加します。
$$ \frac{\partial(1 – \beta)}{\partial \alpha} = \frac{\partial}{\partial \alpha}\Phi(\delta – z_\alpha) > 0 $$
これを具体的に確認します。$z_\alpha = \Phi^{-1}(1 – \alpha)$ であるから、
$$ \frac{dz_\alpha}{d\alpha} = -\frac{1}{\phi(z_\alpha)} < 0 $$
したがって、
$$ \frac{d(1-\beta)}{d\alpha} = \phi(\delta – z_\alpha) \cdot \left(-\frac{dz_\alpha}{d\alpha}\right) = \frac{\phi(\delta – z_\alpha)}{\phi(z_\alpha)} > 0 $$
つまり、$\alpha$ を大きくすると検出力は上がりますが、第1種の過誤のリスクも増加するため、通常は $\alpha = 0.05$ に固定します。
要素3: 母標準偏差 $\sigma$
母標準偏差 $\sigma$ はデータのばらつきの大きさを表します。$\sigma$ が大きいほど非心度 $\delta = (\mu_1 – \mu_0)\sqrt{n}/\sigma$ が小さくなり、検出力が低下します。
$$ \frac{\partial \delta}{\partial \sigma} = -\frac{(\mu_1 – \mu_0)\sqrt{n}}{\sigma^2} < 0 $$
ノイズが大きいと信号(効果)が埋もれてしまうイメージです。実験設計では、測定精度を上げたり、共変量を統制したりして $\sigma$ を小さくする工夫が重要です。
要素4: 標本サイズ $n$
標本サイズ $n$ は研究者が最もコントロールしやすい要素です。$n$ を増やせば非心度 $\delta$ が増加し、検出力が向上します。
$$ \frac{\partial \delta}{\partial n} = \frac{(\mu_1 – \mu_0)}{2\sigma\sqrt{n}} > 0 $$
ただし $\delta \propto \sqrt{n}$ であるため、検出力の改善は $n$ に対して逓減的です。$n$ を4倍にしても $\delta$ は2倍にしかなりません。
標本サイズ決定公式の導出
片側検定の場合
有意水準 $\alpha$、検出力 $1 – \beta$ を達成するために必要な標本サイズ $n$ を求めます。
検出力の式を変形していきます。
$$ 1 – \beta = \Phi(\delta – z_\alpha) $$
両辺に $\Phi^{-1}$ を適用します。
$$ \Phi^{-1}(1 – \beta) = \delta – z_\alpha $$
$\Phi^{-1}(1 – \beta) = z_\beta$(上側 $\beta$ 点)を代入します。ここで $z_\beta$ は正の値です。
$$ z_\beta = \delta – z_\alpha $$
$\delta = d\sqrt{n}$ を代入します。
$$ z_\beta = d\sqrt{n} – z_\alpha $$
$\sqrt{n}$ について解きます。
$$ \sqrt{n} = \frac{z_\alpha + z_\beta}{d} $$
両辺を2乗して、
$$ \boxed{n = \left(\frac{z_\alpha + z_\beta}{d}\right)^2} $$
これが片側検定における標本サイズの決定公式です。
両側検定の場合
両側検定では、棄却域が両側にあるため $z_\alpha$ の代わりに $z_{\alpha/2}$ を使います。
$$ \boxed{n = \left(\frac{z_{\alpha/2} + z_\beta}{d}\right)^2} $$
2標本検定の場合
2つの群の母平均を比較する場合(各群のサイズを同じ $n$ とする)、検定統計量の標準誤差が $\sigma\sqrt{2/n}$ になるため、標本サイズの公式は次のようになります。
非心度は次のように変わります。
$$ \delta = \frac{(\mu_1 – \mu_2)}{\sigma\sqrt{2/n}} = \frac{(\mu_1 – \mu_2)\sqrt{n}}{\sigma\sqrt{2}} = \frac{d\sqrt{n}}{\sqrt{2}} $$
これを標本サイズの公式に代入すると、
$$ z_\beta = \frac{d\sqrt{n}}{\sqrt{2}} – z_\alpha $$
$$ \sqrt{n} = \frac{(z_\alpha + z_\beta)\sqrt{2}}{d} $$
$$ \boxed{n = 2\left(\frac{z_\alpha + z_\beta}{d}\right)^2} $$
これは各群に必要なサイズです。合計では $2n$ が必要となります。
具体例
2標本両側検定で、$\alpha = 0.05$, $1 – \beta = 0.80$, $d = 0.5$(中程度の効果量)の場合に必要な各群のサイズを計算します。
$$ z_{\alpha/2} = z_{0.025} = 1.960 $$
$$ z_\beta = z_{0.20} = 0.842 $$
$$ n = 2 \times \left(\frac{1.960 + 0.842}{0.5}\right)^2 = 2 \times \left(\frac{2.802}{0.5}\right)^2 = 2 \times 5.604^2 = 2 \times 31.40 = 62.81 $$
したがって、各群 $n = 63$ 名、合計126名が必要です。
効果量の計算方法
Cohenの $d$(2群の平均の差)
$$ d = \frac{\bar{X}_1 – \bar{X}_2}{s_p} $$
ここで $s_p$ はプールされた標準偏差です。
$$ s_p = \sqrt{\frac{(n_1 – 1)s_1^2 + (n_2 – 1)s_2^2}{n_1 + n_2 – 2}} $$
導出を確認しましょう。2群の母分散が等しい($\sigma_1^2 = \sigma_2^2 = \sigma^2$)と仮定すると、各群の標本分散 $s_i^2$ はそれぞれ $(n_i – 1)s_i^2 / \sigma^2 \sim \chi^2_{n_i – 1}$ に従います。プールされた分散はこの共通の $\sigma^2$ の最尤推定量に対応します。
$$ s_p^2 = \frac{(n_1 – 1)s_1^2 + (n_2 – 1)s_2^2}{(n_1 – 1) + (n_2 – 1)} = \frac{\sum_{i=1}^{2}(n_i – 1)s_i^2}{n_1 + n_2 – 2} $$
1標本の効果量
1標本検定の場合、効果量は次のように定義します。
$$ d = \frac{\mu_1 – \mu_0}{\sigma} $$
事前に $\sigma$ がわからない場合は、パイロットスタディの標本標準偏差 $s$ で代用します。
効果量の事前設定
効果量を事前に設定する方法は3つあります。
- 先行研究の結果を参考にする: 類似の研究で報告されている効果量を利用
- 臨床的・実用的に意味のある最小の差を設定する: 「このくらいの差があれば臨床的に有意義」という基準
- Cohenの基準を使う: $d = 0.2$(小), $d = 0.5$(中), $d = 0.8$(大)
Pythonでの実装
検出力曲線の描画
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
def power_one_sample_z(d, n, alpha=0.05, alternative='greater'):
"""
1標本z検定の検出力を計算する関数
Parameters
----------
d : float or array
効果量 (Cohen's d)
n : int or array
標本サイズ
alpha : float
有意水準
alternative : str
'greater' (片側), 'two-sided' (両側)
Returns
-------
power : float or array
検出力
"""
if alternative == 'greater':
z_alpha = stats.norm.ppf(1 - alpha)
delta = d * np.sqrt(n)
power = 1 - stats.norm.cdf(z_alpha - delta)
elif alternative == 'two-sided':
z_alpha_half = stats.norm.ppf(1 - alpha / 2)
delta = d * np.sqrt(n)
# 右側の棄却域での検出力 + 左側の棄却域での検出力
power_right = 1 - stats.norm.cdf(z_alpha_half - delta)
power_left = stats.norm.cdf(-z_alpha_half - delta)
power = power_right + power_left
return power
# --- 効果量を変えた検出力曲線 ---
n_range = np.arange(5, 301)
effect_sizes = [0.2, 0.3, 0.5, 0.8]
fig, ax = plt.subplots(figsize=(10, 6))
for d in effect_sizes:
powers = power_one_sample_z(d, n_range, alpha=0.05, alternative='two-sided')
ax.plot(n_range, powers, linewidth=2, label=f'd = {d}')
ax.axhline(y=0.80, color='gray', linestyle='--', linewidth=1, label='Power = 0.80')
ax.set_xlabel('Sample Size $n$', fontsize=13)
ax.set_ylabel('Power ($1 - \\beta$)', fontsize=13)
ax.set_title('Power Curves for Different Effect Sizes (Two-Sided, $\\alpha$ = 0.05)', fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)
ax.set_xlim(5, 300)
plt.tight_layout()
plt.show()
標本サイズ決定の実装
import numpy as np
from scipy import stats
def required_sample_size(d, alpha=0.05, power=0.80, alternative='two-sided', n_groups=1):
"""
必要な標本サイズを計算する関数
Parameters
----------
d : float
効果量 (Cohen's d)
alpha : float
有意水準
power : float
目標検出力
alternative : str
'greater'/'less' (片側), 'two-sided' (両側)
n_groups : int
群の数 (1: 1標本, 2: 2標本)
Returns
-------
n : int
必要な標本サイズ(各群)
"""
z_beta = stats.norm.ppf(power)
if alternative == 'two-sided':
z_alpha = stats.norm.ppf(1 - alpha / 2)
else:
z_alpha = stats.norm.ppf(1 - alpha)
if n_groups == 1:
n = ((z_alpha + z_beta) / d) ** 2
elif n_groups == 2:
n = 2 * ((z_alpha + z_beta) / d) ** 2
return int(np.ceil(n))
# さまざまな条件での必要標本サイズを計算
print("=" * 60)
print("必要標本サイズの計算結果")
print("=" * 60)
print("\n【1標本検定・両側】 α = 0.05, Power = 0.80")
print("-" * 40)
for d in [0.2, 0.3, 0.5, 0.8, 1.0]:
n = required_sample_size(d, alpha=0.05, power=0.80, alternative='two-sided', n_groups=1)
print(f" d = {d:.1f} → n = {n}")
print("\n【2標本検定・両側】 α = 0.05, Power = 0.80")
print("-" * 40)
for d in [0.2, 0.3, 0.5, 0.8, 1.0]:
n = required_sample_size(d, alpha=0.05, power=0.80, alternative='two-sided', n_groups=2)
print(f" d = {d:.1f} → 各群 n = {n}, 合計 {2*n}")
print("\n【検出力を変えた場合】 1標本・両側, α = 0.05, d = 0.5")
print("-" * 40)
for p in [0.70, 0.80, 0.90, 0.95, 0.99]:
n = required_sample_size(0.5, alpha=0.05, power=p, alternative='two-sided', n_groups=1)
print(f" Power = {p:.2f} → n = {n}")
実行結果は次のようになります。
============================================================
必要標本サイズの計算結果
============================================================
【1標本検定・両側】 α = 0.05, Power = 0.80
----------------------------------------
d = 0.2 → n = 197
d = 0.3 → n = 88
d = 0.5 → n = 32
d = 0.8 → n = 13
d = 1.0 → n = 9
【2標本検定・両側】 α = 0.05, Power = 0.80
----------------------------------------
d = 0.2 → 各群 n = 394, 合計 788
d = 0.3 → 各群 n = 176, 合計 352
d = 0.5 → 各群 n = 64, 合計 128
d = 0.8 → 各群 n = 25, 合計 50
d = 1.0 → 各群 n = 17, 合計 34
【検出力を変えた場合】 1標本・両側, α = 0.05, d = 0.5
----------------------------------------
Power = 0.70 → n = 25
Power = 0.80 → n = 32
Power = 0.90 → n = 44
Power = 0.95 → n = 54
Power = 0.99 → n = 76
小さい効果量 $d = 0.2$ を検出するには、1標本検定でも197名が必要であることがわかります。
4要素の感度分析
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
def power_z(d, n, alpha, alternative='two-sided'):
"""z検定の検出力を計算"""
if alternative == 'two-sided':
z_a = stats.norm.ppf(1 - alpha / 2)
else:
z_a = stats.norm.ppf(1 - alpha)
delta = d * np.sqrt(n)
return 1 - stats.norm.cdf(z_a - delta)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# (1) 効果量 vs 検出力(n固定)
d_range = np.linspace(0.01, 1.5, 200)
for n_val in [20, 50, 100, 200]:
p = power_z(d_range, n_val, 0.05)
axes[0, 0].plot(d_range, p, linewidth=2, label=f'n = {n_val}')
axes[0, 0].axhline(0.80, color='gray', linestyle='--', alpha=0.5)
axes[0, 0].set_xlabel('Effect Size $d$', fontsize=12)
axes[0, 0].set_ylabel('Power', fontsize=12)
axes[0, 0].set_title('(a) Effect Size vs Power', fontsize=13)
axes[0, 0].legend(fontsize=10)
axes[0, 0].grid(True, alpha=0.3)
# (2) 標本サイズ vs 検出力(d固定)
n_range = np.arange(5, 301)
for d_val in [0.2, 0.5, 0.8]:
p = power_z(d_val, n_range, 0.05)
axes[0, 1].plot(n_range, p, linewidth=2, label=f'd = {d_val}')
axes[0, 1].axhline(0.80, color='gray', linestyle='--', alpha=0.5)
axes[0, 1].set_xlabel('Sample Size $n$', fontsize=12)
axes[0, 1].set_ylabel('Power', fontsize=12)
axes[0, 1].set_title('(b) Sample Size vs Power', fontsize=13)
axes[0, 1].legend(fontsize=10)
axes[0, 1].grid(True, alpha=0.3)
# (3) 有意水準α vs 検出力(d, n固定)
alpha_range = np.linspace(0.001, 0.20, 200)
for n_val in [20, 50, 100]:
p = power_z(0.5, n_val, alpha_range)
axes[1, 0].plot(alpha_range, p, linewidth=2, label=f'n = {n_val}')
axes[1, 0].axhline(0.80, color='gray', linestyle='--', alpha=0.5)
axes[1, 0].axvline(0.05, color='gray', linestyle=':', alpha=0.5)
axes[1, 0].set_xlabel('Significance Level $\\alpha$', fontsize=12)
axes[1, 0].set_ylabel('Power', fontsize=12)
axes[1, 0].set_title('(c) $\\alpha$ vs Power ($d$ = 0.5)', fontsize=13)
axes[1, 0].legend(fontsize=10)
axes[1, 0].grid(True, alpha=0.3)
# (4) 効果量と標本サイズのヒートマップ
d_vals = np.linspace(0.1, 1.0, 50)
n_vals = np.arange(10, 201)
D, N = np.meshgrid(d_vals, n_vals)
P = power_z(D, N, 0.05)
im = axes[1, 1].contourf(D, N, P, levels=np.linspace(0, 1, 21), cmap='RdYlGn')
axes[1, 1].contour(D, N, P, levels=[0.80], colors='black', linewidths=2)
axes[1, 1].set_xlabel('Effect Size $d$', fontsize=12)
axes[1, 1].set_ylabel('Sample Size $n$', fontsize=12)
axes[1, 1].set_title('(d) Power Heatmap ($\\alpha$ = 0.05)', fontsize=13)
fig.colorbar(im, ax=axes[1, 1], label='Power')
plt.tight_layout()
plt.show()
statsmodelsを使った検出力分析
実務ではゼロから実装する代わりに、statsmodels の検出力分析モジュールを使うことが多いです。
from statsmodels.stats.power import TTestIndPower, TTestPower, NormalIndPower
import numpy as np
import matplotlib.pyplot as plt
# --- 2標本t検定の検出力分析 ---
power_analysis = TTestIndPower()
# 必要標本サイズの計算
# d=0.5, α=0.05, power=0.80 のときの各群のサイズ
n_required = power_analysis.solve_power(
effect_size=0.5,
alpha=0.05,
power=0.80,
ratio=1.0, # 2群のサイズ比 (n2/n1)
alternative='two-sided'
)
print(f"2標本t検定: d=0.5, α=0.05, power=0.80")
print(f" 各群に必要な標本サイズ: {np.ceil(n_required):.0f}")
print(f" 合計: {2 * np.ceil(n_required):.0f}")
# 検出力の計算
# n=50, d=0.5, α=0.05 のときの検出力
power_val = power_analysis.solve_power(
effect_size=0.5,
nobs1=50,
alpha=0.05,
ratio=1.0,
alternative='two-sided'
)
print(f"\n各群n=50, d=0.5, α=0.05 のときの検出力: {power_val:.4f}")
# 効果量の計算
# n=50, α=0.05, power=0.80 で検出できる最小効果量
d_min = power_analysis.solve_power(
nobs1=50,
alpha=0.05,
power=0.80,
ratio=1.0,
alternative='two-sided'
)
print(f"\n各群n=50, α=0.05, power=0.80 で検出できる最小効果量: d={d_min:.4f}")
# --- 検出力曲線をstatsmodelsで描画 ---
fig, ax = plt.subplots(figsize=(10, 6))
# 異なる効果量での検出力曲線
n_range = np.arange(5, 301)
for d in [0.2, 0.5, 0.8]:
powers = []
for n_val in n_range:
p = power_analysis.solve_power(
effect_size=d,
nobs1=n_val,
alpha=0.05,
ratio=1.0,
alternative='two-sided'
)
powers.append(p)
ax.plot(n_range, powers, linewidth=2, label=f'd = {d}')
ax.axhline(0.80, color='gray', linestyle='--', linewidth=1, label='Power = 0.80')
ax.set_xlabel('Sample Size per Group', fontsize=13)
ax.set_ylabel('Power', fontsize=13)
ax.set_title('Power Curves (Two-Sample t-Test, $\\alpha$ = 0.05)', fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.05)
plt.tight_layout()
plt.show()
# --- 1標本t検定の検出力分析 ---
power_1sample = TTestPower()
print("\n" + "=" * 60)
print("1標本t検定の検出力分析")
print("=" * 60)
# さまざまな条件での標本サイズ
for d in [0.2, 0.5, 0.8]:
n_req = power_1sample.solve_power(
effect_size=d,
alpha=0.05,
power=0.80,
alternative='two-sided'
)
print(f" d = {d}, α = 0.05, Power = 0.80 → n = {np.ceil(n_req):.0f}")
事前の検出力分析レポート生成
import numpy as np
from scipy import stats
def power_analysis_report(effect_size, alpha, power, n_groups, alternative='two-sided'):
"""
検出力分析のレポートを生成する関数
Parameters
----------
effect_size : float
効果量 (Cohen's d)
alpha : float
有意水準
power : float
目標検出力
n_groups : int
群の数 (1 or 2)
alternative : str
'two-sided' or 'greater'
"""
z_beta = stats.norm.ppf(power)
if alternative == 'two-sided':
z_alpha = stats.norm.ppf(1 - alpha / 2)
else:
z_alpha = stats.norm.ppf(1 - alpha)
if n_groups == 1:
n = np.ceil(((z_alpha + z_beta) / effect_size) ** 2)
else:
n = np.ceil(2 * ((z_alpha + z_beta) / effect_size) ** 2)
# 実際の検出力を逆算
delta = effect_size * np.sqrt(n) if n_groups == 1 else effect_size * np.sqrt(n / 2)
actual_power = 1 - stats.norm.cdf(z_alpha - delta)
print("=" * 60)
print("検出力分析レポート(Power Analysis Report)")
print("=" * 60)
print(f" 検定の種類: {'1標本' if n_groups == 1 else '2標本'}検定")
print(f" 検定の方向: {'両側' if alternative == 'two-sided' else '片側'}")
print(f" 効果量 (d): {effect_size}")
print(f" 有意水準 (α): {alpha}")
print(f" 目標検出力: {power}")
print(f" z_α: {z_alpha:.4f}")
print(f" z_β: {z_beta:.4f}")
print("-" * 60)
print(f" 必要標本サイズ: 各群 {int(n)} 名")
if n_groups == 2:
print(f" 合計: {int(2 * n)} 名")
print(f" 実際の検出力: {actual_power:.4f}")
print("=" * 60)
# レポート生成例
power_analysis_report(
effect_size=0.5,
alpha=0.05,
power=0.80,
n_groups=2,
alternative='two-sided'
)
print()
power_analysis_report(
effect_size=0.3,
alpha=0.01,
power=0.90,
n_groups=1,
alternative='two-sided'
)
まとめ
本記事では、検出力と標本サイズ設計の理論について解説しました。
- 検出力 $1 – \beta = \Phi(\delta – z_\alpha)$ は、対立仮説が真のときに正しく $H_0$ を棄却できる確率
- 検出力に影響する4要素は 効果量 $d$、有意水準 $\alpha$、母標準偏差 $\sigma$、標本サイズ $n$
- 標本サイズの決定公式: $n = \left(\frac{z_\alpha + z_\beta}{d}\right)^2$(1標本片側)
- 効果量の事前設定が検出力分析の鍵であり、先行研究や臨床的意義に基づいて決定する
statsmodelsを使えば、実務での検出力分析を効率的に実行できる
次のステップとして、以下の記事も参考にしてください。