A/B 테스트 peeking을 수학적으로 허용하는 법 — e-process와 anytime-valid 신뢰 시퀀스로 실시간 효과 크기 추정하기
A/B 테스트를 운영하다 보면 누구나 한 번쯤 유혹을 느낍니다. 중간 결과가 좋아 보이는데, 굳이 수천 명의 추가 데이터를 기다려야 할까요? 이른바 peeking problem — 중간에 결과를 들여다보고 실험을 조기 종료하면 type-I 오류율(거짓 양성)이 걷잡을 수 없이 불어납니다. 시뮬레이션 연구에 따르면, 매 관측 후 p < 0.05를 확인하며 최대 1000번 반복하는 실험의 실제 type-I 오류율은 0.05가 아닌 약 30% 이상으로 치솟습니다. 전통적인 통계 방법은 이 상황을 허락하지 않습니다.
그러나 Netflix, Adobe, Spotify는 이미 이 문제를 해결한 프레임워크를 실제 운영 환경에 적용하고 있습니다(Netflix TechBlog, 2024). 바로 신뢰 시퀀스(Confidence Sequences) 와 e-process 기반의 anytime-valid 추론입니다. 아래에서 보는 것처럼, 단 몇 줄의 코드로 "언제 멈춰도 유효한" 신뢰구간을 실시간으로 계산할 수 있습니다.
from confseq.betting import betting_cs
lower, upper = betting_cs(x=diffs, alpha=0.05, running_intersection=True)TL;DR — 이 글을 읽고 나면
- e-process가 왜 "데이터 수집 중 어느 시점에 멈춰도 오류율이 유지되는가"를 수학적으로 보장하는지 이해할 수 있습니다
confseq라이브러리로 직접 신뢰 시퀀스를 계산하고 시각화할 수 있습니다- 여러 독립 실험의 결과를 e-value 곱셈으로 안전하게 결합할 수 있습니다
권장 사전 지식: 기본적인 A/B 테스트 개념과 p-value에 대한 이해면 충분합니다. 마팅게일이나 측도론 배경이 없어도 게임이론적 직관을 통해 핵심을 이해할 수 있습니다.
목차
핵심 개념
전통적 신뢰구간의 한계: 왜 peeking이 문제인가
고전적인 95% 신뢰구간은 다음을 보장합니다.
동일한 방법으로 실험을 무한히 반복하면, 그 중 95%의 구간이 진짜 모수를 포함한다.
이 보증은 데이터 수집이 끝난 후 한 번만 계산한다는 전제 위에 서 있습니다. 중간에 p-value를 확인하고, 유의미하면 멈추고, 아니면 계속하는 방식은 이 전제를 깨트립니다. 결과를 들여다볼 때마다 "이번엔 우연히 유의미해 보일" 기회가 쌓이기 때문입니다.
신뢰 시퀀스: 언제 멈춰도 유효한 구간
신뢰 시퀀스 ${C_t}_{t \geq 1}$ 은 훨씬 강력한 보증을 제공합니다.
$$P\left(\forall t \geq 1 : \theta^* \in C_t\right) \geq 1 - \alpha$$
이 수식이 말하는 것은 단 하나입니다: 100번 관측하든 1만 번 관측하든, 중간에 몇 번을 들여다보든, 전체 시퀀스에서 진짜 효과 크기가 구간 안에 들어올 확률이 95% 이상으로 유지됩니다.
핵심은 $\forall t$ — 모든 시점에서 동시에 성립한다는 점입니다. 이것이 time-uniform 커버리지입니다.
Time-uniform Coverage: 단일 시점의 확률이 아니라, 모든 가능한 중단 시점에 걸쳐 동시에 성립하는 커버리지 보증. 전통적 신뢰구간의 pointwise 보증보다 본질적으로 강한 조건입니다.
e-value와 e-process: 게임이론적 증거 측도
신뢰 시퀀스를 구성하는 핵심 도구가 e-value입니다.
| 개념 | 정의 | 의미 |
|---|---|---|
| e-value | 귀무가설 $H_0$ 하에서 $E[E] \leq 1$인 비음수 확률변수 | "귀무가설이 맞다면 이 베팅에서 기대 수익은 0 이하"라는 게임이론적 진술 |
| e-process | e-value의 순차적 버전 ${E_t}_{t \geq 1}$ | 데이터가 쌓일수록 갱신되는 실시간 증거 측도 |
| p-value와의 차이 | e-value는 곱셈으로 합성 가능(composable) | p-value는 독립 실험 결과를 단순 곱으로 결합 불가 |
e-process의 핵심 보증은 Ville 부등식으로 표현됩니다.
$$P\left(\exists t \geq 1 : E_t \geq 1/\alpha\right) \leq \alpha$$
Ville 부등식을 쉽게 이해하기: 동전이 공정하다면(귀무가설이 참), 아무리 오래 베팅해도 자산이 특정 임계값(1/α)을 넘을 확률은 α 이하입니다. 반대로 말하면, 임계값을 넘었다는 것은 "이 동전은 공정하지 않다"는 강력한 증거가 됩니다. 이것이 순차 검정의 수학적 기반입니다.
(수학적으로는 마팅게일(Martingale) — "평균적으로 증가하지 않는 확률 과정" — 에 대한 시간-균등 확률 한계이며, Doob의 최댓값 부등식의 확장입니다.)
따라서 $E_t \geq 1/\alpha$가 되는 첫 시점에 귀무가설을 기각해도 오류율이 $\alpha$ 이하로 제어됩니다.
e-process 구성의 세 가지 방법론
귀무가설 $H_0$ 하에서 손실 없는 게임을 설계하는 방식에 따라 세 가지로 나뉩니다.
- 베팅 마팅게일 (Betting Martingale) — 단순 귀무가설
관측 $x_t$마다 베팅 전략 $\lambda_t$를 선택하고 자산을 곱셈적으로 갱신합니다.confseq.betting.betting_cs가 이 방식을 구현합니다.
$$E_t = \prod_{i=1}^{t}(1 + \lambda_i(x_i - \mu_0))$$ - 혼합 마팅게일 (Mixture Martingale) — 복합 귀무가설
모수가 특정 범위에 있을 때 사전 분포 $\pi(\theta)$로 적분합니다. "정확히 어떤 대안인지는 모르지만, 귀무가설이 아닌 어딘가"를 검정하는 상황에 적합합니다.
$$E_t = \int \prod_{i=1}^{t} \frac{f_\theta(x_i)}{f_0(x_i)} , d\pi(\theta)$$ - 지수 마팅게일 (Exponential Martingale) — 정규분포 등 지수족 분포
지수족 분포(정규분포, 베르누이 분포 등 분포 형태가 지수 형태로 표현되는 분포군)에서 자연스럽게 유도됩니다. 2024년 미지 분산 문제를 해결한 방법이 이 범주에 속합니다.
SAVI(Safe Anytime-Valid Inference) 프레임워크: 이 세 가지 방법론을 통합하는 우산 프레임워크입니다. e-process(가설 검정)와 신뢰 시퀀스(효과 크기 추정)를 결합해, 데이터 수집 중 임의의 시점에 유효한 통계적 추론을 가능하게 합니다(Ramdas et al., Statistical Science 2023).
개념을 이해했다면, 이제 실제 코드로 구현해보겠습니다.
실전 적용
예시 1: confseq 라이브러리로 실시간 신뢰 시퀀스 시각화
먼저 라이브러리를 설치합니다.
pip install confseq numpy matplotlib클릭률(CTR) A/B 테스트 시나리오입니다. A그룹(CTR 10%)과 B그룹(CTR 13%)을 실험하며 신뢰 시퀀스를 실시간으로 추적합니다.
import numpy as np
import matplotlib.pyplot as plt
from confseq.betting import betting_cs
# 재현 가능한 시뮬레이션 설정
rng = np.random.default_rng(42)
# 진짜 클릭률: A=0.10, B=0.13 (3%p 효과)
n_obs = 2000
clicks_a = rng.binomial(1, 0.10, n_obs)
clicks_b = rng.binomial(1, 0.13, n_obs)
# 관측 차이 시퀀스 (B - A)
# 귀무가설 H₀: E[클릭률 차이] = 0
# 두 그룹이 동일 모집단에서 독립 무작위 배정된 경우에 이 해석이 유효합니다
diffs = clicks_b.astype(float) - clicks_a.astype(float)
# e-process 기반 신뢰 시퀀스 계산 (시간-균등 95%)
lower, upper = betting_cs(
x=diffs,
alpha=0.05,
running_intersection=True, # 시간이 지날수록 단조 축소 (실무 권장)
parallel=False
)
# 시각화
t = np.arange(1, n_obs + 1)
running_mean = np.cumsum(diffs) / t
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# 상단: 신뢰 시퀀스
axes[0].fill_between(t, lower, upper, alpha=0.3, label='95% 신뢰 시퀀스')
axes[0].plot(t, running_mean, 'b-', linewidth=1.5, label='누적 평균 차이')
axes[0].axhline(y=0.03, color='r', linestyle='--', label='진짜 효과 (0.03)')
axes[0].axhline(y=0, color='k', linestyle='-', linewidth=0.5)
axes[0].set_xlabel('관측 수 (t)')
axes[0].set_ylabel('클릭률 차이 (B - A)')
axes[0].set_title('e-process 기반 신뢰 시퀀스: 언제 멈춰도 유효합니다')
axes[0].legend()
# 하단: 신뢰구간 너비 (수렴 패턴)
width = upper - lower
axes[1].plot(t, width, 'g-', linewidth=1.5)
axes[1].set_xlabel('관측 수 (t)')
axes[1].set_ylabel('신뢰구간 너비')
axes[1].set_title('샘플이 쌓일수록 좁아지는 신뢰구간')
plt.tight_layout()
plt.savefig('confidence_sequence.png', dpi=150)
plt.show()
# 실시간 결론 도출 — 0을 포함하지 않는 첫 시점 찾기
first_significant = np.where((lower > 0) | (upper < 0))[0]
if len(first_significant) > 0:
t_stop = first_significant[0] + 1
print(f"t={t_stop} 시점에 처음으로 유의미한 차이 감지")
print(f" 신뢰구간: [{lower[t_stop-1]:.4f}, {upper[t_stop-1]:.4f}]")
print(f" 누적 관측 수: {t_stop} (전체의 {t_stop/n_obs:.1%})")실행 결과 (예시):
t=847 시점에 처음으로 유의미한 차이 감지
신뢰구간: [0.0021, 0.0589]
누적 관측 수: 847 (전체의 42.4%)전체 2,000명 중 42% 시점에서 이미 효과를 감지할 수 있었습니다. 전통적인 고정 표본 방법이었다면 2,000명을 모두 기다렸을 것입니다.
| 코드 요소 | 역할 |
|---|---|
betting_cs() |
베팅 마팅게일 기반 신뢰 시퀀스 계산 |
running_intersection=True |
시간이 지날수록 구간이 단조 감소 (실무에서 권장) |
alpha=0.05 |
시간-균등 신뢰 수준 1-α = 95% |
lower > 0 조건 |
신뢰구간이 0을 포함하지 않으면 효과가 있는 것으로 판단 |
예시 2: 연속형 지표(세션 시간, 매출)에서의 신뢰 시퀀스
CTR처럼 이진 지표가 아닌, 세션 시간이나 주문 금액 같은 연속형 지표에도 동일한 방식이 적용됩니다. 분산이 얼마인지 몰라도 됩니다.
import numpy as np
from confseq.betting import betting_cs
rng = np.random.default_rng(0)
# 실제 세션 시간 (초): A그룹 평균 120초, B그룹 평균 125초 (5초 개선)
true_effect = 5.0
n = 5000
obs_a = rng.normal(120, 30, n) # 표준편차 30초 — 분산은 알 필요 없음
obs_b = rng.normal(125, 30, n)
diffs = obs_b - obs_a
# 분산을 몰라도 betting_cs가 내부적으로 적응적 추정
lower, upper = betting_cs(
x=diffs,
alpha=0.05,
running_intersection=True
)
t_arr = np.arange(1, n + 1)
running_mean = np.cumsum(diffs) / t_arr
# 주요 체크포인트 모니터링 리포트
check_points = [100, 500, 1000, 2000, 5000]
print(f"{'시점':>6} | {'누적 평균':>10} | {'하한':>8} | {'상한':>8} | {'너비':>8} | {'0 포함?':>8}")
print("-" * 65)
for t in check_points:
idx = t - 1
width = upper[idx] - lower[idx]
contains_zero = "예" if lower[idx] <= 0 <= upper[idx] else "아니오"
print(f"{t:>6} | {running_mean[idx]:>10.4f} | {lower[idx]:>8.4f} | "
f"{upper[idx]:>8.4f} | {width:>8.4f} | {contains_zero:>8}")실행 결과 (예시):
시점 | 누적 평균 | 하한 | 상한 | 너비 | 0 포함?
-----------------------------------------------------------------
100 | 5.2341 | -18.9432 | 29.4114 | 48.3546 | 예
500 | 4.8912 | -5.1203 | 14.9027 | 20.0230 | 예
1000 | 5.1234 | 0.4312 | 9.8156 | 9.3844 | 아니오
2000 | 4.9876 | 1.8234 | 8.1518 | 6.3284 | 아니오
5000 | 5.0312 | 2.7891 | 7.2733 | 4.4842 | 아니오| 시점 | 기대 동작 |
|---|---|
| t = 100 | 구간이 넓지만 진짜 평균을 포함 |
| t = 1000 | 신호를 포착, 의사결정 가능한 수준으로 좁아짐 |
| t = 5000 | 고정 표본 t-구간에 근접한 너비 |
| 전체 경로 | 모든 시점에서 동시에 포함 보증 |
예시 3: 여러 e-value의 합성 — 독립 실험 결합
e-value의 가장 강력한 성질은 곱셈적 합성 가능성입니다. 여러 독립 실험의 e-value를 곱하면 유효한 결합 e-value가 됩니다. p-value가 Fisher 결합법(카이제곱 변환)을 필요로 하는 것과 달리, e-value는 단순히 곱하면 됩니다.
import numpy as np
from scipy.stats import binom
def compute_evalue_bernoulli(successes: int, trials: int, null_p: float) -> float:
"""
베르누이 e-value 계산 (Beta(1,1) = Uniform 사전 분포 하 혼합 우도)
Beta(1,1) 사전 분포에서 베르누이 혼합 우도의 해석적 해는 1/(n+1)이므로:
log_marginal_alt = -log(trials + 1)
"""
log_marginal_alt = -np.log(trials + 1) # 혼합 대안 우도 (해석적 해)
log_null = binom.logpmf(successes, trials, null_p)
return float(np.exp(log_marginal_alt - log_null))
# 3개의 독립 팀이 동일 가설을 별도 실험
experiments = [
{"successes": 45, "trials": 400, "name": "팀 A (마케팅)"},
{"successes": 23, "trials": 200, "name": "팀 B (제품)"},
{"successes": 67, "trials": 600, "name": "팀 C (데이터)"},
]
null_ctr = 0.10 # 귀무가설: CTR = 10%
alpha = 0.05 # 기각 임계값: 1/0.05 = 20
print(f"{'실험':>15} | {'e-value':>10} | {'독립 기각?':>10}")
print("-" * 45)
combined_e = 1.0
for exp_data in experiments:
e = compute_evalue_bernoulli(
exp_data["successes"], exp_data["trials"], null_ctr
)
combined_e *= e
reject = "예" if e >= 1 / alpha else "아니오"
print(f"{exp_data['name']:>15} | {e:>10.3f} | {reject:>10}")
print("-" * 45)
print(f"{'결합 e-value':>15} | {combined_e:>10.3f} | "
f"{'예' if combined_e >= 1/alpha else '아니오':>10}")
print(f"\n기각 임계값(1/α): {1/alpha:.1f}")
print(f"결론: {'귀무가설 기각 — 효과 있음' if combined_e >= 1/alpha else '기각 불가'}")실행 결과 (예시):
실험 | e-value | 독립 기각?
---------------------------------------------
팀 A (마케팅) | 1.842 | 아니오
팀 B (제품) | 1.213 | 아니오
팀 C (데이터) | 2.107 | 아니오
---------------------------------------------
결합 e-value | 4.712 | 아니오
기각 임계값(1/α): 20.0
결론: 기각 불가세 팀 모두 단독으로는 기각에 실패했지만, 결합 e-value는 개별보다 강한 증거를 제공합니다. 더 많은 데이터를 수집하거나 추가 팀의 실험을 합산하면 임계값을 넘을 수 있습니다.
e-value 합성의 핵심: 이 합성 가능성(composability) 덕분에 메타분석, 분산 실험, 멀티암 테스트에서 자연스럽게 활용됩니다. 중요한 점은 합성 자체가 추가적인 통계 보정 없이 유효하다는 것입니다.
실무에서 가장 흔한 실수
1. running_intersection=False 상태로 조기 종료 판단
running_intersection=False일 경우 신뢰구간이 좁아졌다가 다시 넓어질 수 있어, "지금 유의미하다"는 판단이 시간에 따라 뒤집힐 수 있습니다. 실무에서는 반드시 running_intersection=True를 사용하는 것이 권장됩니다. 이 옵션은 구간이 단조적으로만 좁아지도록 보장해 결론의 일관성을 유지합니다.
2. e-value를 "큰 p-value의 역수"로 오해
e-value가 20이라고 해서 p-value 0.05와 동치가 아닙니다. e-value의 스케일은 "귀무가설 하에서 이 정도 이익을 낼 확률"이며, Ville 부등식에 의해 $1/\alpha$ 임계값으로 해석됩니다. e-value = 20은 "귀무가설이 참이라면 이 베팅 결과가 나올 확률이 5% 이하"라는 의미로, p-value와는 계산 방식과 해석 방식이 근본적으로 다릅니다.
3. 분산 추정 없이 가우시안 신뢰 시퀀스 적용
실제 데이터의 분산이 가정과 다르면 time-uniform 보증이 깨집니다. 예시 2처럼 betting_cs를 사용하면 분산을 명시적으로 지정하지 않아도 됩니다. 분산이 알려져 있다고 잘못 가정하고 파라미터를 하드코딩하는 방식은 피하는 것이 안전합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| Optional Stopping | 중간 결과를 보고 실험을 중단해도 type-I 오류율이 α 이하로 유지됩니다 |
| Optional Continuation | 결론이 나지 않으면 실험을 연장할 수 있습니다. 기존 e-value에 새 e-value를 곱하면 유효한 결합 증거가 됩니다 |
| 연속 모니터링 | 미리 정한 중간 분석 일정 없이 매 관측마다 결과를 확인할 수 있습니다 |
| 합성 가능성 | 여러 독립 실험의 e-value를 곱하면 유효한 결합 e-value가 됩니다. 분산 실험 통합에 유리합니다 |
| 비모수적 확장 | 정규분포 가정 없이도 베팅 마팅게일로 유효한 신뢰 시퀀스를 구성할 수 있습니다 |
| 게임이론적 해석 | "귀무가설 하에서 베팅으로 돈을 벌 수 없다"는 직관적 해석이 가능합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 효율 손실 | 동일 신뢰 수준을 위해 고정 표본 방법보다 더 많은 데이터가 필요할 수 있습니다 | 정규화 e-process(arXiv:2410.01427) 또는 회귀 조정으로 효율 회복 가능 |
| 초기 구간 너비 | 초기 관측이 적을 때 신뢰구간이 매우 넓어 의사결정이 어렵습니다 | running_intersection=True 옵션과 최소 관측 수 기준 사전 설정 권장 |
| 복합 귀무가설 복잡성 | 단순 귀무가설보다 복합 귀무가설에 대한 e-process 구성이 수학적으로 복잡합니다 | confseq 라이브러리의 혼합 경계 모듈 활용 |
| 팀 학습 곡선 | p-value에 비해 e-value/e-process 개념이 생소하여 조직 전체의 이해도 확보가 어렵습니다 | safestatistics.com의 시각화 자료, ICML 2025 튜토리얼 자료 활용 권장 |
Optional Stopping vs Optional Continuation: Optional Stopping은 원하는 시점에 멈출 수 있는 자유, Optional Continuation은 반대로 결과가 나오지 않으면 계속 이어갈 수 있는 자유를 뜻합니다. 전통적 통계는 둘 다 허용하지 않지만, SAVI 프레임워크는 둘 다 오류율 보증을 유지하면서 허용합니다.
마치며
e-process 기반 신뢰 시퀀스는 "언제 멈춰도 유효하다"는 보증을 수학적으로 제공하는 실용적 도구이며, Netflix와 Adobe가 이미 운영 환경에서 검증한 기술입니다.
지금 바로 시작해볼 수 있는 3단계입니다.
pip install confseq로 라이브러리를 설치하고, 예시 1의 코드를 실행해볼 수 있습니다.diffs배열에 여러분의 지표 차이값을 넣는 것만으로 바로 동작합니다.- 기존 A/B 테스트 파이프라인에 e-value를 병렬로 계산하는 코드를 추가해볼 수 있습니다. 처음부터 교체하지 않고 기존 p-value와 나란히 비교 관찰하며 신뢰를 쌓아가는 방식을 권장합니다.
# 기존 파이프라인에 한 줄 추가 from confseq.betting import betting_cs # 기존 코드: p_value = ttest_ind(group_a, group_b).pvalue # 추가할 코드: lower_cs, upper_cs = betting_cs( x=group_b - group_a, # 또는 페어링된 차이값 alpha=0.05, running_intersection=True ) is_significant_anytime = (lower_cs[-1] > 0) or (upper_cs[-1] < 0)- Netflix TechBlog의 "Sequential A/B Testing Keeps the World Streaming" 시리즈(파트 1, 파트 2)를 읽어보시면 좋습니다. 실제 운영 환경에서의 구현 세부사항과 조직 내 도입 과정을 생생하게 확인할 수 있습니다.
다음 글: 베이지안 팩터와 e-value의 관계 — 두 프레임워크가 어떻게 수렴하고 어디서 갈라지는지, 실무 선택 기준을 정리합니다.
참고 자료
입문용 — 먼저 읽기를 권장하는 자료
- E-values — Wikipedia
- The Stats Map: E-Process
- Safe Anytime-Valid Inference (SAVI) — CMU Statistics
- Netflix TechBlog: Sequential A/B Testing Keeps the World Streaming (Part 1)
- Netflix TechBlog: Sequential A/B Testing Keeps the World Streaming (Part 2)
심화용 — 수학적 기반을 더 깊이 탐구하고 싶다면
- Game-Theoretic Statistics and Safe Anytime-Valid Inference | Statistical Science, 2023
- Always Valid Inference: Bringing Sequential Analysis to A/B Testing | arXiv:1512.04922
- Anytime-valid t-tests and confidence sequences for Gaussian means with unknown variance | Sequential Analysis, 2024 — arXiv 사전 인쇄본: arXiv:2310.03722
- A tiny review on e-values and e-processes — Ruodu Wang (2023)
- ICML 2025 Tutorial: Game-theoretic Statistics and Sequential Anytime-Valid Inference
구현용 — 코드 레벨에서 바로 적용하고 싶다면
- GitHub: confseq — Confidence sequences and uniform boundaries
- GitHub: expectation — Python library for e-processes, e-values
- Anytime-Valid Confidence Sequences in an Enterprise A/B Testing Platform | ACM Web Conference 2023
- Anytime-Valid Linear Models and Regression Adjusted Causal Inference | Netflix Research
- Regularized e-processes: anytime valid inference with knowledge-based efficiency gains | arXiv:2410.01427
- E-values for Adaptive Clinical Trials | arXiv:2602.06379
- Prediction-Powered E-Values | OpenReview