Bootstrap 가설 검정과 한계

Woodward 14.6+14.7 — H0 강제 부트스트랩과 부트스트랩의 적용 한계

Woodward Ch.14.6 (Bootstrap 가설 검정) 과 14.7 (Limitations) 을 정리한다. 부트스트랩으로 p 값을 계산하는 두 절차 (CI 기반, H0 강제 표집), 작은 표본의 한계, 극단 통계량의 한계, 의존성 무시의 위험을 자세히 다룬다.

Experimentation
Fundamentals
저자

Kwangmin Kim

공개

2026년 05월 08일

1 도입 — Bootstrap 으로 p 값 계산

지금까지 부트스트랩의 CI 추론 을 다뤘다. 부트스트랩은 가설 검정 (p 값 계산) 에도 사용 가능. 두 가지 접근이 있다.

  1. CI 기반 — CI 가 \(H_0\) 값을 포함하는지로 검정 결정
  2. \(H_0\) 강제 부트스트랩 — 영가설 분포를 직접 시뮬레이션

이 글은 두 접근과 부트스트랩의 한계 를 다룬다.

2 Approach 1 — CI 기반 검정

2.1 절차

CI 기반 검정
  1. 부트스트랩으로 통계량 \(\hat{\theta}\)\(1 - \alpha\) CI 계산
  2. CI 가 \(H_0\) 값 (\(\theta_0\), 보통 0) 을 포함 하면 기각하지 못함
  3. 포함하지 않으면 기각

이 절차는 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\) 강제 Bootstrap
  1. 자료를 \(H_0\) 가 참이 되도록 변형
  2. 변형 자료에서 부트스트랩 통계량 계산
  3. 관측 통계량보다 극단적인 비율 = 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_value

3.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\) 하의 분포 모방.

직관 — Centering 의 의미

원 자료에서 부트스트랩하면 원 평균 차이가 보존. 따라서 \(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

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: