회귀에서의 부트스트랩과 사용 결정 트리

Buisson Ch.7 — 회귀 부트스트랩의 두 형태 + 부트스트랩의 적용 결정

회귀 모형에서 부트스트랩의 두 가지 형태 (cases bootstrap, residual bootstrap), 계수의 CI 와 예측의 CI 구분, 가정 위반 시의 robust 추론, 부트스트랩 사용 결정 트리, A/B 테스트의 회귀 분석 응용을 자세히 다룬다.

Experimentation
Fundamentals
저자

Kwangmin Kim

공개

2026년 05월 08일

1 도입 — 회귀의 두 부트스트랩

회귀 모형 \(Y = \beta_0 + \beta_1 X + \varepsilon\) 에서 부트스트랩 적용은 두 가지 형태 가 있다.

  1. Cases Bootstrap — 자료점 \((X_i, Y_i)\) 쌍을 함께 복원 추출
  2. Residual Bootstrap — 잔차 \(\varepsilon_i\) 만 부트스트랩, \(X\) 고정

이 글은 두 형태의 차이, 적용 시점, 그리고 부트스트랩 사용 결정 트리를 다룬다.

2 Cases Bootstrap (자료점 부트스트랩)

정의: Cases Bootstrap

원 자료 \(\{(X_i, Y_i)\}_{i=1}^n\) 에서 복원 추출\(n\) 을 추출하여 새 자료 생성. 이 자료에 모형 적합 → 부트스트랩 계수.

def cases_bootstrap(X, Y, n_boot=5000):
    n = len(Y)
    boot_coefs = []
    for _ in range(n_boot):
        idx = np.random.choice(n, size=n, replace=True)
        X_b, Y_b = X[idx], Y[idx]
        # 회귀 적합 (예: OLS)
        coefs = np.linalg.lstsq(np.column_stack([np.ones(n), X_b]), Y_b, rcond=None)[0]
        boot_coefs.append(coefs)
    return np.array(boot_coefs)

2.1 가정

  • 자료점이 독립 (i.i.d.)
  • \(X\)무작위 (random design)

2.2 장점

  • 분포 가정 X
  • 이분산성·비정규성 에 강건
  • 가장 일반적

2.3 사용 시점

  • \(X\) 가 무작위로 표집됨 (관찰 자료)
  • A/B 테스트, 사용자 행동 자료
  • 잔차 분포 가정 의심 시

2.4 표준 OLS SE 와의 비교

표준 OLS:

\[ \text{SE}(\hat{\beta}_1) = \sqrt{\frac{\hat{\sigma}^2}{\sum (X_i - \bar{X})^2}} \]

가정: 잔차가 정규 + 등분산. 위반 시 SE 부정확.

Cases bootstrap: 자료에서 직접 SE 추정. 가정 약함.

직관 — 왜 Cases Bootstrap 이 자연스러운가

A/B 테스트 자료를 떠올려 본다. 사용자가 무작위로 표집 되어 (또는 트래픽으로 자연스럽게 들어와) 각 사용자의 \((X_i, Y_i)\) 가 관측됨. 즉:

  • \(X_i\): 사용자 특성 (variant, age, segment 등)
  • \(Y_i\): 사용자 outcome (매출, 클릭 등)

자체가 무작위 표본. Cases bootstrap 은 이 표본을 재생성 하므로 자연스럽게 자료의 변동성을 반영.

대조: 실험 설계에서 \(X\)고정 된 경우 (예: 각 처리 수준에서 \(n_j\) 명 측정). 이때 \(X\) 분포가 고정 이라 cases bootstrap 이 약간 부적절. Residual bootstrap 이 더 적합.

A/B 테스트는 후자 구조 — variant 가 사전 결정 + 사용자 무작위 표집. 두 형태 모두 작동하지만 cases 가 더 자연스러움.

3 Residual Bootstrap (잔차 부트스트랩)

정의: Residual Bootstrap

원 모형 \(\hat{Y}_i = \hat{\beta}_0 + \hat{\beta}_1 X_i\) 적합 후, 잔차 \(\hat{\varepsilon}_i = Y_i - \hat{Y}_i\) 를 부트스트랩.

def residual_bootstrap(X, Y, n_boot=5000):
    n = len(Y)
    # 원 모형 적합
    coefs_orig = np.linalg.lstsq(np.column_stack([np.ones(n), X]), Y, rcond=None)[0]
    Y_hat = coefs_orig[0] + coefs_orig[1] * X
    residuals = Y - Y_hat

    boot_coefs = []
    for _ in range(n_boot):
        # 잔차 부트스트랩
        eps_b = np.random.choice(residuals, size=n, replace=True)
        Y_b = Y_hat + eps_b
        coefs = np.linalg.lstsq(np.column_stack([np.ones(n), X]), Y_b, rcond=None)[0]
        boot_coefs.append(coefs)
    return np.array(boot_coefs)

3.1 가정

  • 잔차가 동일 분포 (그러나 어떤 분포든)
  • 잔차가 독립
  • \(X\)고정 (fixed design)

3.2 장점

  • 정규성 가정 X
  • 등분산성 약화
  • 실험 설계에 자연스러움

3.3 사용 시점

  • \(X\) 가 고정 (실험 설계)
  • 잔차 분포 가정 의심

4 두 형태의 비교

측면 Cases Residual
자료 구조 \(X\) 무작위 \(X\) 고정
가정 독립 자료점 동일 분포 잔차
이분산성 강건 부분 강건
일반성 높음 중간
권장 대부분의 경우 전통 실험 설계

A/B 테스트에서는 Cases 권장.

5 회귀 계수의 CI

부트스트랩 계수 \(\hat{\beta}^*_1\) 의 분포에서 직접 CI:

# 위 함수로 부트스트랩 후
boot_coefs = cases_bootstrap(X, Y, n_boot=5000)
beta1_boot = boot_coefs[:, 1]

# Percentile CI
ci_perc = np.percentile(beta1_boot, [2.5, 97.5])

# BCa CI (scipy 사용)
from scipy.stats import bootstrap
def get_beta1(idx_arr):
    X_b = X[idx_arr]
    Y_b = Y[idx_arr]
    return np.linalg.lstsq(np.column_stack([np.ones(len(idx_arr)), X_b]), Y_b, rcond=None)[0][1]

# 더 간단히 — scipy 기본 활용
def slope(x_y):
    x, y = x_y[:, 0], x_y[:, 1]
    return np.cov(x, y)[0, 1] / np.var(x)

# bootstrap with paired
data = np.column_stack([X, Y])
res = bootstrap((data,), slope, n_resamples=5000, method='BCa', random_state=42)
print(f"β1 95 % CI: ({res.confidence_interval.low:.3f}, {res.confidence_interval.high:.3f})")

6 예측의 CI vs 계수의 CI

회귀 부트스트랩에는 두 가지 다른 CI 가 있다.

정의: Confidence vs Prediction Interval

Confidence Interval for \(\hat{Y}_{x_0}\)평균 예측 의 CI (모형의 기대값 에 대한 불확실성).

Prediction Interval for \(Y_{x_0}\)개별 예측 의 PI (모형 + 잔차 변동성 모두).

PI 가 항상 더 넓다.

6.1 Bootstrap CI 절차

# 새로운 X = x0 에서 평균 예측의 CI
x0 = 5.0
predictions_boot = []
for boot_coef in boot_coefs:
    y_pred = boot_coef[0] + boot_coef[1] * x0
    predictions_boot.append(y_pred)
ci_pred = np.percentile(predictions_boot, [2.5, 97.5])

6.2 Bootstrap PI 절차

# 개별 예측의 PI — 잔차 변동도 포함
predictions_pi = []
for _ in range(B):
    idx = np.random.choice(n, size=n, replace=True)
    X_b, Y_b = X[idx], Y[idx]
    coefs = np.linalg.lstsq(np.column_stack([np.ones(n), X_b]), Y_b, rcond=None)[0]
    y_pred_mean = coefs[0] + coefs[1] * x0
    # 잔차에서 추출
    Y_b_hat = coefs[0] + coefs[1] * X_b
    residual_b = np.random.choice(Y_b - Y_b_hat, size=1)[0]
    predictions_pi.append(y_pred_mean + residual_b)
pi = np.percentile(predictions_pi, [2.5, 97.5])
직관 — CI vs PI 의 차이

A/B 테스트 사례:

  • CI: “평균 매출이 +5 % 향상한다는 것의 불확실성. 95 % 확률로 +3 % ~ +7 %.”
  • PI: “개별 사용자의 매출 변화의 분포. 95 % 사용자가 -10 % ~ +20 % 변화.”

CI 는 집단 효과 의 불확실성, PI 는 개별 효과 + 잔차 변동 의 불확실성. 비즈니스 의사결정 (전체 사용자 평균 매출 변화) 은 CI 가 적절. 개별 사용자 예측은 PI 가 적절.

7 회귀 진단의 부트스트랩

7.1 잔차의 부트스트랩

# 잔차 분포의 표본 분위수 CI
residuals = Y - Y_hat
quantile_boot = []
for _ in range(B):
    res_sample = np.random.choice(residuals, size=n, replace=True)
    quantile_boot.append(np.percentile(res_sample, [25, 50, 75]))

7.2 모형 안정성

# 부트스트랩 자료에서 변수 선택 빈도
import statsmodels.api as sm

selected_vars_count = {}
for _ in range(B):
    idx = np.random.choice(n, size=n, replace=True)
    X_b = X[idx]
    Y_b = Y[idx]
    # Forward selection 또는 LASSO
    model = sm.OLS(Y_b, sm.add_constant(X_b)).fit()
    significant = model.pvalues < 0.05
    for j, sig in enumerate(significant[1:]):  # const 제외
        if sig:
            selected_vars_count[j] = selected_vars_count.get(j, 0) + 1

# 변수가 부트스트랩에서 선택된 비율
for j, count in selected_vars_count.items():
    print(f"  Variable {j}: 선택 빈도 = {count/B:.2%}")

선택 빈도가 높으면 변수의 실재성 강함. 빈도 50 % 미만이면 불안정.

8 시계열 부트스트랩

시계열 자료 (자기상관 존재) 에서 단순 부트스트랩 부적절. Block bootstrap 사용.

Block Bootstrap

길이 \(\ell\)연속 블록 단위로 부트스트랩.

def block_bootstrap(data, block_size, n_boot):
    n = len(data)
    n_blocks = (n + block_size - 1) // block_size
    boot_samples = []
    for _ in range(n_boot):
        sample = []
        for _ in range(n_blocks):
            start = np.random.randint(0, n - block_size + 1)
            sample.extend(data[start:start + block_size])
        boot_samples.append(np.array(sample[:n]))
    return boot_samples

블록 크기 권장: \(\ell \approx n^{1/3}\).

A/B 테스트에 시간 효과 가 있는 경우 (요일·시간대 패턴) block bootstrap 권장.

9 클러스터 부트스트랩

클러스터 자료 (학교, 지점, 사용자) 에서 클러스터 단위 추출.

def cluster_bootstrap(data, cluster_id, n_boot):
    clusters = np.unique(cluster_id)
    boot_samples = []
    for _ in range(n_boot):
        boot_clusters = np.random.choice(clusters, size=len(clusters), replace=True)
        boot_data = []
        for c in boot_clusters:
            boot_data.extend(data[cluster_id == c])
        boot_samples.append(np.array(boot_data))
    return boot_samples

A/B 테스트의 반복 노출 사용자 자료에 적합.

10 부트스트랩 사용 결정 트리

부트스트랩 사용 결정:
   ↓
표본 크기 적절 (n ≥ 30)?
   No → 베이즈 또는 모수 가정
   Yes → ↓

자료 의존성?
   시계열 → Block bootstrap
   클러스터 → Cluster bootstrap
   독립 → 일반 bootstrap
   ↓

분석 형태?
   평균/비율 → 단순 부트스트랩
   회귀 → Cases bootstrap (X 무작위) 또는 Residual (X 고정)
   분류 (AUC) → Plug-in + BCa
   분위수 → Percentile bootstrap
   ↓

CI 형태?
   탐색 → Percentile (빠름)
   표준 → BCa (정확)
   정밀 → Bootstrap-t

11 A/B 테스트의 회귀 응용

11.1 시나리오 — 매출 효과 + 공변량 통제

A/B 테스트에서 처리 효과 + 사용자 속성 통제 회귀:

\[ \text{Revenue}_i = \beta_0 + \beta_1 \cdot \text{Variant}_i + \beta_2 \cdot \text{Age}_i + \beta_3 \cdot \text{Tenure}_i + \varepsilon_i \]

\(\beta_1\)처치 효과. Cases bootstrap 으로 robust CI:

import numpy as np
import pandas as pd
import statsmodels.api as sm
from scipy.stats import bootstrap

# 가상 자료
np.random.seed(42)
n = 5000
df = pd.DataFrame({
    'variant': np.random.choice([0, 1], n),
    'age': np.random.uniform(18, 70, n),
    'tenure': np.random.uniform(0, 5, n),
})
df['revenue'] = (50 +
                 5 * df['variant'] +  # 처치 효과
                 0.5 * df['age'] +
                 2 * df['tenure'] +
                 np.random.normal(0, 30, n))

# 표준 OLS
X = sm.add_constant(df[['variant', 'age', 'tenure']])
model = sm.OLS(df['revenue'], X).fit()
print("표준 OLS:")
print(model.summary().tables[1])

# Cases bootstrap
B = 5000
beta_var_boot = []
for _ in range(B):
    idx = np.random.choice(n, size=n, replace=True)
    df_b = df.iloc[idx]
    X_b = sm.add_constant(df_b[['variant', 'age', 'tenure']])
    m_b = sm.OLS(df_b['revenue'], X_b).fit()
    beta_var_boot.append(m_b.params['variant'])

ci_var = np.percentile(beta_var_boot, [2.5, 97.5])
print(f"\nBootstrap β_variant 95 % CI: ({ci_var[0]:.2f}, {ci_var[1]:.2f})")

표준 OLS 와 부트스트랩 CI 가 일관 되면 가정이 충족된 증거. 차이 크면 robustness 확인 필요.

11.2 Heteroscedastic 자료

이분산성 (분산이 \(X\) 에 따라 변함) 에서 표준 OLS SE 부정확. Cases bootstrap 자동 보정.

# Heteroscedastic 시뮬레이션
df['revenue_hetero'] = (50 + 5 * df['variant'] +
                        np.random.normal(0, 10 + 30 * df['variant'], n))  # variant 1 의 분산 큼

# OLS — SE 부정확
model_hetero = sm.OLS(df['revenue_hetero'], X).fit()
print(f"\n이분산 자료 OLS β_variant SE: {model_hetero.bse['variant']:.3f}")

# Bootstrap — SE 정확
beta_hetero_boot = []
for _ in range(B):
    idx = np.random.choice(n, size=n, replace=True)
    df_b = df.iloc[idx]
    X_b = sm.add_constant(df_b[['variant', 'age', 'tenure']])
    m_b = sm.OLS(df_b['revenue_hetero'], X_b).fit()
    beta_hetero_boot.append(m_b.params['variant'])

print(f"Bootstrap β_variant SE: {np.std(beta_hetero_boot, ddof=1):.3f}")

부트스트랩 SE 가 진정한 변동성 반영.

12 부트스트랩의 함정 정리

  1. 작은 표본\(n < 30\) 에서 부정확
  2. 의존성 무시 — 시계열·클러스터에서 단순 부트스트랩 부적절
  3. 편향 통계량 — 복잡한 통계량은 BCa 또는 jackknife 보정
  4. 계산 비용 — 큰 자료에서 분량 폭증, 병렬화 필수
  5. Plug-in 가정 혼동 — 부트스트랩이 모든 가정을 약화시키는 것은 아님

13 Buisson Ch.7 마무리

핵심 메시지:

“부트스트랩은 모든 자료 에서 robust 한 추론 을 가능케 한다. 적용해서 잃을 게 없다.”

13.1 권장 활용

  1. robustness check — 전통 검정 + 부트스트랩 둘 다 보고
  2. 비표준 통계량 — 중앙값, 분위수, AUC, Gini 의 CI
  3. 작은 표본 segment — 사용자 segment 분석
  4. 복잡 모형 — 비선형, 혼합 모형의 모수 CI

이 4 가지가 현대 A/B 테스트 분석의 표준 도구.

14 후속 — Computer-intensive Methods (Woodward)

다음 시리즈 (A-WOO14-) 는 Woodward 의 시각에서 부트스트랩 + 순열 + 결측 처리* 를 다룬다. 더 역학적 응용 + 결측 처리 (multiple imputation) 포함.

15 관련 주제

선행 지식

후속 주제 (Phase A)

  • A-WOO14-* (Computer-intensive 자세히)

다른 카테고리 연결

Subscribe

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