Σ
SDCalc
고급심화·15 min

표준편차를 위한 부트스트랩 방법

부트스트랩 재표본추출을 활용한 표준편차 추정법을 마스터하세요. 백분위수법, BCa법, 모수적 부트스트랩 방법을 Python 구현과 계산 예시로 설명합니다.

부트스트랩: 컴퓨터 시대의 통계 혁명

부트스트랩 재표본추출은 관측 데이터에서 반복적으로 재표본을 추출하여 어떤 통계량의 표본분포를 추정하는 강력한 통계 기법입니다. 1979년 브래들리 에프론이 도입한 이 방법은 수학 공식이나 분포 가정에 의존하지 않고도 복잡한 통계량의 분석을 가능하게 하여 통계적 추론에 혁명을 일으켰습니다.

부트스트랩의 핵심 통찰은 우아할 만큼 단순합니다: 여러분의 표본이 모집단에 대한 최선의 추정입니다. 표본에서 (복원) 재표본을 추출하면, 모집단에서 반복 표본추출했을 때 일어날 일을 시뮬레이션합니다. 이 접근법은 전통적 신뢰구간 공식이 정규성을 가정하는 표준편차에 특히 유용합니다—실무에서 이 가정이 종종 충족되지 않기 때문입니다.

부트스트랩은 어떤 통계량(중앙값, 상관계수, 회귀계수, 신경망 가중치)에도 작동하고 데이터의 기저 분포에 대한 가정을 하지 않으므로 현대 데이터 과학에서 필수적인 방법이 되었습니다.

표준편차에 부트스트랩을 쓰는 이유

표준편차에 대한 전통적 신뢰구간은 데이터가 정규분포에서 왔다고 가정합니다. 이 가정이 틀리면(흔한 일입니다) 이 구간은 심하게 부정확해질 수 있습니다. 부트스트랩은 분포에 의존하지 않는 대안을 제공합니다.

전통적 방법이 실패할 때

표준편차에 대한 카이제곱 기반 신뢰구간은 정규성을 가정합니다. 비대칭 데이터(소득, 반응시간, 생존 데이터)에서는 기대하는 5%가 아니라 20-30%의 확률로 실제 모수를 놓치는 구간을 만들 수 있습니다.

표준편차에 부트스트랩을 사용하는 핵심 장점:

  • 분포 가정 불필요: 정규, 비대칭, 두꺼운 꼬리 데이터 모두에서 동일하게 작동
  • 소표본 성능: n < 30에서 모수적 방법보다 종종 더 정확
  • 복잡한 통계량 처리: 절사 SD, MAD, 또는 사용자 정의 변동성 측도에도 같은 접근법 적용 가능
  • 시각적 통찰: 부트스트랩 분포가 최종 숫자뿐 아니라 무슨 일이 일어나고 있는지를 보여줌

부트스트랩 절차

부트스트랩 알고리즘은 놀라울 만큼 간단합니다. n개 관측으로 구성된 원래 표본에서:

1

부트스트랩 표본 추출

원래 데이터에서 복원추출로 n개의 관측을 무작위 선택합니다. 일부 값은 여러 번 나타나고, 일부는 전혀 나타나지 않습니다.
2

통계량 계산

이 부트스트랩 표본의 표준편차를 계산합니다. 이것이 부트스트랩 복제 하나입니다.
3

여러 번 반복

1-2단계를 수천 번 반복합니다(일반적으로 B = 10,000). 각 반복에서 부트스트랩 SD 하나를 얻습니다.
4

분포 분석

B개의 부트스트랩 SD 모음이 표본분포를 근사합니다. 신뢰구간과 가설검정에 활용합니다.

왜 복원추출인가?

복원추출이 핵심입니다. 구성이 다른 표본을 만들어서, 모집단에서 여러 번 표본을 추출했을 때 볼 수 있는 변동성을 모방합니다. 비복원추출로는 모든 표본이 원래 표본과 동일해져 버립니다.

부트스트랩 표본은 몇 개 필요할까? B = 1,000이면 대략적인 추정과 가설검정에 충분합니다. 신뢰구간에는 B = 10,000이 안정적인 백분위수를 제공합니다. 출판 수준의 BCa 구간에는 B = 15,000 이상을 권장합니다.

부트스트랩 신뢰구간 방법

부트스트랩 표본에서 신뢰구간을 구성하는 여러 방법이 있으며, 각각 장단점이 있습니다:

1. 백분위수법 (가장 간단)

가장 직관적인 접근법: 부트스트랩 분포의 백분위수를 직접 취합니다.

백분위수 CI

95% CI = [θ*₂.₅, θ*₉₇.₅]

10,000개 부트스트랩 표본의 경우 250번째와 9,750번째 정렬된 값입니다. 간단하지만 부트스트랩 분포가 비대칭이면 편향될 수 있습니다.

2. 기본(피벗) 부트스트랩

표본 통계량과 부트스트랩 통계량 사이의 관계를 활용합니다:

기본 부트스트랩 CI

95% CI = [2θ̂ - θ*₉₇.₅, 2θ̂ - θ*₂.₅]

θ̂는 원래 표본 SD입니다. 백분위수 구간을 표본 추정치 주위로 “반사”합니다.

3. BCa (편향 보정 및 가속)

정확도의 최고봉입니다. BCa는 부트스트랩 분포의 편향과 가속(표준오차가 모수 값에 따라 어떻게 변하는지)을 모두 보정합니다. 계산이 더 복잡하지만 2차 정확도의 구간을 제공합니다.

방법장점단점
백분위수법간단하고 직관적비대칭 데이터에서 편향 가능
기본법대칭 구간음수값을 생성할 수 있음
BCa가장 정확, 변환 존중계산 비용이 높음

계산 예시: 비정규 데이터

반응시간(ms) 15개 측정: 245, 312, 287, 456, 234, 298, 267, 523, 289, 301, 278, 645, 256, 289, 312. 이 데이터는 오른쪽으로 치우쳐 있습니다(일부 매우 느린 반응).

1

표본 SD 계산

원래 표본: n=15, SD = 109.8 ms
2

부트스트랩 표본 생성

복원추출로 크기 15의 표본 10,000개를 추출합니다. 각 표본은 다른 구성을 가집니다.
3

부트스트랩 SD 계산

각 부트스트랩 표본의 SD를 계산하여 ~60에서 ~180 범위의 10,000개 값을 얻습니다
4

백분위수 구하기

2.5번째 백분위수: 72.3 ms, 97.5번째 백분위수: 156.8 ms
5

95% CI 구성

95% CI: [72.3, 156.8] ms. 카이제곱 CI: [79.4, 175.2]와 비교 (정규성 가정).

부트스트랩 CI는 비대칭(높은 쪽이 더 넓음)이며, 데이터의 오른쪽 치우침을 반영합니다. 카이제곱 CI는 이 비대칭을 포착하지 못합니다.

Python 구현

여러 CI 방법을 포함한 완전한 부트스트랩 구현:

python
import numpy as np
from scipy import stats

def bootstrap_sd_ci(data, n_bootstrap=10000, ci=0.95, method='percentile'):
    """
    Bootstrap confidence interval for standard deviation.

    Parameters:
    -----------
    data : array-like - Original sample
    n_bootstrap : int - Number of bootstrap samples
    ci : float - Confidence level (e.g., 0.95)
    method : str - 'percentile', 'basic', or 'bca'

    Returns:
    --------
    tuple : (lower_bound, upper_bound, bootstrap_sds)
    """
    data = np.array(data)
    n = len(data)
    original_sd = np.std(data, ddof=1)

    # Generate bootstrap samples and calculate SDs
    bootstrap_sds = np.array([
        np.std(np.random.choice(data, size=n, replace=True), ddof=1)
        for _ in range(n_bootstrap)
    ])

    alpha = 1 - ci

    if method == 'percentile':
        lower = np.percentile(bootstrap_sds, 100 * alpha/2)
        upper = np.percentile(bootstrap_sds, 100 * (1 - alpha/2))

    elif method == 'basic':
        lower = 2*original_sd - np.percentile(bootstrap_sds, 100*(1-alpha/2))
        upper = 2*original_sd - np.percentile(bootstrap_sds, 100*alpha/2)

    elif method == 'bca':
        # Bias correction
        prop_less = np.mean(bootstrap_sds < original_sd)
        z0 = stats.norm.ppf(prop_less)

        # Acceleration (jackknife estimate)
        jackknife_sds = np.array([
            np.std(np.delete(data, i), ddof=1) for i in range(n)
        ])
        jack_mean = jackknife_sds.mean()
        a = np.sum((jack_mean - jackknife_sds)**3) / \
            (6 * np.sum((jack_mean - jackknife_sds)**2)**1.5)

        # Adjusted percentiles
        z_alpha = stats.norm.ppf([alpha/2, 1-alpha/2])
        adj_percentiles = stats.norm.cdf(
            z0 + (z0 + z_alpha) / (1 - a*(z0 + z_alpha))
        ) * 100
        lower = np.percentile(bootstrap_sds, adj_percentiles[0])
        upper = np.percentile(bootstrap_sds, adj_percentiles[1])

    return lower, upper, bootstrap_sds

# Example usage
response_times = [245, 312, 287, 456, 234, 298, 267, 523, 289, 301, 278, 645, 256, 289, 312]

for method in ['percentile', 'basic', 'bca']:
    lower, upper, _ = bootstrap_sd_ci(response_times, method=method)
    print(f"{method.upper():12s} 95% CI: [{lower:.1f}, {upper:.1f}]")