Rでサンプルサイズ設計:t検定(2群、対応なし、異なるサンプルサイズ)

Rで サンプルサイズ設計:t検定(2群、対応なし、異なるサンプルサイズ) を確認します。

1. シミュレーションのシナリオ

シナリオ:新しい学習アプリの効果測定

ある教育テクノロジー企業が、中学生向けの新しい数学学習アプリを開発しました。このアプリが、従来の教科書ベースの学習方法よりも、数学のテストの点数を向上させる効果があるかを検証したいと考えています。

  • 研究の目的: 新学習アプリ(介入群)は、従来学習(対照群)と比較して、学期末の数学テストの平均点を有意に向上させるか。
  • 被験者: 中学1年生
  • 介入群 (Group 1): 新しい学習アプリを3ヶ月間使用する。
  • 対照群 (Group 2): 従来通り、教科書と問題集で学習する。
  • 制約: アプリのライセンス数とサポート体制の都合上、アプリを使用できる生徒数(介入群)は、従来学習の生徒数(対照群)の半分しか確保できません。つまり、サンプルサイズの比は n₁ / n₂ = 0.5、すなわち n₂ = 2 * n₁ となります。

サンプルサイズ設計のためのパラメータ設定:

  1. 有意水準 (α): 慣例に従い 0.05 に設定します。
  2. 検出力 (1 – β): 新アプリに本当に効果があるなら、それを80%の確率で検出したいので 0.80 に設定します。
  3. 効果量 (d):

    • 過去の全国調査から、この年代の数学テストの点数の標準偏差は σ = 15点 であることが分かっています。
    • 教育的に意味のある差として、平均点が 7.5点 向上すれば、このアプリは導入する価値があると判断します。
    • したがって、効果量 d = (μ₁ - μ₂) / σ = 7.5 / 15 = 0.5(中程度の効果量)を検出できるようにしたいと考えます。
  4. サンプルサイズの比 (k): 対照群(n₂)が介入群(n₁)の2倍なので k = n₂ / n₁ = 2

このシナリオに基づき、「介入群n₁人、対照群n₂=2*n₁人の生徒で研究を行った場合、平均点に7.5点の差があれば、その差を80%の確率で統計的に有意(p < 0.05)だと結論付けられるか?」という問いに答えるためのサンプルサイズを計算し、シミュレーションで検証します。

各パラメータの意味

  1. 有意水準 (α, alpha)

    • 帰無仮説(H₀: 2群の母平均に差はない)が棄却されないにもかかわらず、それを棄却してしまう確率。「第1種の過誤」を犯す確率です。
    • 一般的に α = 0.05 (5%) に設定されます。これは、偶然によって「差がある」と誤って結論づけるリスクを5%に抑えることを意味します。
  2. 検出力 (Power, 1 – β)

    • 対立仮説(H₁: 2群の母平均に差がある)が真であるときに、それを正しく検出(帰無仮説を棄却)できる確率です。β は「第2種の過誤」(差があるのに見逃してしまう確率)です。
    • 一般的に 1 - β = 0.80 (80%) や 0.90 (90%) に設定されます。これは、本当に存在する差を80%(または90%)の確率で見つけ出せるようにしたい、という目標を意味します。
  3. 効果量 (Effect Size, d)

    • 検出したい2群間の差の大きさを示す、標準化された指標です。一般的にCohen’s dが用いられます。
    • d = (μ₁ - μ₂) / σ

      • μ₁ - μ₂: 検出したい2群の母平均の差。
      • σ: 母標準偏差(2群で共通と仮定)。
    • 効果量の目安: d = 0.2 (小), d = 0.5 (中), d = 0.8 (大)
    • 研究分野の過去の知見や、臨床的に意味のある最小の差に基づいて設定します。
  4. サンプルサイズの比 (k)

    • 2群のサンプルサイズの比率です。k = n₂ / n₁
    • 例えば、一方の群の被験者を見つけるのが難しい場合や、コストが異なる場合などに、不等サンプルサイズ(k ≠ 1)が用いられます。

2. R言語によるシミュレーションコード

まず、必要なライブラリを読み込みます。

library(pwr)
library(ggplot2)
library(dplyr)

seed <- 20250706

ステップ1: シナリオに基づいたサンプルサイズの計算

Rのpwrパッケージにあるpwr.t2n.test関数は、不等サンプルサイズのt検定におけるサンプルサイズ設計に使用できます。

n2 = 2 * n1 という比率を保ったまま、両方の群のサンプルサイズを見つける ために、n1 を小さい値から1つずつ増やしていき、n2 = 2 * n1 という関係を保ちながら、検出力が目標値(0.8)を最初に超える n1 を探します。

# パラメータ設定
effect_size <- 0.5 # Cohen's d
sig_level <- 0.05 # 有意水準 alpha
power_level <- 0.80 # 検出力 1-beta
n_ratio <- 2 # サンプルサイズの比 n2/n1

# n1を小さい値から1つずつ増やし、検出力が目標を超える最初の値を見つける
n1_current <- 2 # 探索を開始するn1の初期値
calculated_power <- 0

# 検出力が目標値(power_level)に達するまでループを続ける
while (calculated_power < power_level) {
  n1_current <- n1_current + 1
  n2_current <- n1_current * n_ratio

  # 現在のn1, n2における検出力を計算
  # power = NULLとすることで、検出力を計算させる
  power_result <- pwr.t2n.test(
    d = effect_size,
    sig.level = sig_level,
    power = NULL,
    n1 = n1_current,
    n2 = n2_current
  )

  # 計算された検出力を更新
  calculated_power <- power_result$power
}

# ループを抜けた時点のn1, n2が求めるサンプルサイズ
n1_required <- n1_current
n2_required <- n1_required * n_ratio

# 結果の表示
print(paste("介入群(n1)に必要なサンプルサイズ:", n1_required))
print(paste("対照群(n2)に必要なサンプルサイズ:", n2_required))
print(paste("その際の検出力:", round(calculated_power, 3)))
[1] "介入群(n1)に必要なサンプルサイズ: 48"
[1] "対照群(n2)に必要なサンプルサイズ: 96"
[1] "その際の検出力: 0.802"

このコードでは、whileループを使って n1 を3から順に増やしていき、各ステップで n2 も計算し、そのペアで検出力を求めます。計算された検出力が目標の 0.80 を超えた瞬間にループが停止し、その時点の n1n2 が、私たちが求めるサンプルサイズとなります。

ステップ2: 検出力のシミュレーション (対立仮説が真の場合)

計算で得られたサンプルサイズ(n₁=48, n₂=96)を用いて、本当に検出力が80%程度になるかシミュレーションで確認します。

# シミュレーションのパラメータ設定
set.seed(seed) # 結果を再現可能にする
n_sim <- 10000 # シミュレーション回数

# シナリオの母集団パラメータ
mean_g1 <- 7.5 # 介入群の平均点の上昇分
mean_g2 <- 0 # 対照群の平均点の上昇分
sd_val <- 15 # 両群共通の標準偏差
# (μ1 - μ2) / σ = (7.5 - 0) / 15 = 0.5 となり、効果量と一致

# 結果を格納するベクトル
p_values_h1 <- numeric(n_sim)

# シミュレーションループ
for (i in 1:n_sim) {
  # 1. 対立仮説が真である状況のデータを生成
  # 介入群データ (新アプリ)
  group1_data <- rnorm(n = n1_required, mean = mean_g1, sd = sd_val)
  # 対照群データ (従来学習)
  group2_data <- rnorm(n = n2_required, mean = mean_g2, sd = sd_val)

  # 2. Welchのt検定を実行 (不等分散を仮定)
  t_test_result <- t.test(group1_data, group2_data, var.equal = FALSE)

  # 3. p値を保存
  p_values_h1[i] <- t_test_result$p.value
}

# シミュレーションによる検出力の計算
simulated_power <- mean(p_values_h1 < sig_level)

print(paste("シミュレーションによる検出力:", round(simulated_power, 3)))
[1] "シミュレーションによる検出力: 0.796"

シミュレーション結果は、目標とした検出力0.80に近い値(約0.796)となり、サンプルサイズ設計が妥当であったことを裏付けています。

ステップ3: 検出力シミュレーション結果の可視化

シミュレーションで得られたp値の分布をヒストグラムで可視化します。対立仮説が真の場合、p値は0に近い方に偏ります。

# p値をデータフレームに変換
p_values_df_h1 <- data.frame(p_value = p_values_h1)

# --- 検出力シミュレーション結果の可視化 ---
ggplot(p_values_df_h1, aes(x = p_value)) +
  geom_histogram(binwidth = 0.05, fill = "skyblue", color = "black", boundary = 0) +
  geom_vline(xintercept = sig_level, color = "red", linetype = "dashed", linewidth = 1) +
  annotate("text",
    x = 0.45, y = 4000,
    label = paste("シミュレーションによる検出力 (p < 0.05) =", round(simulated_power, 3)),
    color = "red", size = 5
  ) +
  labs(
    title = "p値の分布(対立仮説が真の場合)",
    subtitle = paste0("介入群n₁=", n1_required, ", 対照群n₂=", n2_required, ", 効果量d=", effect_size),
    x = "p値",
    y = "度数"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 16, face = "bold"),
    plot.subtitle = element_text(size = 12)
  )
Figure 1

Figure 1 から、p値が0.05(赤い破線)よりも左側に分布する割合(=検出力)が全体の約80%を占めていることが視覚的にわかります。

ステップ4: 第1種の過誤のシミュレーション (帰無仮説が棄却されない場合)

次に、もしアプリに全く効果がなかった場合(帰無仮説が棄却されない)に、誤って「効果あり」と結論づけてしまう確率(第1種の過誤率)が、設定した有意水準α (0.05) と一致するかを確認します。

# 結果を格納するベクトル
p_values_h0 <- numeric(n_sim)

# シミュレーションループ (帰無仮説が棄却されない)
for (i in 1:n_sim) {
  # 1. 帰無仮説が棄却されない状況のデータを生成 (平均点の差がない)
  group1_data_h0 <- rnorm(n = n1_required, mean = 0, sd = sd_val)
  group2_data_h0 <- rnorm(n = n2_required, mean = 0, sd = sd_val)

  # 2. Welchのt検定を実行
  t_test_result_h0 <- t.test(group1_data_h0, group2_data_h0, var.equal = FALSE)

  # 3. p値を保存
  p_values_h0[i] <- t_test_result_h0$p.value
}

# シミュレーションによる第1種の過誤率の計算
type1_error_rate <- mean(p_values_h0 < sig_level)

print(paste("シミュレーションによる第1種の過誤率:", round(type1_error_rate, 3)))
[1] "シミュレーションによる第1種の過誤率: 0.048"

シミュレーションによる第1種の過誤率は、設定した有意水準0.05に近い値(約0.048)となり、検定が正しくコントロールされていることがわかります。

ステップ5: 第1種の過誤シミュレーション結果の可視化

帰無仮説が棄却されない場合、p値は[0, 1]の範囲で一様に分布するはずです。

# p値をデータフレームに変換
p_values_df_h0 <- data.frame(p_value = p_values_h0)

# --- 第1種の過誤シミュレーション結果の可視化 ---
ggplot(p_values_df_h0, aes(x = p_value)) +
  geom_histogram(binwidth = 0.05, fill = "lightcoral", color = "black", boundary = 0) +
  geom_vline(xintercept = sig_level, color = "blue", linetype = "dashed", linewidth = 1) +
  geom_hline(yintercept = n_sim * 0.05, color = "darkgreen", linetype = "dotted", linewidth = 1) +
  annotate("text",
    x = 0.55, y = 600,
    label = paste("第1種の過誤の確率 (p < 0.05) =", round(type1_error_rate, 3)),
    color = "blue", size = 5
  ) +
  labs(
    title = "p値の分布(帰無仮説が棄却されない場合)",
    subtitle = "p値は一様に分布する",
    x = "p値",
    y = "度数"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 16, face = "bold"),
    plot.subtitle = element_text(size = 12)
  )
Figure 2

Figure 2 は、p値が特定の値に偏ることなく、ほぼ一様に分布していることを示しています。緑の点線は、完全な一様分布の場合の期待される度数(n_sim * binwidth = 10000 * 0.05 = 500)を示しています。

以上です。