콘텐츠로 이동

붓스트랩 표준오차

개요

앞 쪽에서 표준오차를 구하는 길은 하나뿐이었다. \(\sigma/\sqrt n\) 같은 공식을 아는 것이다. 그런데 중앙값의 표준오차는 어떻게 구하는가. 두 추정값의 비는, 상관계수는, 복잡한 모형에서 파생된 양은 어떻게 하는가. 간단한 공식이 아예 없거나, 있더라도 모집단 밀도나 4차적률처럼 우리가 모르는 값을 알아야 쓸 수 있는 경우가 대부분이다.

붓스트랩은 그럴 때 공식을 포기하고 계산으로 밀어붙이는 방법이다. 관측된 자료에서 복원추출로 재표본을 여러 번 뽑아 매번 통계량을 계산하고, 그렇게 얻은 복제값들의 표준편차를 표준오차의 추정값으로 삼는다.

처음 들으면 어딘가 반칙 같다. 새 자료를 얻은 것도 아닌데 자기 표본을 다시 뒤적이는 것으로 무엇이 나온다는 말인가. 이 쪽의 절반은 그 의심에 답하는 데 쓰고, 나머지 절반은 답이 맞는지 확인하는 데 쓴다. 표본평균에는 이미 믿을 만한 공식이 있으니 붓스트랩이 그 값을 되찾아 오는지 보면 된다.

표본을 모집단인 셈 친다

관측된 표본 \(x_1, x_2, \ldots, x_n\)에서 \(\text{SE}(\hat{\theta})\)를 추정하는 절차는 네 줄이다. 원래 자료에서 복원추출로 크기 \(n\)짜리 붓스트랩 표본 \(x_1^*, x_2^*, \ldots, x_n^*\)을 뽑고, 거기서 관심 통계량 \(\hat{\theta}^* = T(x_1^*, \ldots, x_n^*)\)을 계산한다. 이 일을 \(B\)번 되풀이해 \(\hat{\theta}_1^*, \ldots, \hat{\theta}_B^*\)을 모은 뒤, 마지막으로 그 값들의 표본표준편차를 취한다.

\[ \widehat{\text{SE}}_{\text{boot}} = \sqrt{\frac{1}{B-1}\sum_{b=1}^B \left(\hat{\theta}_b^* - \bar{\hat{\theta}}^*\right)^2} \]

여기서 \(\bar{\hat{\theta}}^* = \frac{1}{B}\sum_{b=1}^B \hat{\theta}_b^*\)이다.

이제 처음의 의심에 답할 차례다. 우리가 정말 하고 싶은 일은 모집단에서 표본을 몇 번이고 다시 뽑아 통계량이 얼마나 튀는지 보는 것이다. 모집단을 모르니 그럴 수 없다. 그러나 손에 쥔 표본은 그 모집단에서 무작위로 뽑혀 온 것이므로, 모집단의 모양을 성글게나마 베껴 온 그림이다. 모집단이 없으면 그 그림을 모집단인 셈 치고 뽑는다는 것이 붓스트랩의 전부다. 형식을 갖춰 말하면, 관측된 각 값에 질량 \(1/n\)을 주는 경험분포 \(\hat F_n\)을 참 분포 \(F\)의 대역으로 삼는 것이며, 자료에서 복원추출하는 일이 곧 \(\hat F_n\)에서 표본을 뽑는 일이다.

복원이라는 단서가 붙는 이유도 여기서 나온다. 비복원으로 \(n\)개를 모두 뽑으면 언제나 원래 표본이 그대로 나와 잴 변동 자체가 생기지 않는다. 복원으로 뽑아야 어떤 값은 두 번 들어오고 어떤 값은 빠지며, 그 우연이 "표본을 다시 뽑았더라면" 생겼을 흔들림을 흉내 낸다.

그럴듯한 이야기라고 해서 맞는 것은 아니니 확인이 필요하다. 표본평균에는 고전적인 공식

\[ \text{SE}(\bar{x}) = \frac{s}{\sqrt{n}} \]

이 이미 있고 여기서 \(s\)는 표본표준편차다. 붓스트랩이 이 값을 되찾아 오는지가 첫 시험이다. 덤으로 같은 계산을 조금 다르게 적은 방식도 함께 견준다. 복제값들의 흩어짐을 그들 자신의 평균이 아니라 원래 표본평균 \(\bar x\)에서 재는 것이다.

\[ \widehat{\text{SE}} = \sqrt{\frac{1}{B}\sum_{b=1}^B \left(\bar{x}_b^* - \bar{x}\right)^2} \]

\(B\)가 크면 두 방식이 거의 같은 답을 준다.

모의실험

다음 코드는 31개의 가격 관측값 표본에 고전적 방법과 붓스트랩 방법을 모두 적용한다.

보기 1. 붓스트랩으로 표준오차 구하기. 가격 관측값 \(31\)개에 고전적 공식 \(s/\sqrt n\)과 붓스트랩 두 변형을 모두 적용해 세 숫자를 나란히 놓는다.

(1) 붓스트랩 표준오차가 \(B \to \infty\)에서 정확히 무엇으로 수렴하는지 유도하고, 고전적 \(s/\sqrt n\)과의 비를 구하시오. 두 번째 변형(흩어짐을 원래 표본평균에서 재는 것)의 극한도 함께 구하시오.

(2) 실행해 확인하고, 세 숫자 사이의 차이를 체계적인 몫과 우연한 몫으로 가르시오.

풀이

(1) 붓스트랩의 극한은 정확히 계산된다. 붓스트랩 표본 \(x_1^*, \ldots, x_n^*\)은 경험분포 \(\hat F_n\)에서 독립으로 뽑힌다. \(\hat F_n\)은 관측값 하나하나에 질량 \(1/n\)을 주는 분포이므로 그 평균과 분산이

\[ E_*[X^*] = \bar x, \qquad \operatorname{Var}_*(X^*) = \hat\sigma^2 \equiv \frac1n \sum_{i=1}^n (x_i - \bar x)^2 \]

다. \(n-1\)이 아니라 \(n\)으로 나눈 것이라는 데 주의할 것. 경험분포의 분산은 추정량이 아니라 그 분포의 성질이므로 불편보정이 끼어들 자리가 없다. 독립인 \(n\)개의 평균이므로

\[ \operatorname{Var}_*(\bar X^*) = \frac{\hat\sigma^2}{n} \;\Longrightarrow\; \widehat{\operatorname{SE}}_{\text{boot}} \xrightarrow[B \to \infty]{} \frac{\hat\sigma}{\sqrt n} \]

이다. 고전적 공식은 \(s/\sqrt n\)이고 \(\hat\sigma = s\sqrt{(n-1)/n}\)이므로 비가

\[ \frac{\hat\sigma/\sqrt n}{s/\sqrt n} = \sqrt{\frac{n-1}{n}} = \sqrt{\frac{30}{31}} = 0.98374 \]

로 붓스트랩 쪽이 체계적으로 \(1.6\%\) 작다. 이것은 \(B\)를 아무리 키워도 남는다.

두 번째 변형도 같은 값을 겨냥한다. \(E_*[\bar X^*] = \bar x\)이므로

\[ E_*\!\left[(\bar X^* - \bar x)^2\right] = \operatorname{Var}_*(\bar X^*) = \frac{\hat\sigma^2}{n} \]

이다. \(\bar x\)를 중심으로 재든 복제값들의 평균을 중심으로 재든 극한이 똑같다. 두 변형 사이의 차이는 전부 몬테카를로 요동이다.

수를 넣어 보자. \(s = 1.032017\), \(\hat\sigma = 1.015235\)이므로

\[ \frac{s}{\sqrt{31}} = 0.185356, \qquad \frac{\hat\sigma}{\sqrt{31}} = 0.182342 \]

이고, \(B = 10{,}000\)에서 표준오차 추정값의 몬테카를로 요동은 \(0.182342/\sqrt{2B} = 0.001289\)다. 그러므로 셋째 자리까지만 의미가 있다.

(2) 실행.

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# 자료: 가격 관측값 31개.
data = np.array([
    245.02, 244.88, 244.76, 244.65, 244.53, 244.42, 244.30,
    244.18, 244.08, 243.97, 243.85, 243.74, 243.63, 243.52,
    243.40, 243.28, 243.17, 243.06, 242.95, 242.83, 242.72,
    242.61, 242.49, 242.38, 242.27, 242.15, 242.04, 241.93,
    241.81, 241.70, 241.59,
])

n = len(data)
n_boot = 10_000     # 붓스트랩 재표본 개수

# 고전적 표준오차. s/sqrt(n) 이라는 **공식**에 의존한다.
se_classical = data.std(ddof=1) / np.sqrt(n)

# 붓스트랩 표준오차. 공식 대신 **재표본추출**로 구한다.
# 핵심은 replace=True 다. 원자료에서 크기 n짜리를 복원추출하므로
# 같은 값이 여러 번 뽑히거나 아예 안 뽑히기도 한다.
# 그 우연이 만들어 내는 표본평균의 흩어짐이 곧 표준오차의 추정이다.
#
# 발상은 이렇다. 우리는 모집단에서 표본을 다시 뽑을 수 없다.
# 그래서 **표본을 모집단인 셈 치고** 거기서 다시 뽑는다.
boot_means = np.array([
    np.random.choice(data, size=n, replace=True).mean()
    for _ in range(n_boot)
])
se_bootstrap = boot_means.std(ddof=1)

# 같은 양을 조금 다르게 적은 것. 흩어짐을 복제값들 자신의 평균이 아니라
# 원래 표본평균에서 잰다. B 가 크면 위와 거의 같은 답을 준다.
sq_errors = np.array([
    (np.random.choice(data, size=n, replace=True).mean() - data.mean()) ** 2
    for _ in range(n_boot)
])
se_squared_error = np.sqrt(sq_errors.mean())

print(f"Classical SE:       {se_classical:.4f}")
print(f"Bootstrap SE:       {se_bootstrap:.4f}")
print(f"Squared-error SE:   {se_squared_error:.4f}")

출력:

Classical SE:       0.1854
Bootstrap SE:       0.1834
Squared-error SE:   0.1810

셋이 맞는다. 고전 \(0.1854\), 붓스트랩 \(0.1834\), 제곱오차판 \(0.1810\)이다. 차이를 두 몫으로 가른다.

체계적인 몫. 고전과 붓스트랩의 겨냥값 차이가 \(0.185356 - 0.182342 = 0.003014\)다. 비로는 \(0.98374\)이고, 이것은 (1)에서 유도한 \(\sqrt{30/31}\) 그대로다. 우연이 아니라 \(n\)으로 나눈 것과 \(n-1\)으로 나눈 것의 차이이며, \(n\)이 커지면 \(\sqrt{(n-1)/n} \to 1\)로 사라진다. \(n = 31\)에서 \(1.6\%\)다.

우연한 몫. 두 붓스트랩 변형은 같은 값 \(0.182342\)를 겨냥한다. 모의값이 \(0.183396\)과 \(0.180975\)로, 겨냥값에서 각각 \(+0.001054\)와 \(-0.001367\) 떨어져 있다. 몬테카를로 요동 \(0.001289\) 단위로 \(0.82\)배와 \(1.06\)배다. 서로 다른 난수열을 썼으므로 둘의 차 \(0.00242\)가 요동의 \(\sqrt2\)배인 \(0.00182\)의 \(1.3\)배가 되는 것도 맞는다. 두 변형의 차이는 전부 요동이다.

되풀이를 늘려 확인할 수 있다. \(B\)를 백만 번으로 올리면 두 변형이 모두 \(0.1823\) 근처로 모여 극한값 \(0.182342\)에 붙는다. 반면 고전 \(0.185356\)과의 \(1.6\%\) 차이는 그대로 남는다. \(B\)를 키워 없어지는 것과 없어지지 않는 것을 가려 읽는 것이 이 보기의 요점이다.

시험은 통과했다. 표본평균처럼 공식을 아는 통계량에서 붓스트랩이 그 공식과 (알려진 보정만큼 다른) 같은 답을 내놓았으니, 공식을 모르는 통계량에서 내놓는 답도 믿어 볼 근거가 생겼다.

세 값이 소수점 둘째 자리까지 같다. 공식을 쓴 쪽과 공식 없이 재표집만으로 얻은 쪽이 같은 답에 이르렀다는 뜻이다. 시험은 통과했다.

남은 차이는 두 몫이다. 하나는 재표집 1만 번이 만들어 낸 우연이고, 다른 하나는 붓스트랩 쪽이 체계적으로 \(\sqrt{(n-1)/n} = 0.984\)배 작게 나오는 데서 온다. 뒤의 것은 우연이 아니라 극한값 자체의 성질이며, 연습문제 5가 그 까닭을 밝힌다.

시각화

붓스트랩이 만들어 낸 것이 무엇인지는 그림으로 보는 편이 빠르다. 왼쪽은 재표본 1만 개의 평균이 이루는 분포이고, 이 히스토그램의 폭이 곧 붓스트랩 표준오차다. 오른쪽은 쓰는 자료를 늘려 갈 때 고전적 표준오차가 어떻게 움직이는지를 그린 것이다.

보기 2. 붓스트랩 분포 시각화. 왼쪽은 재표본 1만 개의 평균이 이루는 분포, 오른쪽은 쓰는 자료를 늘려 갈 때 고전적 표준오차가 어떻게 움직이는지를 그린다(보기 1의 변수를 그대로 이어 쓴다).

(1) 왼쪽 히스토그램의 중심·폭·모양이 각각 무엇이 되어야 하는지 적고, 오른쪽 회색 점선이 \(k = 5,\ 15,\ 31\)에서 지나는 값을 구하시오.

(2) 그려서 확인하시오. 자료를 섞지 않고 앞에서부터 잘랐다면 무엇이 어떻게 달라지는지 수로 보이시오.

풀이

(1) 왼쪽 패널. 재표본평균 \(\bar X^*\)의 분포이므로 (1)에서 구한 두 적률이 그대로 답이다.

\[ \text{중심} = \bar x = 243.28742, \qquad \text{폭} = \frac{\hat\sigma}{\sqrt n} = 0.182342 \]

모양도 계산된다. 경험분포의 왜도가 \(0.00762\), 첨도가 \(\hat\beta_2 = 1.80711\)이므로(\(31\)개가 거의 등간격이라 균등분포의 \(1.8\)에 가깝다) 독립인 \(n\)개의 평균에 대해

\[ \text{왜도}(\bar X^*) = \frac{0.00762}{\sqrt{31}} = 0.00137, \qquad \text{초과첨도}(\bar X^*) = \frac{1.80711 - 3}{31} = -0.0385 \]

다. 거의 정규다. 재표집이 흉내 내려던 것이 \(\bar X\)의 표집분포이고, 그 분포가 정규여야 하므로 예상과 맞는다.

오른쪽 패널. 회색 점선은 전체 표본의 \(s = 1.032017\)을 고정해 둔 \(s/\sqrt k\)다.

\[ k = 5: \ 0.46153, \qquad k = 15: \ 0.26647, \qquad k = 31: \ 0.18536 \]

파란 곡선은 앞 \(k\)개만으로 \(s\)를 다시 재므로 이 점선 주위에서 흔들린다. \(k\)가 작을 때 흔들림이 크고 \(k = 31\)에서는 정의상 두 값이 같아진다.

(2) 그려서 확인.

# 보기 1 의 data, n, boot_means, se_bootstrap, plt, np 를 그대로 이어 쓴다.
from scipy import stats

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))

# 왼쪽: 붓스트랩 분포.
# 이 히스토그램의 **표준편차**가 곧 붓스트랩 표준오차다.
# 표집분포를 모의실험으로 만든 것과 모양이 같지만,
# 모집단이 아니라 표본에서 뽑았다는 점이 다르다.
ax = axes[0]
ax.hist(boot_means, bins=40, edgecolor="white", alpha=0.7)
ax.axvline(data.mean(), color="red", linestyle="--",
           label=f"Sample mean = {data.mean():.2f}")
ax.set_xlabel("Bootstrap sample mean")
ax.set_ylabel("Frequency")
ax.set_title(f"Bootstrap Distribution (SE = {se_bootstrap:.3f})")
ax.legend()

# 오른쪽: 표본 크기에 따른 SE의 변화.
# 주의. 이 자료는 시간순으로 기록돼 있어 값이 계속 내려간다.
# data[:k] 로 앞에서부터 자르면 k가 커질수록 s 자체가 커져서
# SE가 오히려 **늘어난다**. 한 번 무작위로 섞은 뒤 앞에서 k개를 쓴다.
ax = axes[1]
shuffled = np.random.default_rng(0).permutation(data)
sizes = np.arange(5, n + 1)
se_vals = [shuffled[:k].std(ddof=1) / np.sqrt(k) for k in sizes]
ax.plot(sizes, se_vals, marker="o", markersize=4, label="observed")
ax.plot(sizes, data.std(ddof=1) / np.sqrt(sizes), "--", color="gray",
        label="$s/\\sqrt{n}$")
ax.set_xlabel("Sample size n")
ax.set_ylabel("SE (classical)")
ax.set_title("Standard Error Shrinks Like 1/sqrt(n)")
ax.legend()

plt.tight_layout()
plt.show()
# 왼쪽 패널을 수로 읽는다. 중심은 x-bar, 폭은 붓스트랩 표준오차여야 한다.
sd_plug = data.std(ddof=0) / np.sqrt(n)
print(f"왼쪽  중심  원표본평균 {data.mean():.6f}   붓스트랩 평균 {boot_means.mean():.6f}   (MC오차 {sd_plug / 100:.6f})")
print(f"      폭    극한 {sd_plug:.6f}   모의 {se_bootstrap:.6f}   (MC오차 {sd_plug / np.sqrt(20000):.6f})")
beta2_hat = stats.kurtosis(data) + 3
print(f"      모양  왜도 {stats.skew(boot_means):+.4f} (이론 {stats.skew(data) / np.sqrt(n):+.4f}),  "
      f"초과첨도 {stats.kurtosis(boot_means):+.4f} (이론 {(beta2_hat - 3) / n:+.4f})")

# 오른쪽 패널. 섞은 것과 안 섞은 것을 나란히 본다.
print("\n  k   섞은 뒤 SE   s/sqrt(k) (전체 s)   섞지 않고 앞에서부터 SE")
for k in (5, 10, 15, 20, 31):
    print(f"{k:>3} {shuffled[:k].std(ddof=1) / np.sqrt(k):>11.4f} "
          f"{data.std(ddof=1) / np.sqrt(k):>18.4f} {data[:k].std(ddof=1) / np.sqrt(k):>22.4f}")

출력:

왼쪽  중심  원표본평균 243.287419   붓스트랩 평균 243.287386   (MC오차 0.001823)
      폭    극한 0.182342   모의 0.183396   (MC오차 0.001289)
      모양  왜도 +0.0289 (이론 +0.0014),  초과첨도 +0.0110 (이론 -0.0385)

  k   섞은 뒤 SE   s/sqrt(k) (전체 s)   섞지 않고 앞에서부터 SE
  5      0.4665             0.4615                 0.0856
 10      0.3393             0.3264                 0.1109
 15      0.2875             0.2665                 0.1318
 20      0.2356             0.2308                 0.1506
 31      0.1854             0.1854                 0.1854

Standard Error Shrinks Like 1/sqrt(n)

왼쪽 패널의 세 줄이 모두 맞는다. 붓스트랩 평균 \(243.287386\)이 원표본평균 \(243.287419\)에서 \(3.3 \times 10^{-5}\) 떨어져 있어 몬테카를로 오차 \(0.001823\)의 \(0.02\)배다. 재표집이 중심을 옮기지 않는다는 \(E_*[\bar X^*] = \bar x\)가 그대로 확인된 것이고, 그래서 붓스트랩은 편의를 고쳐 주는 도구가 아니라 퍼짐을 재는 도구다. 폭은 모의 \(0.183396\)이 극한 \(0.182342\)에서 요동 \(0.001289\)의 \(0.82\)배다.

모양도 예상대로다. 왜도 \(+0.0289\)가 이론 \(+0.0014\)에서 왜도 추정값의 요동 \(\sqrt{6/B} = 0.0245\)의 \(1.2\)배, 초과첨도 \(+0.0110\)이 이론 \(-0.0385\)에서 \(\sqrt{24/B} = 0.0490\)의 \(1.0\)배 떨어져 있다. 이론값 자체가 \(0\)에 너무 가까워 1만 번으로는 \(0\)과 구별되지 않는다. 히스토그램이 종 모양으로 보이는 것이 그 뜻이다.

오른쪽 패널. 섞은 뒤의 관측값이 \(k = 5,\ 15,\ 31\)에서 \(0.4665\), \(0.2875\), \(0.1854\)이고 회색 점선이 \(0.4615\), \(0.2665\), \(0.1854\)다. 작은 \(k\)에서 위로 조금 벗어나는 것은 그 \(k\)개로 다시 잰 \(s\)가 전체 \(s\)보다 컸다는 뜻이며, \(k = 31\)에서는 두 값이 정의상 같다. \(1/\sqrt k\) 곡선을 따라 내려간다는 것이 패널의 전부다.

섞지 않았다면. 마지막 열이 그 경우다. \(k = 5\)에서 \(0.0856\), \(k = 31\)에서 \(0.1854\)로 표준오차가 줄기는커녕 두 배 넘게 늘어난다. 이 \(31\)개는 시간순으로 \(245.02\)에서 \(241.59\)까지 계속 내려가는 값이고 인접한 차이가 \(0.11\)–\(0.14\)로 거의 일정하다. 앞에서부터 \(k\)개를 자르면 잘라낸 구간의 폭이 \(k\)에 비례해 넓어지므로 \(s\)가 \(k\)에 거의 비례해 커지고, \(\sqrt k\)로 나눈 뒤에도 \(\sqrt k\)만큼 커진 채 남는다. 실제로 \(0.0856\sqrt{31/5} = 0.213\)으로 \(0.1854\)와 같은 자리다.

그러므로 추세가 있는 자료에서 앞에서부터 자르는 것은 표본크기를 키우는 일이 아니라 모집단을 바꾸는 일이다. \(1/\sqrt n\) 법칙은 같은 모집단에서 독립으로 뽑는다는 전제 위의 이야기이고, 그 전제가 깨지면 법칙이 부호까지 뒤집혀 보인다. 코드가 한 번 섞고 나서 쓰는 이유가 이것이다.

해석

세 가지 표준오차가 모두 비슷한 값으로 모였다는 것이 이 모의실험의 첫 결론이다. 표본평균처럼 공식을 아는 통계량에서 붓스트랩이 그 공식과 같은 답을 내놓는다면, 공식을 모르는 통계량에서 내놓는 답도 믿어 볼 근거가 된다. 왼쪽 패널의 붓스트랩 분포가 근사적으로 정규이고 원래 표본평균을 중심으로 한다는 점도 예상과 맞는다. 재표집이 흉내 내려던 것이 곧 \(\bar X\)의 표집분포이고, 그 분포는 정규여야 하기 때문이다.

오른쪽 패널은 자료를 더 많이 쓸수록 표준오차가 익숙한 \(1/\sqrt{n}\) 곡선을 따라 줄어드는 모습이다. 5개만 쓰면 0.47, 15개면 0.29, 전체 31개를 쓰면 0.19로 내려가고, 회색 점선으로 그린 \(s/\sqrt{n}\) 곡선을 거의 그대로 따라간다.

여기에는 조심할 곳이 하나 있다. 이 31개는 시간순으로 기록된 값이라 245.02에서 241.59까지 계속 내려간다. 앞에서부터 \(k\)개를 잘라 쓰면 \(k\)가 커질수록 잘라낸 구간의 폭이 넓어져 표본표준편차 \(s\)가 함께 커지고, \(\sqrt{k}\)로 나눈 뒤에도 표준오차가 줄기는커녕 0.09에서 0.19로 늘어난다. 추세가 있는 자료에서 앞에서부터 자르는 것은 표본크기를 키우는 일이 아니라 모집단을 바꾸는 일이다. 그래서 위 코드는 한 번 무작위로 섞은 뒤 앞에서부터 쓴다.

그렇다면 굳이 붓스트랩을 쓸 이유는 무엇인가. 이 쪽의 예에는 없다. 표본평균에는 \(s/\sqrt n\)이 있으니 붓스트랩은 검산일 뿐이다. 붓스트랩이 정말 필요해지는 자리는 셋이다. 첫째, 통계량의 표준오차에 간단한 공식이 없을 때다. 중앙값, 상관계수, 백분위수가 그렇다. 둘째, 이론적 공식이 있기는 하되 우리가 추정하기 어려운 양에 기대고 있을 때다. \(\text{SE}(S^2)\)가 모집단의 4차적률을 요구하는 것이 그런 경우다. 셋째, 자료의 분포가 복잡하거나 표본이 작아 분포 가정 자체를 피하고 싶을 때다.

연습문제 4에서 위 코드의 np.mean을 np.median으로 한 글자 바꾸는 것만으로 중앙값의 표준오차가 나오는 것을 보게 된다. 통계량이 무엇이든 절차가 그대로라는 점, 그 손쉬움이 붓스트랩의 진짜 값어치다.

연습문제

연습문제 1. 크기 \(n\)인 원래 자료에서 붓스트랩 표본을 비복원이 아니라 복원으로 뽑는 이유를 설명하라.

풀이

\(n\)개의 자료점에서 비복원으로 \(n\)개를 모두 뽑는다면 언제나 똑같은 자료 집합이 나오고, 어떤 통계량이든 붓스트랩 복제값이 원래 값과 동일해진다. 잴 변동성 자체가 없어진다.

복원추출은 변동성을 만들어 낸다. 어떤 관측값은 한 붓스트랩 표본에 여러 번 나타나고 어떤 것은 아예 빠진다. 평균적으로 원래 관측값의 약 \(1 - (1 - 1/n)^n \approx 1 - e^{-1} \approx 63.2\%\)가 각 붓스트랩 표본에 나타난다. 이 변동성이 참 모집단에서 새 표본을 뽑을 때 생기는 변동성을 모방한다.

형식적으로 붓스트랩은 (관측된 각 값에 질량 \(1/n\)을 주는) 경험분포 \(\hat{F}_n\)을 참 모집단 분포 \(F\)의 대역으로 삼는다. 자료에서 복원추출하는 것은 \(\hat{F}_n\)에서 표본을 뽑는 것과 같다. \(\square\)

연습문제 2. 위의 가격 자료(\(n = 31\), \(s \approx 1.03\))에 대해 고전적 표준오차를 손으로 계산하고 모의실험 출력과 일치하는지 확인하라.

풀이

표본표준편차는:

\[ s = \sqrt{\frac{1}{30}\sum_{i=1}^{31}(x_i - \bar{x})^2} \]

자료는 241.59에서 245.02까지 거의 균등한 간격으로 분포한다. 표본평균은 약 \(\bar{x} \approx 243.29\)이다. \(s\)를 계산하면(또는 코드 출력에서 확인하면):

\[ s \approx 1.032 \]

고전적 표준오차는:

\[ \text{SE} = \frac{s}{\sqrt{n}} = \frac{1.032}{\sqrt{31}} = \frac{1.032}{5.568} \approx 0.185 \]

보기 1이 찍은 Classical SE: 0.1854와 맞는다. \(\square\)

연습문제 3. 실무에서 붓스트랩 복제 횟수 \(B\)는 얼마나 권장되는가? 계산 시간과 붓스트랩 표준오차 추정값의 정확도 사이의 맞바꿈을 논하라.

풀이

붓스트랩 표준오차 추정값의 표준오차는 근사적으로:

\[ \text{SE}(\widehat{\text{SE}}_{\text{boot}}) \approx \frac{\widehat{\text{SE}}_{\text{boot}}}{\sqrt{2B}} \]

상대 정밀도가 \(1/\sqrt{2B}\)라는 뜻이므로, \(B\)는 넘어야 할 문턱이 아니라 정밀도를 돌리는 손잡이다. 얼마나 돌릴지는 그 정밀도로 무엇을 할지가 정한다.

표준오차 하나를 얻는 것이 목적이라면 \(B = 200\)에서 상대 정밀도가 5%다. 표준오차 자체가 대개 한두 자리 유효숫자로 보고되는 양이라는 점을 생각하면 이것으로 충분한 경우가 많다. \(B = 1{,}000\)이면 2.2%, \(B = 10{,}000\)이면 0.7%로 내려간다.

신뢰구간이나 가설검정은 사정이 다르다. 여기서는 분포의 꼬리에 있는 분위수를 추정해야 하는데, 꼬리는 재표본 가운데 극히 일부만 가지고 추정하는 자리라 중앙의 퍼짐보다 훨씬 느리게 안정된다. 위 공식이 적용되지 않으며 \(B\)가 수천은 되어야 하고, 정밀한 구간이 필요하면 \(50{,}000\) 이상을 쓰기도 한다.

맞바꿈은 단순하다. \(B\)를 두 배로 하면 정밀도가 \(\sqrt{2} \approx 1.41\)배 좋아지고 계산 시간도 두 배가 된다. 네 배 정밀하게 만들려면 열여섯 배를 계산해야 한다는 뜻이다. \(\square\)

연습문제 4. 가격 자료에 붓스트랩을 적용하여 표본중앙값의 표준오차를 추정하라. 중앙값에 붓스트랩이 특히 유용한 이유는 무엇인가?

풀이
import numpy as np
np.random.seed(42)

data = np.array([245.02, 244.88, 244.76, 244.65, 244.53, 244.42,
                 244.30, 244.18, 244.08, 243.97, 243.85, 243.74,
                 243.63, 243.52, 243.40, 243.28, 243.17, 243.06,
                 242.95, 242.83, 242.72, 242.61, 242.49, 242.38,
                 242.27, 242.15, 242.04, 241.93, 241.81, 241.70,
                 241.59])

# 평균 대신 중앙값을 계산한다. 바뀐 것은 np.mean -> np.median 뿐이다.
# 이것이 붓스트랩의 가장 큰 장점이다.
# 중앙값의 표준오차에는 s/sqrt(n) 같은 간단한 공식이 없다.
# (있긴 하지만 모집단 밀도를 알아야 해서 실무에서 쓸 수 없다.)
# 붓스트랩은 통계량이 무엇이든 같은 절차로 답을 준다.
boot_medians = np.array([
    np.median(np.random.choice(data, size=len(data), replace=True))
    for _ in range(10_000)
])
se_median = boot_medians.std(ddof=1)
print(f"Bootstrap SE of the median: {se_median:.4f}")

출력:

Bootstrap SE of the median: 0.3071

중앙값에 붓스트랩이 특히 유용한 이유는:

  1. 임의의 분포에서 통하는, 널리 알려진 간단한 닫힌 형태의 중앙값 표준오차 공식이 없다.
  2. 점근 공식 \(\text{SE}(\text{median}) \approx 1 / (2f(m)\sqrt{n})\)은 중앙값 \(m\)에서의 모집단 밀도 \(f\)를 알아야 하는데, 이것 자체가 추정하기 어렵다.
  3. 붓스트랩은 분포 모양을 자동으로 반영하며 밀도추정 없이 비모수적 추정값을 준다.

\(\square\)

연습문제 5. 표본평균에 대해 \(B \to \infty\)일 때 붓스트랩 표준오차가 \(s/\sqrt{n}\)으로 수렴함을 증명하라. 여기서 \(s\)는 표본표준편차이다.

풀이

붓스트랩 표본에서 각 \(x_i^*\)는 \(\{x_1, \ldots, x_n\}\)에서 독립적이고 균등하게 뽑힌다. 붓스트랩 표본평균은 \(\bar{x}^* = \frac{1}{n}\sum_{j=1}^n x_j^*\)이다.

(자료로 조건화한) 붓스트랩 분포 아래에서:

\[ E^*[x_j^*] = \frac{1}{n}\sum_{i=1}^n x_i = \bar{x} \]
\[ \text{Var}^*(x_j^*) = \frac{1}{n}\sum_{i=1}^n (x_i - \bar{x})^2 = \frac{n-1}{n} s^2 \]

붓스트랩 아래에서 \(x_j^*\)들이 i.i.d.이므로:

\[ \text{Var}^*(\bar{x}^*) = \frac{1}{n} \cdot \frac{n-1}{n} s^2 = \frac{(n-1)s^2}{n^2} \]

\(B \to \infty\)일 때 붓스트랩 표준오차는 다음으로 수렴한다:

\[ \widehat{\text{SE}}_{\text{boot}} \to \sqrt{\frac{(n-1)s^2}{n^2}} = \frac{s\sqrt{n-1}}{n} \]

이는 \(n\)이 크면 \(s/\sqrt{n}\)에 매우 가깝다(\(\sqrt{(n-1)/n}\)배만큼 다르다). 이 작은 차이는 붓스트랩 세계의 분산 \(\text{Var}^*(x_j^*)\)가 편차제곱합을 \(n-1\)이 아니라 \(n\)으로 나눈 값이라는 데서 오며, \(n \to \infty\)일 때 사라진다.

복제값들의 표준편차를 \(\text{ddof}=1\)로 계산해도 이 차이는 없어지지 않는다. \(\text{ddof}\)는 \(B\)개의 복제값을 평균 내는 쪽의 보정이라 유한한 \(B\)에서의 치우침만 손볼 뿐, \(B \to \infty\) 극한값은 \(\text{ddof}\)와 무관하게 \(s\sqrt{n-1}/n\)이다. 보기 1이 바로 그 확인이다. \(\text{ddof}=1\)로 계산한 붓스트랩 값이 \(0.1834\)로, 고전 값 \(0.1854\)가 아니라 \(1.0320 \times \sqrt{30}/31 = 0.1823\) 쪽에 앉는다. \(\square\)

연습문제 6. 붓스트랩 표본 하나에 원자료의 특정 관측값이 한 번도 포함되지 않을 확률을 구하라. \(n\)이 클 때의 극한은 얼마인가? 이 사실이 어디에 쓰이는가?

풀이

크기 \(n\)인 복원추출을 \(n\)번 하므로, 한 번의 추출에서 특정 관측값이 뽑히지 않을 확률이 \(1-1/n\)이고 \(n\)번 모두 뽑히지 않을 확률은

\[ \left(1-\frac1n\right)^n \]

이다. \(n\to\infty\)에서

\[ \left(1-\frac1n\right)^n \to e^{-1} = 0.3679 \]

이다.

\(n\) 10 31 100 1000
제외될 확률 0.349 0.362 0.366 0.368

수렴이 아주 빨라 \(n \ge 20\)이면 사실상 0.368이다. 각 붓스트랩 표본은 원자료의 약 63%만 담고 나머지 37%는 빠진다.

쓰임새. 빠진 관측값들을 아웃오브백(OOB) 표본이라 하고, 모형 성능을 공짜로 평가하는 데 쓴다. 랜덤 포레스트가 대표적이다. 각 트리를 붓스트랩 표본으로 학습시키고, 그 트리가 보지 못한 37%로 예측 성능을 잰다. 별도의 검증셋을 떼어 놓지 않고도 교차검증에 준하는 추정값을 얻는다.

같은 계산이 배깅(bagging) 전반에 적용되고, 0.632 부트스트랩이라는 오차 추정법의 이름도 \(1-e^{-1} = 0.632\)에서 왔다. 훈련오차(낙관적)와 OOB 오차(비관적)를 \(0.368 : 0.632\)로 가중평균해 절충하는 방법이다.

연습문제 7. 붓스트랩으로 신뢰구간을 만드는 세 가지 방법 — 백분위법, 기본(basic)법, BCa — 을 적고 어느 것을 언제 써야 하는지 정리하라.

풀이

붓스트랩 복제값을 \(\hat\theta^*_1,\dots,\hat\theta^*_B\)라 하자.

(1) 백분위법. 복제값의 2.5 백분위수와 97.5 백분위수를 그대로 구간으로 쓴다.

\[ \left(\hat\theta^*_{(0.025)},\ \hat\theta^*_{(0.975)}\right) \]

가장 간단하고 직관적이며, 단조변환에 대해 불변이다(로그 척도에서 구간을 만들고 되돌린 것과 같다). 다만 \(\hat\theta\)에 편향이 있으면 구간도 함께 밀린다.

(2) 기본(basic)법. 붓스트랩 분포의 중심이 \(\theta\)가 아니라 \(\hat\theta\)라는 점을 반영해 뒤집는다.

\[ \left(2\hat\theta - \hat\theta^*_{(0.975)},\ 2\hat\theta-\hat\theta^*_{(0.025)}\right) \]

\(\hat\theta^*-\hat\theta\)가 \(\hat\theta-\theta\)의 분포를 근사한다는 논리에서 나온다. 편향을 어느 정도 보정하지만 변환 불변성을 잃고, 모수공간을 벗어날 수 있다(비율 구간이 음수가 되는 등).

(3) BCa(편향보정·가속). 백분위법의 두 끝점을 편향 \(\hat z_0\)와 가속 \(\hat a\)로 조정한다. \(\hat z_0\)은 복제값 가운데 \(\hat\theta\)보다 작은 것의 비율에서, \(\hat a\)는 잭나이프 값들의 왜도에서 추정한다.

  • 장점: 변환 불변이면서 편향과 왜도를 모두 보정한다. 이론적으로 이차정확이라 포함확률이 명목값에 가장 가깝다.
  • 단점: 계산이 무겁고(\(n\)번의 잭나이프가 추가로 필요하다), \(B\)가 2000 이상이어야 안정적이다.

권고.

상황 방법
기본값, 계산 여유 있음 BCa
\(\hat\theta\)가 대칭이고 편향이 작음 백분위법으로 충분
빠른 탐색, \(B\)가 작음 백분위법
분산이 안정적으로 추정됨 부트스트랩-\(t\)(스튜던트화)가 가장 정확할 수 있음

scipy.stats.bootstrap의 기본값이 BCa인 것이 이 권고를 반영한 것이다.

연습문제 8. 붓스트랩이 실패하는 예를 들어라. \(X_i \sim \text{Uniform}(0,\theta)\)에서 \(\hat\theta = \max_i X_i\)의 붓스트랩 분포가 왜 참 표집분포를 근사하지 못하는가?

풀이

참 표집분포에서 \(\hat\theta = X_{(n)}\)는 연속분포를 따르고 \(P(\hat\theta = \theta) = 0\)이다.

그런데 붓스트랩 표본의 최댓값 \(\hat\theta^*\)는 원자료의 값들 중 하나일 수밖에 없고, 특히

\[ P(\hat\theta^* = \hat\theta) = P(\text{최댓값이 적어도 한 번 뽑힘}) = 1-\left(1-\frac1n\right)^n \to 1-e^{-1} = 0.632 \]

이다. 붓스트랩 분포의 63%가 한 점에 몰려 있다. 연속분포를 근사해야 하는데 질량의 3분의 2가 한 점에 쌓여 있으니 근사가 될 수 없다. 표준오차는 참값보다 작게 나오고, 백분위 신뢰구간의 위쪽 끝이 언제나 \(\hat\theta\)에 붙어 버린다. \(n\)을 늘려도 0.632는 그대로라 일치성조차 없다.

왜 그런가. 붓스트랩이 통하는 근거는 통계량이 경험분포 \(\hat F_n\)에 매끄럽게(하다마르 미분가능하게) 의존한다는 것이다. 평균, 분산, 상관계수, 분위수(밀도가 양수인 곳에서)가 모두 그렇다. 그러나 최댓값은 분포의 경계라는 극도로 국소적인 특징에 의존하며, 경험분포는 경계를 잘 담지 못한다.

다른 실패 사례.

  • 분산이 무한한 분포(파레토 \(\alpha<2\))의 표본평균. 극한분포가 안정분포인데 붓스트랩이 이를 따라가지 못한다.
  • 모수가 모수공간의 경계에 있는 경우(분산성분이 0인지 검정할 때 등).
  • 독립이 아닌 자료. 시계열·군집자료에 단순 붓스트랩을 쓰면 의존구조가 파괴되어 표준오차를 과소평가한다.

대처. 최댓값 같은 경우에는 \(m\)-out-of-\(n\) 붓스트랩(\(m/n \to 0\)인 크기 \(m\)을 뽑는다)이나 서브샘플링을 쓰면 일치성이 회복된다. 시계열에는 블록 붓스트랩을 쓴다. "붓스트랩은 만능"이 아니라는 점을 아는 것이 중요하다.

연습문제 9. 회귀분석에서 붓스트랩을 적용하는 두 방식 — 사례 재표집과 잔차 재표집 — 을 설명하고 각각이 무엇을 가정하는지 밝혀라.

풀이

사례 재표집. \((x_i, y_i)\) 쌍을 통째로 복원추출해 새 자료를 만들고 매번 회귀를 다시 적합한다.

  • 가정: 관측 쌍이 독립이고 같은 분포를 따른다. 오차의 등분산이나 정규성을 요구하지 않는다.
  • 적합한 경우: 관측연구처럼 \(x\)도 확률적으로 얻어진 경우. 이분산이 있어도 자동으로 반영되므로 강건한 표준오차를 준다.
  • 주의: 설계행렬이 매번 달라지므로 범주형 변수의 어떤 수준이 재표본에서 아예 빠져 적합이 실패할 수 있다.

잔차 재표집. 원자료로 한 번 적합해 \(\hat\beta\)와 잔차 \(\hat\varepsilon_i\)를 얻고, 잔차를 복원추출해 \(y_i^* = \hat y_i + \hat\varepsilon^*_i\)를 만든 뒤 \(x\)는 고정한 채 다시 적합한다.

  • 가정: 모형이 옳고(평균 구조가 맞고) 오차가 독립이며 등분산이다. 오차의 분포 모양은 가정하지 않는다.
  • 적합한 경우: 실험처럼 \(x\)가 설계된 값으로 고정된 경우.
  • 주의: 등분산 가정이 깨지면 잘못된 표준오차를 준다. 잔차의 분산이 \(\hat y_i\)에 따라 다른데 무작위로 섞어 붙이기 때문이다. 또 잔차는 참 오차보다 분산이 작으므로(\(\operatorname{Var}(\hat\varepsilon_i) = \sigma^2(1-h_{ii})\)) \(\hat\varepsilon_i/\sqrt{1-h_{ii}}\)로 보정해 쓰는 편이 낫다.

고르는 기준. 가정을 덜 하는 쪽이 사례 재표집이므로 의심스러우면 사례 재표집이다. 잔차 재표집은 가정이 맞을 때 더 효율적이고, \(x\)가 고정된 설계에서 개념적으로 옳다. 두 결과가 크게 다르면 등분산 가정을 의심해야 한다는 진단 신호로도 쓸 수 있다.

연습문제 10. 붓스트랩 표준오차에는 두 가지 오차가 섞여 있다. 하나는 \(B\)가 유한해서 생기는 오차이고 다른 하나는 \(n\)이 유한해서 생기는 오차다. 둘을 구별하고 각각을 줄이는 방법을 적어라.

풀이

세 가지 양을 구별해야 한다.

\[ \underbrace{\operatorname{SE}(\hat\theta)}_{\text{참값}} \quad\longleftarrow\quad \underbrace{\operatorname{SE}_{\hat F_n}(\hat\theta^*)}_{\text{이상적 붓스트랩}} \quad\longleftarrow\quad \underbrace{\widehat{\operatorname{SE}}_B}_{\text{실제 계산값}} \]

(1) 몬테카를로 오차 (\(B\)가 유한해서). 이상적 붓스트랩 값과 실제 계산값의 차이다. 표준편차의 추정오차이므로 상대오차가 대략

\[ \frac{1}{\sqrt{2(B-1)}} \]

이다. \(B=200\)이면 5%, \(B=1000\)이면 2.2%, \(B=10000\)이면 0.7%다.

줄이는 법: \(B\)를 늘린다. 순전히 계산 문제이고 자료를 더 모을 필요가 없다. 연습문제 3에서 본 대로 표준오차만 필요하면 \(B = 200\)에서 이미 상대 정밀도가 5%라 쓸 만하지만, 신뢰구간의 분위수를 추정하려면 꼬리를 다루므로 \(B\)가 수천은 되어야 한다.

(2) 통계적 오차 (\(n\)이 유한해서). 이상적 붓스트랩 값과 참 표준오차의 차이다. \(\hat F_n\)이 \(F\)와 다르기 때문에 생기며, \(B\)를 아무리 늘려도 줄지 않는다. \(B = \infty\)로 계산해도 남는다.

줄이는 법: 자료를 더 모으는 수밖에 없다. 대체로 \(O(1/\sqrt n)\)이나 \(O(1/n)\) 규모다.

실무적 교훈. \(B\)를 10만으로 키워 소수점 넷째 자리까지 안정된 값을 얻었다고 해서 그 값이 참 표준오차에 가깝다는 뜻이 아니다. \(B\)를 늘리면 "붓스트랩이 답하는 질문"에 대한 답이 정밀해질 뿐, 그 질문이 원래 질문과 얼마나 가까운지는 \(n\)이 정한다.

진단 방법이 하나 있다. \(B\)를 두 배로 늘렸을 때 결과가 거의 변하지 않으면 몬테카를로 오차는 충분히 작은 것이다. 그 뒤에도 남는 불확실성은 자료의 양이 정하는 몫이며, 그것은 붓스트랩이 해결해 줄 수 없다.


정리하며

붓스트랩은 표본을 모집단인 셈 치고 재표집하는 방법이다. 절차는 네 줄로 끝난다. 복원추출로 크기 \(n\)짜리 재표본을 뽑고, 통계량을 계산하고, \(B\)번 되풀이하고, 그 복제값들의 표준편차를 취한다. 복원이라는 조건만은 양보할 수 없다. 비복원으로 \(n\)개를 뽑으면 원래 표본이 그대로 나와 잴 변동이 생기지 않기 때문이다.

표본평균에서 고전 공식과 거의 같은 답이 나온 것이 이 쪽의 검증이었다. 그러나 붓스트랩의 진가는 공식이 있는 자리가 아니라 없는 자리에서 나온다. 중앙값, 비, 상관계수, 분위수, 회귀계수처럼 델타 방법이 번거롭거나 가정이 의심스러운 통계량에서, 같은 네 줄이 그대로 쓰인다.

\(B\)는 몇 개면 되는지 묻는다면, 표준오차만 필요할 때는 \(B\approx200\)으로도 충분하고 신뢰구간의 꼬리 분위수가 필요할 때는 \(B\ge2000\)이 권장된다. 다만 \(B\)를 늘려 줄일 수 있는 것은 모의오차뿐이며, 원래 표본이 모집단을 얼마나 잘 베꼈는지는 \(B\)와 아무 상관이 없다. 표본이 모집단을 대표하지 못하면 붓스트랩은 그 편향을 고스란히 복제하고, 최댓값처럼 분포의 경계에 의존하는 통계량에서는 아예 실패한다. 만능이 아니라는 점을 아는 것까지가 이 도구를 쓰는 조건이다.

여기까지가 통계량을 가리지 않는 공통 도구다. 다음 절 표본평균 \(\bar X\)부터는 통계량을 하나씩 잡아, 그 표본분포가 모집단 가정에 얼마나 기대는지를 이론과 모의실험으로 확인한다.