실험 네 개를 규칙대로 판정한다
목표
SRM·z 검정·신뢰구간·표본 크기·Holm 보정을 표준 라이브러리로 계산하는 함수를 만들고, 실험 네 개의 결과를 정해진 규칙으로 판정한다.
왜 중요한가
작은 팀의 A/B 테스트는 표본이 적고, 매일 들여다보고, 결과가 없으면 쪼개 본다. 셋 다 우연을 발견으로 바꾸는 습관이다. 규칙을 먼저 정하고 그 규칙대로 계산해야 실험이 제품 결정을 지켜 준다.
재료
/opt/fixtures/founder/ab/exp1.csv—user_id,variant,day,segment,converted(A 대조, B 변경, 14일, 세그먼트 10개)/opt/fixtures/founder/ab/exp2.csv—user_id,variant,converted(50:50 설계)/opt/fixtures/founder/ab/exp3_daily.csv—day,a_users,a_conv,b_users,b_conv(A/A 실험의 날짜별 증분)/opt/fixtures/founder/ab/exp4.csv—variant,users,conversions(control 과 v1~v5)
정의
- SRM: 기대 50:50 에 대한 카이제곱 적합도, 자유도 1.
chi2 = Σ(관측−기대)²/기대,p = math.erfc(math.sqrt(chi2/2)). p < 0.001 이면 SRM. - z 검정:
diff = pb − pa. z 는 합동 비율p = (ca+cb)/(na+nb)의 표준오차√(p(1−p)(1/na+1/nb))로, 양측p = 2(1−Φ(|z|)). - 95% 신뢰구간:
diff ± Φ⁻¹(0.975)·√(pa(1−pa)/na + pb(1−pb)/nb)(비합동). Φ 는statistics.NormalDist(). - 판정: p < 0.05 이고 ci_low > 0 →
"ship_b", p < 0.05 이고 ci_high < 0 →"keep_a", 나머지"inconclusive". - 표본 크기(한 쪽): 읽기 자료의 식, α = 0.05 양측, 검정력 0.8,
math.ceil. - Holm: p 를 오름차순으로 k=0,1,… 번째에
p·(m−k) ≤ 0.05이면 기각, 처음 아니면 멈춘다. - 모든 p·비율·차이는 소수 여섯째 자리 반올림.
단계
/root/founder/ab/ab.py에srm(n_a, n_b)→{"chi2": x, "p": y}를 만들고,/root/founder/ab/srm.json에 exp1·exp2 각각{"a": n, "b": n, "chi2": x, "p": y, "srm": true/false}를 쓴다.ab.py에ztest(ca, na, cb, nb)→{"diff", "z", "p", "ci_low", "ci_high"}를 만든다(ca·cb 는 전환 수, na·nb 는 사용자 수)./root/founder/ab/result.json에 exp1 의a_users,a_conv,b_users,b_conv,diff,p,ci_low,ci_high,decision을 쓴다.ab.py에sample_size(p_base, mde)를 만들고/root/founder/ab/power.json에baseline(exp1 A 전환율, 소수 넷째 자리),mde(0.01),n_per_arm(= sample_size(baseline, 0.01)),exp1_min_arm(exp1 두 쪽 사용자 수 가운데 작은 것),powered(exp1_min_arm ≥ n_per_arm)를 쓴다./root/founder/ab/peeking.json에 exp3 의daily_p(1일차부터 누적 합계로 계산한 p 21개),first_significant_day(누적 p < 0.05 인 첫 날, 없으면 null),final_p,final_decision(마지막 날 누적값으로 한 판정)을 쓴다.ab.py에holm(pvalues)(이름→p 딕셔너리 → 기각되는 이름의 정렬된 목록)를 만들고,/root/founder/ab/multi.json에 exp4 의p(v1~v5 각각 control 대비),naive(p < 0.05 인 이름, 정렬),holm을 쓴다./root/founder/ab/segments.json에 exp1 세그먼트 10개 각각의p,naive(p < 0.05, 정렬),holm을 쓴다./root/founder/ab/decision.json에exp1(판정),exp2_trustworthy(SRM 이 없으면 true),exp3_decision(마지막 날 판정),exp4_ship(Holm 기각 목록),segment_claims(Holm 기각 세그먼트 목록)를 쓴다.
참고
from statistics import NormalDist; N = NormalDist(); N.cdf(z); N.inv_cdf(0.975)- 흔한 실수: 신뢰구간에 합동 표준오차를 쓰기, 단측 p, 증분을 누적하지 않고 날짜별 p 를 내기, 보정 없이 세그먼트를 보고하기, SRM 이 있는 실험을 그대로 판정하기.
표본 비율부터 확인한다
/root/founder/ab/ab.py 에 srm(n_a, n_b) 를 만들고 /root/founder/ab/srm.json 에 exp1·exp2 의 a·b·chi2·p·srm 을 쓴다.
기대값은 (n_a+n_b)/2 입니다. 자유도 1 의 카이제곱 꼬리 확률은 math.erfc(math.sqrt(chi2/2)) 로 구할 수 있습니다. SRM 기준은 p < 0.001.
두 비율의 z 검정과 95% 신뢰구간
ab.py 에 ztest(ca, na, cb, nb) 를 만든다. diff·z·p(양측)·ci_low·ci_high 를 소수 여섯째 자리로.
검정 통계량은 합동 비율의 표준오차, 신뢰구간은 각자 비율의 표준오차(비합동)입니다. Φ 는 statistics.NormalDist().cdf, 임계값은 inv_cdf(0.975).
exp1 을 판정한다
/root/founder/ab/result.json 에 exp1 의 a_users·a_conv·b_users·b_conv·diff·p·ci_low·ci_high·decision 을 쓴다.
판정 규칙: p < 0.05 이고 하한 > 0 이면 ship_b, p < 0.05 이고 상한 < 0 이면 keep_a, 그 밖은 inconclusive.
이 실험은 1%p 를 잡을 만큼 컸나
ab.py 에 sample_size(p_base, mde) 를 만들고 /root/founder/ab/power.json 에 baseline·mde·n_per_arm·exp1_min_arm·powered 를 쓴다.
p2 = p1 + mde, p̄ = (p1+p2)/2, z 값은 inv_cdf(0.975) 와 inv_cdf(0.8). 괄호 안 전체를 mde 로 나눠 제곱하고 올림합니다.
A/A 실험을 매일 들여다보면
/root/founder/ab/peeking.json 에 exp3 의 daily_p(누적 21개)·first_significant_day·final_p·final_decision 을 쓴다.
exp3_daily.csv 는 그날의 증분입니다. 1일차부터 더해 가며 그날까지의 누적 합으로 ztest 를 부르세요. 날짜는 1부터 셉니다.
변형 다섯 개 — Holm 보정
ab.py 에 holm(pvalues) 를 만들고 /root/founder/ab/multi.json 에 exp4 의 p(v1~v5)·naive·holm 을 쓴다.
각 변형을 control 과 ztest(control 이 a 쪽) 합니다. Holm 은 오름차순 k 번째(0부터)에 p·(m−k) ≤ 0.05 를 보고, 처음 실패하면 멈춥니다.
세그먼트 열 개로 쪼개 보면
/root/founder/ab/segments.json 에 exp1 세그먼트 10개의 p·naive·holm 을 쓴다.
세그먼트마다 A 와 B 를 따로 세어 같은 ztest 를 부르고, 10개 p 에 같은 holm 을 적용합니다.
실험마다 배포 판단
/root/founder/ab/decision.json 에 exp1·exp2_trustworthy·exp3_decision·exp4_ship·segment_claims 를 쓴다.
앞 단계 파일에서 모읍니다. SRM 이 있는 실험은 믿을 수 없는 실험입니다. 엿보기 반례의 결론은 첫 유의한 날이 아니라 마지막 날의 판정입니다.