MINERVA Phase C-4 — A/B 테스트 심화 (실험 설계·통계 검정·조기 종료)

06편 운영 토대 위에 통계 방법론을 얹는다 — 표본 크기·검정 선택·CUPED·Sequential Testing·다중 비교

MINERVA 06편이 YAML 실험 정의·sticky hash 할당·JSONL 메트릭 수집 같은 운영 토대를 다뤘다. 본 편은 그 위에 통계 방법론을 얹는다. 표본 크기·검정력 계산, 메트릭별 검정 선택, 분산 감소 기법(CUPED), 다중 비교 보정, Sequential Testing, A/A test, 자주 발생 함정(peeking·HARKing·novelty effect)을 정리한다. 06편과 옵션 A로 보완 — 운영 메커니즘은 06, 통계 방법론은 22.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 A/B 심화가 필요한가

06편 A/B 실험은 실험을 돌릴 수 있게 하는 운영 토대를 다뤘다 — YAML override, sticky hash, JSONL 수집, 기본 t-test/카이제곱. 그러나 운영 도구가 갖춰지면 곧 통계 질문이 따라온다.

06편이 답하는 것 22편이 답하는 것
어떻게 실험을 정의·할당·기록하는가 표본을 얼마나 모아야 검정력이 충분한가
어떤 검정을 기본 선택하는가 (t-test·카이제곱) 메트릭 분포·연속성·종속성에 따라 어떤 검정을 골라야 하는가
SRM이 무엇인가 A/A test·Sequential Testing·Peeking 보정을 어떻게 운영에 녹이는가
결과 분석 API 결과 해석 — 통계 유의 vs 실용 유의, 다중 비교 보정

운영 시스템이 잘 돌아가는데 결론이 틀린다면 통계 단계에서 새는 것이다. 본 편은 MINERVA 운영 가능한 의사결정 시스템으로 만드는 데 필요한 통계 방법을 다룬다.

2 실험 설계 7단계

A/B 실험 설계 표준 절차
  1. 비즈니스 질문 → 실험 가설로 변환
  2. 메트릭 선정 — North Star + Guardrail
  3. MDE(Minimum Detectable Effect) 결정 — “이만큼은 잡아내야 한다”
  4. 표본 크기 계산 — Power Analysis
  5. 할당 설계 — 비율·계층화·기간
  6. 실험 실행 — sticky 할당, SRM 모니터링
  7. 분석과 의사결정 — 검정 선택, 다중 비교 보정, 사후 분석

각 단계가 실패할 때 어떤 해석 오류로 이어지는지 본 편의 절들이 다룬다.

3 단계 1 — 가설 정의

# experiments/exp_001.yaml
hypothesis: |
  reranker를 BGE-large로 교체하면 답변 정확도(thumbs-up rate)가
  baseline 대비 +3pp 이상 개선된다.
metrics:
  primary: thumbs_up_rate                # 단일 의사결정 메트릭
  guardrail:                              # 악화되면 안 되는 메트릭
    - p95_latency_ms                     # 성능 회귀 방지
    - cost_per_query_usd                 # 비용 폭증 방지
mde: 0.03                                 # +3pp 절대 차이
alpha: 0.05
power: 0.8

Primary는 의사결정 메트릭 1개로 제한 — 여러 개로 바꾸면 다중 비교 문제. Guardrail은 “악화 안 됨”을 확인하는 메트릭으로 별도 보정 필요.

4 단계 2 — 표본 크기와 검정력

비율(thumbs_up_rate) 비교의 표본 크기 공식:

\[ n = \frac{(z_{\alpha/2} + z_{\beta})^2 \cdot (p_1(1-p_1) + p_2(1-p_2))}{(p_1 - p_2)^2} \]

기호 의미 일반 값
\(\alpha\) 1종 오류율 0.05
\(\beta\) 2종 오류율 0.2 (검정력 0.8)
\(z_{\alpha/2}\) 1.96 (양측)
\(z_{\beta}\) 0.84
\(p_1, p_2\) 두 그룹 비율 (baseline·treatment) 0.55 → 0.58
\(\Delta = p_1 - p_2\) MDE 0.03
# scripts/sample_size.py — Power Analysis
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize


def required_sample_per_arm(p_baseline: float, mde: float,
                             alpha: float = 0.05, power: float = 0.8) -> int:
    p_treatment = p_baseline + mde
    h = proportion_effectsize(p_treatment, p_baseline)   # Cohen's h
    n = zt_ind_solve_power(effect_size=h, alpha=alpha, power=power, alternative="two-sided")
    return int(n) + 1


# 예: baseline 55%, MDE +3pp → 그룹당 4,343
print(required_sample_per_arm(p_baseline=0.55, mde=0.03))
검정력 부족이 만드는 함정

표본이 부족한 상태에서 t-test로 p > 0.05가 나오면 “효과 없음”이라 결론짓는 실수가 흔하다. 정답은 결정 보류 — 효과가 없는 것이 아니라 있어도 못 잡는다.

실험 결과 해석 매트릭스:
                  | p < 0.05 (유의)            | p >= 0.05 (비유의)
-----------------+----------------------------+--------------------------
검정력 충분 (≥0.8) | 효과 있음으로 결론         | 효과 없음으로 결론 가능
검정력 부족        | 효과 있다 — 단, replicate | 결정 보류 — 표본 더 모음

검정력 보고서는 결과 분석에 항상 동봉한다.

5 단계 3 — 메트릭 분포에 맞는 검정 선택

06편의 검정 선택 표를 확장한다.

연속형 메트릭 (latency, score)
├── 정규 + 등분산 → Student's t-test
├── 정규 + 이분산 → Welch's t-test (06편 기본)
└── 비정규 → Mann-Whitney U (위치 검정) 또는 bootstrap

이분 메트릭 (thumbs_up: 0/1)
├── 그룹별 표본 ≥ 30 → z-test (정규 근사)
├── 표본 작음 → Fisher's exact test
└── 매칭/계층화된 경우 → Mantel-Haenszel

카운트 메트릭 (queries_per_user)
├── 평균 비교 → Welch's t-test (보통 대용량이라 CLT 적용)
├── 분산 큰 long-tail → Mann-Whitney 또는 bootstrap
└── 비율 분모 명확 → Poisson rate ratio test

중복 측정 (한 사용자가 여러 query)
└── 마지막 N개로 평균 또는 cluster-robust SE

5.1 Welch vs Student — 등분산 가정의 위험

# 06편 기본은 Welch (등분산 가정 안 함)
import scipy.stats as stats

t_stat, p_value = stats.ttest_ind(group_a, group_b, equal_var=False)  # Welch
t_stat, p_value = stats.ttest_ind(group_a, group_b, equal_var=True)   # Student

Welch가 보수적이지만 분산이 다른 경우 Student는 거짓 양성률이 명목 5%를 벗어난다. MINERVA처럼 모델 변경이 분산 자체에 영향을 줄 수 있는 환경에서는 Welch 기본.

5.2 비정규 분포 — Mann-Whitney와 Bootstrap

LLM latency는 long-tail. 평균 비교가 의미 없거나 robust 검정이 필요.

# Mann-Whitney U — 분포 위치(중앙값 근사) 비교
u_stat, p_value = stats.mannwhitneyu(group_a, group_b, alternative="two-sided")

# Bootstrap — 임의 통계량의 신뢰구간
import numpy as np

def bootstrap_diff(group_a, group_b, n_iter=10_000, statistic=np.median):
    diffs = []
    for _ in range(n_iter):
        a_sample = np.random.choice(group_a, size=len(group_a), replace=True)
        b_sample = np.random.choice(group_b, size=len(group_b), replace=True)
        diffs.append(statistic(b_sample) - statistic(a_sample))
    return np.percentile(diffs, [2.5, 97.5])

ci_low, ci_high = bootstrap_diff(latency_a, latency_b, statistic=np.median)

p-value보다 신뢰구간이 의사결정에 직접적이다 — “0이 포함되지 않으면 유의” + “구간 폭”으로 효과 크기까지 한 번에 본다.

6 단계 4 — 분산 감소 (CUPED)

같은 표본 크기로 검정력을 높이는 기법. 사용자의 사전 메트릭(실험 시작 전 행동)을 covariate로 활용해 분산을 줄인다.

\[ Y_{\text{adj}} = Y - \theta(X - \bar{X}) \]

여기서: - \(Y\): 실험 기간 메트릭 - \(X\): 사전 기간 메트릭 (같은 사용자, 같은 종류) - \(\theta = \mathrm{Cov}(X, Y) / \mathrm{Var}(X)\) - \(Y_{\text{adj}}\): 분산 감소된 메트릭

import numpy as np


def cuped_adjust(y: np.ndarray, x: np.ndarray) -> np.ndarray:
    theta = np.cov(x, y, ddof=1)[0, 1] / np.var(x, ddof=1)
    x_mean = x.mean()
    return y - theta * (x - x_mean)


# MINERVA 적용 예
# x: 사용자의 실험 직전 4주 thumbs_up_rate (사전 메트릭)
# y: 실험 기간 thumbs_up_rate
y_adj_a = cuped_adjust(y_a, x_a)
y_adj_b = cuped_adjust(y_b, x_b)

t_stat, p_value = stats.ttest_ind(y_adj_a, y_adj_b, equal_var=False)
# ↑ 같은 표본인데 검정력이 보통 1.5~2× 증가

CUPED는 사용자 행동이 시간적으로 안정(time-invariant)일 때 효과가 크다. MINERVA의 thumbs_up_rate·queries_per_user 같은 메트릭은 안정적이라 적용 가치가 있다.

7 단계 5 — 다중 비교 보정

여러 실험 또는 여러 메트릭을 동시에 검정하면 false positive가 누적된다.

메트릭 개수 m 어느 하나라도 거짓 양성 확률 (α=0.05)
1 0.05
5 0.226
10 0.401
20 0.642

7.1 FWER (Family-Wise Error Rate) — Bonferroni

# 가장 단순·보수적
n_tests = 5
alpha_corrected = 0.05 / n_tests       # = 0.01
# → 각 검정의 p-value를 0.01과 비교

7.2 FDR (False Discovery Rate) — Benjamini-Hochberg

표본 많고 메트릭 많을 때 FWER은 너무 보수적. 대안:

from statsmodels.stats.multitest import multipletests

p_values = [0.001, 0.012, 0.034, 0.045, 0.078]
reject, p_adj, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
# reject: [True, True, True, False, False]
상황 권장
Primary metric 1개 (의사결정 핵심) 보정 안 함 (사전 정의된 단일 검정)
Guardrail 5개 (안 악화 확인) Bonferroni — 거짓 양성 회피
Exploratory 10~50개 (탐색 분석) BH-FDR — 발견을 놓치지 않으면서 통제

7.3 Pre-registration

가장 강력한 다중 비교 방어는 사전 등록: 어떤 메트릭을 어떤 순서로 검정할지 실험 시작 전에 적어 둠. 사후에 메트릭을 골라 보고하는 것(HARKing)을 막는다.

# experiments/exp_001.yaml
analysis_plan:
  primary: thumbs_up_rate
  guardrail: [p95_latency_ms, cost_per_query_usd]    # 사전 등록
  exploratory: []                                       # 탐색은 사후 별도 보고
  correction: fdr_bh                                    # 다중 비교 보정 방법

8 단계 6 — Sequential Testing (조기 종료)

매일 결과를 확인하고 “유의” 시점에 종료하면 거짓 양성률이 명목 5%를 크게 초과한다 (peeking 문제).

해결책 — Sequential Testing:

# Alpha Spending — Pocock 또는 O'Brien-Fleming
# 매 중간 분석 시점마다 alpha를 계산해 더 엄격하게 비교

# 간단 구현: 5번 중간 분석, Pocock-style
# (실무는 statsmodels.stats.power.NormalIndPower + 라이브러리)

import numpy as np

def pocock_boundary(n_looks: int, alpha: float = 0.05) -> float:
    # Pocock 1977 — 모든 look에서 동일 boundary
    # 근사: alpha_per_look ≈ alpha / sqrt(n_looks)
    return alpha / np.sqrt(n_looks)


# 예: 5번 look, total alpha 5%
# → 각 look에서 p < 0.0224일 때만 reject

또는 Sequential Probability Ratio Test (SPRT) / always-valid p-values:

# 라이브러리 예 — `confseq` (Howard et al. 2021)
# from confseq.boundaries import betting_mart_seq_test
# always-valid p-value: 매 시점 확인해도 5% 거짓 양성 보장
시나리오 권장
사전에 분석 시점 5~10개 결정 Pocock 또는 OBF (statsmodels)
매일 모니터링·실시간 종료 Always-valid p-value (SAVI, Howard et al.)
단일 사후 분석만 보정 불필요 (전통 t-test)

MINERVA에서 매일 thumbs_up_rate를 보고 빠르게 종료하고 싶으면 always-valid 라이브러리 도입이 가치 있다.

9 단계 7 — A/A test로 시스템 검증

A/A: 같은 모델·같은 프롬프트로 두 그룹을 만들어 “차이 없음”을 검정. 거짓 양성률이 5%로 나오면 운영 시스템이 정상.

# experiments/aa_validate.py
import numpy as np
from scipy.stats import ttest_ind

n_trials = 1000
n_per_arm = 5000
false_positives = 0

for _ in range(n_trials):
    # 같은 분포에서 두 sample
    a = np.random.normal(0.55, 0.15, n_per_arm)
    b = np.random.normal(0.55, 0.15, n_per_arm)
    _, p = ttest_ind(a, b, equal_var=False)
    if p < 0.05:
        false_positives += 1

rate = false_positives / n_trials
assert 0.04 <= rate <= 0.06, f"A/A 실패 — 거짓 양성률 {rate}"

A/A가 맞지 않으면 의심해야 할 곳: - 할당 시드 (sticky hash 충돌) - 메트릭 수집 race condition - 같은 사용자 양쪽에 들어감 (cross-contamination) - 분포 자체의 가정 위반 (정규성 X)

06편 SRM 검정이 할당 비율의 sanity check라면, A/A는 분석 파이프라인 전체의 sanity check다.

10 자주 발생하는 함정

10.1 Peeking — 매일 보고 유의 시 종료

# WRONG — 거짓 양성률 5%가 아니라 ~25%
for day in range(30):
    p = run_test(data_until_day(day))
    if p < 0.05:
        ship_treatment()    # 종료
        break

해법: Sequential Testing 또는 사전 결정된 종료 시점만.

10.2 HARKing — 결과 보고 가설 후위 결정

여러 메트릭 중 유의한 것만 골라 “이것이 핵심 메트릭이었다”고 보고. 사전 등록으로 방어.

10.3 Novelty Effect — 신규성 편향

새 UI/모델 도입 직후 사용자가 호기심으로 더 사용 → 단기 유의 → 장기 사라짐. 보통 2~4주 모니터링.

10.4 Simpson’s Paradox

전체에서는 treatment 우세인데 세그먼트별로는 모두 baseline 우세. 세그먼트 비율 차이로 발생. 세그먼트별 분석 + 가중 평균.

# 예시 — 부서별 thumpus_up_rate
# 전체: A 60%, B 62% → B 우세
# 부서 X (트래픽 80%): A 65%, B 60% → A 우세
# 부서 Y (트래픽 20%): A 40%, B 70% → B 우세 (작은 그룹)
# 세그먼트별 봐야 함

10.5 분산 감소 없이 표본 키우기만

CUPED 같은 분산 감소를 적용하면 표본을 1.5~2× 적게 모아도 같은 검정력. 무조건 표본을 늘리는 것보다 covariate 활용이 효율적.

11 MINERVA 적용 가이드

단계 06편 (운영) 22편 (통계)
가설 정의 YAML override hypothesis·MDE·primary/guardrail 명시
표본 크기 (없음) scripts/sample_size.py로 계산
할당 sticky hash A/A 검증 + SRM 모니터링
메트릭 수집 JSONL (없음 — 06편 그대로)
검정 t-test 기본 분포 따라 Welch·MWU·bootstrap
다중 비교 (없음) Bonferroni (guardrail) + FDR (탐색)
조기 종료 (없음) Pocock 또는 always-valid
사후 분석 (없음) A/A·세그먼트별·CUPED 보정 결과

운영에서 추가 구현이 필요한 부분: - scripts/sample_size.py — 사전 표본 계산기 - scripts/aa_check.py — 분기마다 A/A run - app/analysis/cuped.py — CUPED 적용 분석기 - app/analysis/sequential.py — Pocock·OBF·always-valid 옵션

이들이 모두 11-0 환경변수·12-0 테스트 fixture 구조 위에 자연스럽게 얹힌다.

12 정리

영역 핵심
설계 가설 + MDE + primary 1개 + guardrail 사전 등록
표본 Power Analysis (zt_ind_solve_power) — 검정력 0.8 표준
검정 선택 분포·이분산·종속성으로 결정 트리
분산 감소 CUPED — 사전 메트릭으로 분산 절감
다중 비교 Primary 보정 X, Guardrail Bonferroni, 탐색 FDR-BH
조기 종료 Pocock·OBF (사전 시점) 또는 always-valid (실시간)
검증 A/A test로 false positive 5% 확인
함정 peeking·HARKing·novelty·Simpson’s paradox 사전 정의로 방어

13 응용 분야

시나리오 본 편의 어느 절
새 reranker 도입 (정확도 +3pp 가설) 표본 크기 + Welch + guardrail Bonferroni
새 LLM 모델 (latency 분포 변화) Welch (이분산) + MWU 보조 + bootstrap CI
매일 모니터링 + 빠른 결정 Sequential (Pocock 5 looks) 또는 always-valid
다양한 메트릭 탐색 BH-FDR + 사전 등록은 primary만
사용자 사전 행동 covariate 보유 CUPED로 표본 절감
운영 시스템 정합성 점검 A/A test 분기마다

14 관련 주제

선행 학습 (선수)

후속 (Phase C-4 다음 편)

Cross-reference

  • 통계 검정 일반: Statistics 카테고리 가설검정 시리즈
  • LangGraph 분기: 13편 LangGraph 기초 — A/B 분기를 conditional edge로

Subscribe

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