1 도입 — Bootstrap 으로 p 값 계산
지금까지 부트스트랩의 CI 추론 을 다뤘다. 부트스트랩은 가설 검정 (p 값 계산) 에도 사용 가능. 두 가지 접근이 있다.
- CI 기반 — CI 가 \(H_0\) 값을 포함하는지로 검정 결정
- \(H_0\) 강제 부트스트랩 — 영가설 분포를 직접 시뮬레이션
이 글은 두 접근과 부트스트랩의 한계 를 다룬다.
2 Approach 1 — CI 기반 검정
2.1 절차
- 부트스트랩으로 통계량 \(\hat{\theta}\) 의 \(1 - \alpha\) CI 계산
- CI 가 \(H_0\) 값 (\(\theta_0\), 보통 0) 을 포함 하면 기각하지 못함
- 포함하지 않으면 기각
이 절차는 CI 와 검정의 쌍대성 (duality) 을 활용.
2.2 사례
A/B 테스트 매출 차이의 부트스트랩 CI = \((2.5, 7.8)\).
- \(H_0: \mu_T - \mu_C = 0\)
- 0 이 CI 에 포함되지 않음 → 기각
- 효과 양수, 통계적 유의
2.3 장점
- 단순
- 효과 크기 + 검정 한 도구로
2.4 한계
- \(H_0\) 가 경계 위 에 있는 경우 (예: 대등성 검정) 부정확
3 Approach 2 — \(H_0\) 강제 Bootstrap
3.1 절차
- 자료를 \(H_0\) 가 참이 되도록 변형
- 변형 자료에서 부트스트랩 통계량 계산
- 관측 통계량보다 극단적인 비율 = p 값
3.2 사례 — 두 그룹 평균 차이
\(H_0: \mu_T - \mu_C = 0\).
def bootstrap_hypothesis_test(group_T, group_C, n_boot=5000):
obs_diff = group_T.mean() - group_C.mean()
# H_0 강제: 두 그룹의 평균을 *공통 평균* 으로 이동
pooled_mean = np.concatenate([group_T, group_C]).mean()
T_centered = group_T - group_T.mean() + pooled_mean
C_centered = group_C - group_C.mean() + pooled_mean
# 부트스트랩
boot_diffs = []
for _ in range(n_boot):
T_b = np.random.choice(T_centered, len(T_centered), replace=True)
C_b = np.random.choice(C_centered, len(C_centered), replace=True)
boot_diffs.append(T_b.mean() - C_b.mean())
boot_diffs = np.array(boot_diffs)
p_value = np.mean(np.abs(boot_diffs) >= np.abs(obs_diff))
return p_value3.3 핵심 — Centering
자료를 \(H_0\) 분포로 이동 시키는 centering 이 핵심:
- \(T_{\text{centered}} = T - \bar{T} + \bar{X}_{\text{pooled}}\)
- \(C_{\text{centered}} = C - \bar{C} + \bar{X}_{\text{pooled}}\)
이 절차로 분산은 보존 + 평균을 같게. \(H_0\) 하의 분포 모방.
원 자료에서 부트스트랩하면 원 평균 차이가 보존. 따라서 \(H_0\) 의 분포를 시뮬레이션 못함.
Centering: 두 그룹의 평균을 같은 값 으로 이동 → \(H_0\) 분포 시뮬레이션. 이 분포에서 관측 차이 (\(\bar{T} - \bar{C}\)) 가 얼마나 극단적 인지 측정 → p 값.
이 절차는 permutation test 와 비슷 하지만 복원 추출 사용. Permutation 은 비복원 (라벨 셔플).
A/B 테스트의 \(H_0\) 강제 부트스트랩이 robustness check 로 유용. 결과가 t 검정과 다르면 자료의 가정 위반 의심.
4 Permutation 검정과의 관계
4.1 차이점
| 측면 | \(H_0\) 강제 Bootstrap | Permutation |
|---|---|---|
| 추출 | 복원 추출 (centered) | 비복원 (라벨 셔플) |
| 가정 | 그룹 분산 비슷 (centering) | 그룹 분포 동일 |
| 정확성 | 근사 | 정확 |
| 확장성 | 임의 통계량 | 임의 통계량 |
4.2 사용 시점
- Permutation: 작은 표본 + 정확한 p 값 필요 (A-WOO14-5 자세히)
- Bootstrap H_0: 큰 표본 + CI 와 일관성
대부분의 경우 둘 다 비슷한 결과. Permutation 이 약간 더 정확.
5 사례 — 회귀 계수의 Bootstrap 검정
회귀 계수 \(\beta_1\) 의 \(H_0: \beta_1 = 0\) 검정:
5.1 절차
def regression_h0_boot(X, Y, n_boot=5000):
n = len(Y)
# 원 모형
beta_orig = np.linalg.lstsq(np.column_stack([np.ones(n), X]), Y, rcond=None)[0]
obs_beta1 = beta_orig[1]
# H_0: β_1 = 0 강제 — Y 에서 X 의 예측 효과 제거
Y_residual = Y - X * beta_orig[1] # X 효과 제거
# 잔차에서 부트스트랩
boot_beta1 = []
for _ in range(n_boot):
idx = np.random.choice(n, n, replace=True)
Y_b = Y_residual[idx] # 부트스트랩 잔차 (β_1=0 모형 하)
X_b = X[idx]
beta = np.linalg.lstsq(np.column_stack([np.ones(n), X_b]), Y_b, rcond=None)[0]
boot_beta1.append(beta[1])
boot_beta1 = np.array(boot_beta1)
p_value = np.mean(np.abs(boot_beta1) >= np.abs(obs_beta1))
return p_value, obs_beta1이 절차로 분포 가정 없이 회귀 계수 p 값 계산.
6 Bootstrap 의 한계
Woodward 가 강조하는 5 가지 한계:
6.1 한계 1 — 작은 표본
\(n = 5 \sim 10\) 같은 매우 작은 표본:
- 가능한 부트스트랩 표본 수 한정
- 자료가 모집단 대표 못함
- 추정 부정확
권장: \(n \geq 30\) 에서 부트스트랩.
6.2 한계 2 — 극단 통계량
최댓값, 최솟값, 극단 분위수 같은 통계량은 부트스트랩 부정확.
이유: 부트스트랩 표본의 max 가 항상 원 자료의 max (또는 그 이하). 표집 분포의 왜곡.
대안: 극단값 이론 (extreme value theory) 또는 m-out-of-n bootstrap.
6.3 한계 3 — 의존성 자료
시계열, 클러스터, 공간 자료에서 단순 부트스트랩 부적절.
대안: A-WOO14-3 의 block bootstrap, cluster bootstrap 등 변형.
6.4 한계 4 — Plug-in 추정의 편향
복잡 통계량의 plug-in 추정 이 편향. 부트스트랩 분포도 편향 반영 → CI 부정확.
대안: BCa 또는 jackknife 보정.
6.5 한계 5 — 매우 비대칭 분포의 꼬리
분포 꼬리가 매우 두꺼움 (Cauchy 같은 무한 분산):
- CLT 미작동
- 부트스트랩 분포가 유한 평균에 수렴 X
대안: 분위수 기반 통계량 (median, quartiles), trimmed mean.
부트스트랩이 강력하지만 모든 문제 해결 은 아니다. 한계 인식이 중요.
A/B 테스트 사례:
- Long-tail revenue (소수 사용자가 매출 대부분) — 부트스트랩 평균 추정 부정확. Median 이나 trimmed mean 사용.
- 작은 segment (예: 신규 사용자) — 표본 부족. 베이즈 분석 또는 segment 통합.
- 시간 효과 — 일별 자료에 자기 상관. Block bootstrap 또는 모형 기반 분석.
부트스트랩의 적절한 적용 이 핵심. 자료 구조 인식 후 변형 선택.
7 부트스트랩의 정확성 점검
7.1 시뮬레이션 기반 검증
자료의 진짜 모집단 을 안다면 (시뮬레이션), 부트스트랩 CI 의 coverage 를 직접 측정.
import numpy as np
# 알려진 모집단
true_mean = 100
population = np.random.lognormal(np.log(100) - 0.5, 1.0, 1000000)
print(f"진짜 모집단 평균: {population.mean():.2f}")
# 시뮬레이션
n_simulations = 1000
n_sample = 50
coverage_count = 0
for _ in range(n_simulations):
sample = np.random.choice(population, n_sample, replace=False)
# 부트스트랩 CI
boot_means = [np.random.choice(sample, n_sample, replace=True).mean()
for _ in range(2000)]
ci = np.percentile(boot_means, [2.5, 97.5])
if ci[0] < true_mean < ci[1]:
coverage_count += 1
print(f"95 % CI Coverage: {coverage_count / n_simulations:.3f}")
# 이상적: 0.95 근처이 절차로 부트스트랩의 정확성 직접 검증. 95 % 미만이면 한계 있음.
7.2 진단 도구
- Bootstrap 분포 시각화 — 정규에서 벗어남?
- 원 통계량 과 부트스트랩 분포 평균 비교 — 편향 있음?
- Jackknife 와 비교 — 일관성?
8 CI 와 검정의 쌍대성
부트스트랩 CI 가 \(H_0\) 값을 포함하면 p ≥ α, 포함하지 않으면 p < α. 이 쌍대성 이 두 접근의 일관성 보장.
8.1 사례
| 자료 | 95 % CI | \(H_0\) 포함? | p 값 |
|---|---|---|---|
| A | \((2.5, 7.8)\) | 0 미포함 | < 0.05 |
| B | \((-1.2, 4.5)\) | 0 포함 | > 0.05 |
| C | \((0, 3.2)\) | 0 경계 | ≈ 0.05 |
이 일관성은 모든 부트스트랩 방법 에서 성립.
9 A/B 테스트의 부트스트랩 검정
import numpy as np
np.random.seed(42)
n_per = 1000
control = np.random.lognormal(4, 1.0, n_per)
treatment = np.random.lognormal(4.05, 1.0, n_per)
# Method 1 — CI 기반
diff_boot = []
for _ in range(5000):
c_b = np.random.choice(control, n_per, replace=True)
t_b = np.random.choice(treatment, n_per, replace=True)
diff_boot.append(t_b.mean() - c_b.mean())
ci = np.percentile(diff_boot, [2.5, 97.5])
print(f"95 % CI: ({ci[0]:.4f}, {ci[1]:.4f})")
print(f" → 0 포함? {ci[0] < 0 < ci[1]}")
# Method 2 — H_0 강제
obs_diff = treatment.mean() - control.mean()
combined = np.concatenate([control, treatment])
pooled_mean = combined.mean()
control_centered = control - control.mean() + pooled_mean
treatment_centered = treatment - treatment.mean() + pooled_mean
h0_diffs = []
for _ in range(5000):
c_b = np.random.choice(control_centered, n_per, replace=True)
t_b = np.random.choice(treatment_centered, n_per, replace=True)
h0_diffs.append(t_b.mean() - c_b.mean())
p_boot = np.mean(np.abs(h0_diffs) >= np.abs(obs_diff))
print(f"\nH_0 강제 Bootstrap p = {p_boot:.4f}")
# Method 3 — Permutation (비교)
combined_perm = np.concatenate([control, treatment])
perm_diffs = []
for _ in range(5000):
np.random.shuffle(combined_perm)
perm_diffs.append(combined_perm[:n_per].mean() - combined_perm[n_per:].mean())
p_perm = np.mean(np.abs(perm_diffs) >= np.abs(obs_diff))
print(f"Permutation p = {p_perm:.4f}")
# Method 4 — Welch t (전통, 비교)
from scipy.stats import ttest_ind
t_stat, p_t = ttest_ind(treatment, control, equal_var=False)
print(f"Welch t: p = {p_t:.4f}")4 방법의 결과 일치 가 robustness 의 증거.
10 검정 결과 보고 형식
A/B 테스트 결과:
Primary metric (매출):
관측 효과: +$5.30 / 사용자
Bootstrap 95 % CI (BCa): ($3.20, $7.80)
Bootstrap H_0 p-value: 0.001
Welch t p-value: 0.002 (robustness check)
결론: 매출 효과 유의 (p < 0.01). 효과 크기 추정의 정밀도 양호.
이 형식이 부트스트랩 + 전통 검정 + CI 의 통합.
11 Bootstrap 검정의 추가 변형
11.1 Bootstrap Likelihood Ratio Test
import numpy as np
def bootstrap_lrt(group_A, group_B, n_boot=5000):
"""가설 검정의 Bootstrap LRT 변형"""
# H_0: 두 그룹이 같은 분포
n_A, n_B = len(group_A), len(group_B)
combined = np.concatenate([group_A, group_B])
# 관측 LR
obs_lr = compute_lr(group_A, group_B)
# H_0 강제 (centered)
boot_lrs = []
for _ in range(n_boot):
sample = np.random.choice(combined, n_A + n_B, replace=True)
a_b = sample[:n_A]
b_b = sample[n_A:]
boot_lrs.append(compute_lr(a_b, b_b))
return np.mean(np.array(boot_lrs) >= obs_lr)
def compute_lr(a, b):
"""단순 LR — 평균 차이의 표준화"""
return (a.mean() - b.mean()) / np.sqrt(a.var()/len(a) + b.var()/len(b))11.2 Wild Bootstrap (Heteroscedastic)
이분산 자료의 부트스트랩:
def wild_bootstrap_test(X, Y, n_boot=5000):
"""Wild bootstrap 회귀 계수 검정"""
n = len(Y)
X_design = np.column_stack([np.ones(n), X])
beta_orig = np.linalg.lstsq(X_design, Y, rcond=None)[0]
obs_beta1 = beta_orig[1]
# H_0: β_1 = 0
Y_under_null = beta_orig[0] + np.zeros(n) # X 효과 없음
residuals_null = Y - Y_under_null
boot_betas = []
for _ in range(n_boot):
# Rademacher 부호
signs = np.random.choice([-1, 1], n)
Y_b = Y_under_null + residuals_null * signs
beta_b = np.linalg.lstsq(X_design, Y_b, rcond=None)[0][1]
boot_betas.append(beta_b)
return np.mean(np.abs(boot_betas) >= np.abs(obs_beta1))11.3 Studentized Bootstrap (Bootstrap-t)
def studentized_bootstrap_test(group_A, group_B, n_boot=5000):
"""Studentized bootstrap (가장 정확)"""
obs_diff = group_A.mean() - group_B.mean()
obs_se = np.sqrt(group_A.var()/len(group_A) + group_B.var()/len(group_B))
obs_t = obs_diff / obs_se
# H_0 강제
pooled = np.concatenate([group_A, group_B])
pooled_mean = pooled.mean()
A_centered = group_A - group_A.mean() + pooled_mean
B_centered = group_B - group_B.mean() + pooled_mean
boot_ts = []
for _ in range(n_boot):
a_b = np.random.choice(A_centered, len(group_A), replace=True)
b_b = np.random.choice(B_centered, len(group_B), replace=True)
diff = a_b.mean() - b_b.mean()
se = np.sqrt(a_b.var()/len(a_b) + b_b.var()/len(b_b))
boot_ts.append(diff / se if se > 0 else 0)
return np.mean(np.abs(boot_ts) >= np.abs(obs_t))12 Bootstrap 검정의 시뮬레이션 비교
12.1 Type I Error 측정
영가설 시나리오에서 false positive 비율 측정.
import numpy as np
np.random.seed(42)
n_sim = 1000
n_per = 30
# 4 검정의 Type I error
def t_test_p(a, b):
from scipy.stats import ttest_ind
return ttest_ind(a, b)[1]
def bootstrap_h0_p(a, b, n_boot=2000):
obs = a.mean() - b.mean()
pooled = np.concatenate([a, b])
pm = pooled.mean()
a_c = a - a.mean() + pm
b_c = b - b.mean() + pm
diffs = []
for _ in range(n_boot):
a_b = np.random.choice(a_c, len(a), replace=True)
b_b = np.random.choice(b_c, len(b), replace=True)
diffs.append(a_b.mean() - b_b.mean())
return np.mean(np.abs(diffs) >= np.abs(obs))
results = {'t': 0, 'bootstrap_h0': 0}
for _ in range(n_sim):
a = np.random.normal(0, 1, n_per)
b = np.random.normal(0, 1, n_per)
if t_test_p(a, b) < 0.05:
results['t'] += 1
if bootstrap_h0_p(a, b, 500) < 0.05:
results['bootstrap_h0'] += 1
for method, count in results.items():
print(f"{method}: Type I error = {count/n_sim:.3f}")
# 둘 다 약 0.05 (양쪽 정확)12.2 Power 측정
대립가설 시나리오에서 검정력 측정.
results_power = {'t': 0, 'bootstrap_h0': 0}
for _ in range(n_sim):
a = np.random.normal(0.5, 1, n_per) # 효과 0.5
b = np.random.normal(0, 1, n_per)
if t_test_p(a, b) < 0.05:
results_power['t'] += 1
if bootstrap_h0_p(a, b, 500) < 0.05:
results_power['bootstrap_h0'] += 1
for method, count in results_power.items():
print(f"{method}: Power = {count/n_sim:.3f}")T 검정과 부트스트랩이 비슷한 검정력. 정규 자료에서 거의 동등.
13 Bootstrap 의 통계적 한계
13.1 한계 1 — Pivot 통계량의 부재
Studentized 부트스트랩이 가장 정확하지만 Pivot 통계량 필요. 일부 통계량은 pivot 없음 (예: 분위수).
해법: 비-pivot 부트스트랩 (BCa) 사용.
13.2 한계 2 — Coverage 의 부정확성
Bootstrap CI 의 coverage 가 목표 95 % 와 약간 다를 수 있음. 비대칭 자료에서.
해결: BCa 또는 Bootstrap-t.
13.3 한계 3 — Edge Cases
- 영-인플레이션 자료
- 카테고리 자료 (희귀 범주)
- 극단 분위수
이 경우 전문화 변형 필요.
14 후속 — Permutation Tests
다음 글 A-WOO14-5 는 순열 검정 (permutation tests) 을 자세히 다룬다. 부트스트랩 검정의 자매격.
15 관련 주제
선행 지식
후속 주제 (Phase A)
- A-WOO14-5 Permutation Tests
다른 카테고리 연결