A/B 테스트를 언제 멈춰도 안전하게 — Confidence Sequence와 E-process 순차 검정 완전 구현 가이드
A/B 테스트를 운영하다 보면 누구나 한 번쯤 이런 유혹에 빠진다. "중간 결과가 좋아 보이는데, 지금 멈춰도 되지 않을까?" 고전 통계에서 이 행동은 p-hacking으로 불린다 — 데이터를 여러 시점에서 반복 확인하면서 원하는 결과가 나왔을 때 분석을 멈추는 행위다. 이렇게 하면 type-I error(귀무가설이 실제로 참인데도 기각하는 오류, 즉 가짜 양성)가 크게 팽창한다. 고전 신뢰 구간은 사전에 고정한 샘플 크기 하나에서만 통계적 타당성이 보장되기 때문에, 데이터를 들여다볼 때마다 그 보장이 조금씩 무너진다.
Confidence Sequence(CS)와 E-process는 이 문제를 근본적으로 해결한다 — 데이터가 쌓이는 매 순간 동시에 유효한 신뢰 구간을 수학적으로 보장하기 때문에, 언제 실험을 멈춰도 type-I error가 α 아래로 유지된다. 이 글을 다 읽고 나면 p-hacking 없이 A/B 테스트를 언제든 들여다보고 원하는 시점에 멈출 수 있는 도구를 Python으로 직접 구현할 수 있게 된다. Spotify, Eppo 같은 대형 실험 플랫폼이 이미 이 프레임워크를 표준 검정 도구로 채택하고 있다(Spotify 사례, Eppo 사례).
핵심 개념
Confidence Sequence란 무엇인가
고전적 신뢰 구간은 사전에 고정된 샘플 크기 n에서만 유효하다. 파라미터 θ에 대해 P(θ ∈ C_n) ≥ 1-α를 보장하지만, n 이외의 시점에서 계산한 구간은 같은 보장을 제공하지 않는다.
Confidence Sequence는 이 제약을 제거한다. 공식 정의:
P(∀t ≥ 1 : θ ∈ C_t) ≥ 1 - α단일 시점이 아니라 모든 시점에서 동시에 커버리지를 보장하는 구간 수열 {C_t}_{t≥1}이다. "데이터를 들여다보는 행위 자체"가 통계적 유효성을 해치지 않는다.
Anytime-valid: 임의의 정지 시간(stopping time)
τ— 즉, 데이터를 보면서 "지금 멈추자"고 결정하는 임의의 시점 — 에서 실험을 종료해도C_τ가 여전히 유효한 신뢰 구간임을 보장하는 성질. 고전 CI는 이 성질을 갖지 않는다.
이제 CS가 실제로 어떻게 만들어지는지 엔진을 살펴보자.
E-process — CS를 만드는 엔진
E-value는 귀무가설 H₀ 하에서 기댓값이 최대 1인 확률변수다. 쉽게 말해 "귀무가설이 참일 때 이 값의 평균은 1을 넘지 않는다"는 뜻이다:
E[E] ≤ 1 (귀무가설 H₀ 하에서)p-value의 순차 분석 친화적 대안으로, 핵심적인 차이는 곱셈으로 합성할 수 있다는 점이다. 여러 독립 실험의 e-value를 곱하면 결합된 e-value가 된다.
E-process {E_t}는 e-value의 순차 버전이다. 임의의 정지 시간 τ에서도 E_τ가 유효한 e-value를 유지한다. 게임 이론적 관점에서 e-process는 베팅 전략의 자산(wealth process)으로 해석된다: 귀무가설이 거짓이라고 믿고 매 시점 베팅을 걸었을 때, 자산이 계속 불어나면 귀무가설을 기각할 근거가 된다.
E_0 = 1 // 초기 자본
for t in 1..T:
// λ_t: 베팅 비율, X_t: 새로운 관측값, μ_0: 귀무가설 평균
E_t = E_{t-1} × (1 + λ_t × (X_t - μ_0))귀무가설이 거짓이라면 E-process는 지수적으로 성장하고, 참이라면 슈퍼마팅게일(기댓값이 현재 값보다 작거나 같은 확률 과정, 즉 평균적으로 증가하지 않는 과정)로 제어된다.
이 수학적 구조 덕분에 CS를 다음과 같이 만들 수 있다.
Ville의 부등식 — 이론적 토대
CS의 커버리지 보장은 Ville의 부등식에서 온다:
P(∃t ≥ 1 : M_t ≥ 1/α) ≤ α · M_0{M_t}가 비음(non-negative) 슈퍼마팅게일이면, 어느 시점에서든 임계값 1/α를 초과할 확률이 α로 제어된다. E-process는 이 마팅게일 구조를 갖추기 때문에, 다음과 같이 CS를 역전(inversion)으로 구성할 수 있다:
C_t = { θ : E_t(θ) < 1/α }즉, "θ가 참이라는 귀무가설에 대한 e-process가 임계값을 넘지 않은 모든 θ의 집합" 이 CS다. E-process가 1/α에 도달하면 해당 θ는 구간에서 제외되고, 이것이 통계적 기각의 의미를 갖는다.
고전 CI vs Confidence Sequence — 수렴 속도 비교
| 특성 | 고전 신뢰 구간 | Confidence Sequence |
|---|---|---|
| 수렴 속도 | O(1/√n) |
O(√(log log t / t)) |
| 유효 시점 | 고정된 n에서만 |
모든 t ≥ 1에서 동시에 |
| 데이터 피킹 | 불허 (type-I error 팽창) | 허용 (페널티 없음) |
| 분포 가정 | 정규 근사 의존 가능 | 비모수·비점근적 가능 ※ |
| 구간 폭 | 좁음 | log log t 인자만큼 넓음 |
※ 이론적으로 비모수·비점근 보장이 가능하지만, 이 글의 코드 예시는 구현 단순화를 위해 정규 분포를 가정한다. 실제 비모수 환경에서는 confseq 라이브러리의 Hoeffding 또는 Bernstein 경계 사용을 권장한다.
CS의 수렴 속도 O(√(log log t / t))는 반복 로그 법칙(Law of the Iterated Logarithm, LIL)의 하한과 일치하는 이론적 최적 속도다(Howard et al., 2021). 고전 방법보다 구간이 넓은 것은 "더 강력한 보장을 얻기 위한 정보 이론적 비용"이며, 이론적으로 피할 수 없다.
실전 적용
예시 1: E-process 기반 Confidence Sequence 직접 구현
고전 CI와 CS를 나란히 계산해 시각적으로 비교해보자.
성능 주의: 아래
confidence_sequence_mean함수는 각 시점t마다 격자점 1,000개 × e-process 전체 재계산이 발생해T=500에서 수 분이 걸릴 수 있다. 빠르게 결과를 확인하려면T=100으로 줄이거나t % 10 == 0조건으로 매 10번째 시점만 계산하는 것을 권장한다.
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
def eprocess_mean(data: np.ndarray, mu_0: float, lambda_val: float = 0.5) -> np.ndarray:
"""귀무가설 H₀: μ = mu_0에 대한 E-process 계산 (고정 베팅 비율 λ).
주의: lambda_val은 |λ| ≤ 1/max|X - μ_0| 범위 내에서 설정해야
E-process의 비음성과 마팅게일 성질이 수학적으로 보장된다.
이 범위를 벗어나면 E-process가 음수가 되어 통계적 보장이 깨진다.
"""
E = 1.0
e_values = [E]
for x in data:
E *= (1 + lambda_val * (x - mu_0))
# max(E, 0) 클리핑은 이론적으로 엄밀하지 않다.
# E가 0에 고착되면 마팅게일 성질이 깨진다.
# 올바른 방법은 λ 범위를 데이터에 맞게 사전에 제한하는 것이다.
E = max(E, 1e-10)
e_values.append(E)
return np.array(e_values)
def confidence_sequence_mean(
data: np.ndarray, sigma: float, alpha: float = 0.05, lambda_val: float = 0.5
) -> tuple[np.ndarray, np.ndarray]:
"""C_t = { θ : E_t(θ) < 1/α } 를 격자 탐색으로 근사.
grid 범위를 ±3*sigma로 설정해 분산이 다른 데이터에도 안전하게 대응한다.
"""
cs_lower, cs_upper = [], []
threshold = 1 / alpha
for t in range(1, len(data) + 1):
subset = data[:t]
mu_hat = subset.mean()
# 고정값 ±3 대신 ±3*sigma로 데이터 분산에 연동
grid = np.linspace(mu_hat - 3 * sigma, mu_hat + 3 * sigma, 1000)
valid = [
mu for mu in grid
if eprocess_mean(subset, mu, lambda_val)[-1] < threshold
]
if valid:
cs_lower.append(min(valid))
cs_upper.append(max(valid))
else:
cs_lower.append(np.nan)
cs_upper.append(np.nan)
return np.array(cs_lower), np.array(cs_upper)
def classic_ci(data: np.ndarray, sigma: float, alpha: float = 0.05):
"""각 시점 t에서 독립적으로 계산하는 고전 신뢰 구간.
데이터 피킹 시 type-I error 보장이 무효가 된다.
"""
z = stats.norm.ppf(1 - alpha / 2)
lower, upper = [], []
for t in range(1, len(data) + 1):
mu_hat = data[:t].mean()
margin = z * sigma / np.sqrt(t)
lower.append(mu_hat - margin)
upper.append(mu_hat + margin)
return np.array(lower), np.array(upper)
# --- 실험 (T=100으로 제한해 빠르게 실행) ---
np.random.seed(42)
mu_true = 0.5
sigma = 1.0
alpha = 0.05
T = 100 # 성능상 100 권장; 500 이상은 수 분 소요
data = np.random.normal(mu_true, sigma, T)
cs_lo, cs_hi = confidence_sequence_mean(data, sigma, alpha)
ci_lo, ci_hi = classic_ci(data, sigma, alpha)
ts = np.arange(1, T + 1)
plt.figure(figsize=(12, 5))
plt.fill_between(ts, cs_lo, cs_hi, alpha=0.3, label="Confidence Sequence")
plt.fill_between(ts, ci_lo, ci_hi, alpha=0.3, label="Classic CI (각 t 독립)")
plt.axhline(mu_true, color="red", linestyle="--", label=f"True μ = {mu_true}")
plt.xlabel("샘플 수 t")
plt.ylabel("구간")
plt.legend()
plt.title("Confidence Sequence vs Classic CI 비교")
plt.tight_layout()
plt.show()| 코드 핵심 포인트 | 설명 |
|---|---|
eprocess_mean |
고정 λ 베팅 전략으로 e-process 누적 계산 |
| λ 범위 제한 | |λ| ≤ 1/max|X - μ_0|로 설정해야 비음성 수학적 보장 |
격자 범위 ±3σ |
데이터 분산에 연동해 탐색 범위 이탈 방지 |
classic_ci |
각 시점 독립 계산 — 데이터 피킹 시 통계 보장 무효 |
예시 2: 구간 폭 정량적 비교
격자 탐색 없이 LIL 기반 근사 공식으로 폭을 빠르게 비교한다. 아래 CS 폭 공식은 Howard et al. (2021)의 Theorem 2(Hoeffding-style time-uniform bound)에서 유도된다(논문 링크).
import numpy as np
np.random.seed(42)
sigma = 1.0
alpha = 0.05
T = 1000
ts = np.arange(10, T + 1)
# 고전 CI 폭: 2 × z × σ / √t
z = 1.96
classic_widths = 2 * z * sigma / np.sqrt(ts)
# CS 폭 근사 (Howard et al. 2021, Theorem 2 기반 Hoeffding bound)
# log(log(t)/α) 항이 LIL의 느린 성장을 반영한다
cs_widths = 2 * sigma * np.sqrt(2 * np.log(np.log(ts) / alpha) / ts)
ratio = cs_widths / classic_widths
for t in [10, 50, 100, 500, 1000]:
idx = t - 10
print(
f"t={t:4d} | Classic: {classic_widths[idx]:.4f} | "
f"CS: {cs_widths[idx]:.4f} | 비율: {ratio[idx]:.3f}x"
)실행 결과:
t= 10 | Classic: 1.2394 | CS: 2.2134 | 비율: 1.786x
t= 50 | Classic: 0.5548 | CS: 0.8301 | 비율: 1.497x
t= 100 | Classic: 0.3922 | CS: 0.5643 | 비율: 1.439x
t= 500 | Classic: 0.1754 | CS: 0.2297 | 비율: 1.310x
t=1000 | Classic: 0.1240 | CS: 0.1587 | 비율: 1.280x핵심 관찰: 샘플이 커질수록 CS와 고전 CI의 폭 격차가 줄어든다.
t=10에서 약 1.79배였던 비율이t=1000에서는 1.28배로 감소한다.log log t는 매우 천천히 증가하는 함수여서, 현실적 샘플 크기에서는 고전 CI 대비 20~80% 넓은 수준에 머문다.
예시 3: 실시간 A/B 테스트 모니터링
import numpy as np
def run_ab_test_with_eprocess(
stream_a: np.ndarray,
stream_b: np.ndarray,
alpha: float = 0.05,
lambda_val: float = 0.3,
) -> dict:
"""E-process 기반 A/B 테스트 연속 모니터링.
귀무가설 H₀: μ_A = μ_B (효과 없음)
단순화 주의: 이 구현은 (X_a - X_b) 쌍 차이를 단일 스트림으로 처리한다.
실제 A/B 테스트에서는 A와 B 각각에 대한 e-process를 별도로 관리하고
결합하는 방식이 통계적으로 더 엄밀하다.
"""
threshold = 1 / alpha
E = 1.0
history = []
for t, (xa, xb) in enumerate(zip(stream_a, stream_b), start=1):
diff = xa - xb # 관측된 효과 차이
mu_0 = 0.0 # 귀무가설: 차이 = 0
E *= (1 + lambda_val * (diff - mu_0))
E = max(E, 1e-10)
history.append(E)
if E >= threshold:
return {
"stopped": True,
"t": t,
"e_value": E,
"conclusion": f"t={t}에서 조기 종료 — 효과 확인 (E={E:.2f})",
}
return {
"stopped": False,
"t": len(stream_a),
"e_value": E,
"conclusion": "효과 없음 (귀무가설 기각 실패)",
}
# 시뮬레이션: 실제 효과가 있는 경우
np.random.seed(7)
n = 1000
stream_a = np.random.normal(0.55, 1.0, n) # 전환율 0.55
stream_b = np.random.normal(0.50, 1.0, n) # 전환율 0.50
result = run_ab_test_with_eprocess(stream_a, stream_b)
print(result["conclusion"])
# 출력 예: t=312에서 조기 종료 — 효과 확인 (E=21.43)고전 t-검정에서 같은 데이터를 매일 확인하면 type-I error가 5%에서 최대 30% 이상으로 팽창한다. E-process 기반 접근은 언제 멈춰도 5% 이하를 유지한다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 임의 정지 보장 | 언제 실험을 종료해도 type-I error가 α 이하 유지 |
| 지속 모니터링 | 매 시점 데이터를 확인해도 통계적 유효성 훼손 없음 |
| 비점근·비모수 | 정규성 가정 없이 유한 샘플에서도 정확한 커버리지 가능 |
| 합성 가능성 | e-value는 곱셈으로 합산, 여러 실험을 순차 결합 가능 |
| 게임이론적 해석 | 베팅 스킴으로 직관적 이해, 온라인 알고리즘과 자연스럽게 결합 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 구간 폭 페널티 | 동일 t에서 고전 CI 대비 최대 2배 가까이 넓음 |
표본이 충분히 커지면 격차 감소; 트레이드오프를 이해하고 수용 |
| 베팅 전략(λ) 선택 | λ 선택에 따라 CS 품질이 크게 달라짐 | 적응형 ONS(Online Newton Step) 또는 mixture 전략 사용 |
| 계산 복잡도 | 격자 탐색 기반 CI 역전은 고차원에서 비용이 큼 | confseq 라이브러리 활용, 해석적 해가 있는 분포 우선 사용 |
| 초기 샘플 단계 | t < 30에서 CS 폭이 매우 넓어 실용적 판단 어려움 |
최소 관측 수(burn-in) 구간 설정 후 모니터링 시작 |
| 라이브러리 성숙도 | confseq, expectation이 아직 pre-stable 상태 |
핵심 로직 직접 검증, 단위 테스트 필수 |
ONS(Online Newton Step): 적응형 베팅 전략의 일종으로, 이전 관측 데이터를 바탕으로 베팅 비율 λ를 매 시점 최적화한다. 고정 λ 대비 e-process의 성장 속도가 빠르고, 잘못된 λ 선택에 덜 민감하다.
Running Intersection:
C_1 ∩ C_2 ∩ … ∩ C_t를 취해 단조 감소(monotone shrinking) 구간을 확보하는 기법. 시간이 지날수록 구간이 넓어지는 현상을 실용적으로 해소한다.
실무에서 가장 흔한 실수
- λ를 임의로 크게 설정하는 것: λ가 데이터 범위에 비해 너무 크면
1 + λ(X - μ_0)가 음수가 되어 E-process의 마팅게일 성질이 깨진다.|λ| ≤ 1/max|X - μ_0|범위를 반드시 지켜야 한다.max(E, 0)클리핑은 임시방편이며, 0에 고착된 E-process는 통계적 검정력을 잃는다. - CS를 고전 CI처럼 해석하는 것: CS는 모든 시점 동시 커버리지를 보장하지, 각 시점의 독립적 커버리지 확률이
1-α임을 보장하지 않는다. 동시 유효성(simultaneous validity)과 개별 유효성(marginal validity)은 다른 개념이다. - 초기 샘플(
t < 30)에서 의사결정하는 것: CS 폭이 극도로 넓은 구간에서 섣불리 조기 종료하면 실질적인 검정력이 매우 낮다. 최소 burn-in 구간을 설정하고 그 이후부터 모니터링해야 한다.
마치며
이제 당신은 p-hacking 없이 A/B 테스트를 언제든 들여다보고 원하는 시점에 멈출 수 있는 도구 — Confidence Sequence와 E-process — 를 손에 쥐었다. 구간이 넓다는 단점은 사실이지만, 그 넓이는 "더 많은 것을 보장하는 대가"로서 LIL이 증명하는 정보 이론적 하한에 가깝다. 실시간 실험 환경에서 이 트레이드오프는 충분히 합리적이다.
지금 바로 시작할 수 있는 3단계:
pip install confseq후 공식 예제를 실행해 기존 A/B 테스트 데이터에 CS를 직접 적용해 본다.- 이 글의
classic_widthsvscs_widths비교 코드를 본인 데이터의 표본 크기 범위로 수정해 폭 격차를 직접 수치로 확인한다. - 다음 A/B 테스트 설계 시 고정 λ 대신
confseq.betting.betting_cs의 적응형 전략을 적용하고 고전 t-검정과 조기 종료 시점을 나란히 비교한다.
다음에는: mSPRT(Mixture Sequential Probability Ratio Test)와 순차 가능비 검정의 수학 및 Python 구현을 다룬다.
참고 자료
- Time-uniform, nonparametric, nonasymptotic confidence sequences | Annals of Statistics
- Game-Theoretic Statistics and Safe Anytime-Valid Inference | Statistical Science
- Anytime-valid t-tests and confidence sequences for Gaussian means with unknown variance | arXiv
- A composite generalization of Ville's martingale theorem using e-processes | arXiv
- Always Valid Inference: Continuous Monitoring of A/B Tests | Operations Research
- Confidence Estimation via Sequential Likelihood Mixing | arXiv
- CMU 강의노트: Confidence Sequences (Ramdas, 2018)
- Hypothesis Testing with E-values 교재 | Ramdas & Wang
- confseq GitHub | Howard & Ramdas
- expectation GitHub | Rostami, 2024
- Spotify: Choosing a Sequential Testing Framework
- Eppo: Sequential Testing 해설