1 정의
이론적으로는 매우 단순 (Buisson, 2021, Ch.8):
“각 customer 가 페이지에 도착할 때, control 또는 treatment 그룹에 무작위로 배정.”
수식:
새 customer 도착
↓
random_number = uniform(0, 1)
↓
if random_number < 0.5:
group = "control"
else:
group = "treatment"
그러나 실무에서는 두 가지 도전:
- Timing: 언제 배정할 것인가?
- Level: 누구 단위로 배정할 것인가?
두 결정이 실험의 외적·내적 타당도를 결정.
분석가가 처음 실험을 설계할 때 자연스러운 직감:
“Random uniform 으로 절반씩 나누면 끝. 통계 책에 그렇게 나옴.”
이론 책의 단순함:
- 표본은 명확
- 배정은 즉시
- 추적은 자명
실무의 현실:
- 누가 표본인가? (방문자? 회원? 결제자?)
- 언제 배정? (도착 시점? 결정 시점?)
- 같은 사람이 여러 번 방문하면? (한 번 배정 vs 매번?)
각 결정이 실험 결과를 바꿈. 이 결정들이 실험 설계의 80% 를 차지.
→ 이론은 5%, 실무가 95%. 분석가의 가치는 실무 결정에서 나옴.
2 Timing — 언제 배정?
2.1 문제 설정
A. 첫 페이지 도착 시 배정
B. 검색 결과 페이지 도착 시 배정
C. 호텔 상세 페이지 도착 시 배정
D. 예약 페이지 도착 시 배정 (← 1-click 버튼이 보이는 시점)
E. 결제 시작 시 배정
각 timing 의 영향:
- A: 모든 방문자 배정. 95% 가 예약 페이지 도달 안 함 → 효과 dilute
- D: 예약 페이지 도달자만 배정 → 모두 1-click 노출 가능
- E: 결제까지 간 사람만 → 표본 매우 작음, 효과 측정 어려움
→ 분석가의 결정이 실험의 검출력 결정.
2.2 Buisson 의 원칙
Buisson 의 권장:
“Production 에서 누가 treatment 를 받게 되나” 와 같게 배정.
AirCnC 사례:
- Production 에서 1-click 버튼은 예약 페이지에만 표시
- 따라서 실험에서도 예약 페이지 도달자만 배정 (Timing D)
이 일치가 실험의 외적 타당도 (external validity) 보장:
- 실험 효과 = production 효과 (외삽 정확)
- 다른 timing → 외삽 부정확
상상: 1-click 출시 후 production 에서 작동.
- 첫 페이지 방문자 100 만
- 검색 결과 도달 50 만
- 호텔 상세 도달 20 만
- 예약 페이지 도달 5 만 ← 여기서 1-click 보임
- 결제 완료 1 만
Production 에서 1-click 노출자 = 5 만 명 (예약 페이지 도달자).
만약 실험에서 첫 페이지 방문자 (100 만) 배정 시:
- 그 100 만 중 5 만만 진짜 효과 노출
- 나머지 95 만은 효과 측정에 noise 추가
- 실험 결과 = 5/100 의 효과 측정 → 큰 표본 필요 (또는 효과 dilute)
만약 실험에서 예약 페이지 도달자 (5 만) 배정 시:
- 5 만 모두 진짜 효과 노출
- 효과 측정 정확
→ 효율 차이 20 배 (100/5).
실험 효율의 비용 계산:
| Timing | 배정 표본 | 노출 표본 | 효과 측정 효율 |
|---|---|---|---|
| A (첫 페이지) | 100 만 | 5 만 | 5% (95% noise) |
| B (검색 결과) | 50 만 | 5 만 | 10% |
| C (호텔 상세) | 20 만 | 5 만 | 25% |
| D (예약 페이지) | 5 만 | 5 만 | 100% |
| E (결제 시작) | 2 만 | 2 만 | 100% (그러나 더 작음) |
같은 효과 검출에 필요한 표본:
- D: 5 만 → 1 주 실험
- A: 100 만 → 4 주 실험 (× 4 시간)
→ 잘못된 timing 이 실험 시간을 4 배 늘림. 비즈니스 비용 큼.
2.3 누구를 포함하는가
분석가의 자문:
“1-click 출시되면 누가 그것을 보게 될까?”
답:
- 예약 페이지 도달자 — Yes, 봄
- 첫 페이지에서 이탈한 사람 — No, 안 봄
- 검색만 하고 떠난 사람 — No, 안 봄
- 결제 진행 후 취소한 사람 — Yes, 봄 (예약 페이지는 거침)
- 모바일 앱 사용자 — 모바일에 1-click 있으면 Yes, 아니면 No
각 사용자의 경로를 추적 → production 노출 여부 결정 → 실험 표본 정의.
모든 사이트 방문자
↓
첫 페이지 → [이탈자 제외]
↓
검색 진행 → [검색만 한 자 제외]
↓
호텔 상세 → [상세만 본 자 제외]
↓
예약 페이지 도달 ← **여기서 배정**
↓
1-click 또는 기존 버튼 노출
↓
예약 완료 또는 이탈
배정 화살표가 정확히 1-click 노출 시점에 위치 → production 과 일치.
2.4 미래 promotion 고려
Buisson 의 미묘한 지적:
“미래에 적용될 promotion 은 1-click 위에 layered 됨. 따라서 promotion 받는 customer 도 실험에 포함.”
비유: 새 도로 건설 후 그 위에 표지판 추가. 도로 + 표지판 모두 같은 차량에게 영향.
AirCnC 사례:
- 1-click 출시 후 X 월에 promotion (할인 코드) 시작
- Promotion 받는 customer 도 1-click 페이지 통과
- 따라서 실험 표본 = 모든 예약 페이지 도달자 (promotion 무관)
이 일관성이 실험의 generalizability 보장.
3 Level — 누구 단위?
3.1 4 가지 후보
| Level | 의미 | Timing |
|---|---|---|
| Visit | 방문 1 회마다 새 배정 | 매 방문 |
| Booking | 예약 시도 1 건마다 | 매 예약 |
| Customer | 한 계정에 한 배정 (영구) | 첫 도달 시 1 회 |
| Person | 가족 계정 분리 | 첫 도달 시 1 회 |
각 level 의 trade-off:
- Visit: 표본 크지만 일관성 ↓ (한 사람 control + treatment)
- Customer: 표본 작지만 일관성 ↑
3.2 Visit-Level 의 함정
Visit-level 시나리오:
- John 이 월요일 방문 → Control (1-click 안 보임, 기존 버튼만)
- John 이 화요일 방문 → Treatment (1-click 보임)
- John 이 수요일 방문 → Control (1-click 안 보임)
문제:
- 사용자 혼란: John 이 “어제는 1-click 있었는데?” 의문
- 통계적 가정 위반: 같은 사람의 여러 방문은 독립적이지 않음
- 노출 효과: 1-click 한 번 본 사람이 다음 방문에 그 메모리 유지 → 다음 visit 의 conversion 증가 (control 그룹인데도)
이 contamination 이 실험 결과 흐림.
Visit-level 이 OK 한 경우:
- 한 번 방문이 평균 (예: 광고 이미지 변경 — 한 번 보면 끝)
- 사용자가 같은 사람인지 식별 안 됨 (anonymous)
- 효과가 즉시 (한 visit 내 결정)
비즈니스 사례:
- 광고 banner A/B (1 회 노출 평균)
- 검색 결과 정렬 A/B (한 검색 내 결정)
이런 경우 visit-level 이 자연스러움.
3.3 Customer-Level 의 권장
Buisson 의 권장:
“사람에 가장 가까운 level 에서 배정. 한 사람은 실험 기간 내내 같은 그룹.”
이유:
- 인간의 기억: 사람은 과거 경험을 기억. 다른 그룹 노출 = 혼란.
- 노출 일관성: 한 사람이 1-click 만 (또는 기존 만) 경험 → 그 효과의 진짜 측정.
- 장기 효과: 학습·습관 형성 (1-click 에 익숙해짐) → customer-level 만 측정 가능.
AirCnC 사례:
- John 의 모든 방문 → Control 또는 Treatment 중 하나 (영구)
- 일관된 경험 → John 의 booking 행동 변화의 정확한 attribution
Netflix 의 사례:
- 한 가구 (account) 안에 여러 사용자 (subaccount)
- 각 사용자가 별도 시청 행동
- 실험 배정도 person-level (subaccount)
이 정밀함이 가능한 이유:
- Netflix 의 추적 시스템이 person 식별 가능
- 한 가구의 여러 사람이 서로 다른 그룹 OK (서로 시청 안 영향)
→ Person-level 이 customer-level 보다 정밀. 그러나 추적 어려움.
비즈니스 분석에서 default = customer-level. Person-level 은 데이터가 허용할 때.
3.5 Sample Size 와 Level 의 관계
“배정 level 이 sample size 결정의 level 이어야 한다.”
Customer-level 배정 + Visit-level sample size 계산 = 잘못.
예:
- 평균 customer 가 월 3 visit
- Visit-level sample size = 30,000
- 잘못 결론: “Customer 10,000 명만 있으면 됨” (= 30,000 visit / 3)
- 실제로는: customer 30,000 명 필요 (각 customer 가 1 unit)
이유:
- Visit 단위로 통계 가정 (독립 관측) 위반
- Effective sample size 가 visit 보다 작음 (intra-customer 상관)
이 함정이 흔함. Sample size 와 배정 level 일치 필수.
Customer 30,000 명, 각자 평균 3 visit = visit 90,000.
Visit 단위의 통계 검정력:
- 만약 한 customer 의 3 visit 가 완전 독립 (다른 사람처럼) → sample size = 90,000
- 만약 완전 종속 (하나만 의미) → sample size = 30,000
- 현실은 그 사이 (intra-customer 상관 ρ)
Effective sample size:
\[ n_{\text{eff}} = \frac{n_{\text{total}}}{1 + (m - 1) \cdot \rho} \]
여기서 \(m\) = visits per customer, \(\rho\) = intra-customer 상관.
ρ = 0.5, m = 3 → \(n_{\text{eff}} = 90000 / (1 + 2 \cdot 0.5) = 45000\).
→ 90,000 visit 의 effective 가 45,000. Customer 단위 통계는 30,000 (보수적).
→ Customer-level 통계가 가장 안전.
4 A/A Test — 시스템 검증
4.1 A/A Test 의 목적
A/A Test: 두 그룹에 같은 (control) 버전 노출. 통계적 차이가 없어야 함.
목적:
- 배정 시스템의 무작위성 검증
- Logging 시스템의 정확성
- 통계적 분석의 baseline 잡음 측정
가능한 시스템 버그:
| 버그 | 증상 | A/A 발견 |
|---|---|---|
| 배정 알고리즘 편향 | 한 그룹에 특정 customer 집중 | 표본 수 차이 |
| Logging 누락 | 한 그룹의 일부 metric 안 기록 | metric 평균 차이 |
| 시간 효과 | 두 그룹이 다른 시간대에 노출 | 시간 패턴 차이 |
| Bot 트래픽 | 한 그룹에 bot 집중 | 평균이 한쪽으로 치우침 |
A/A test 가 5%~10% 차이 보이면 → 시스템 점검 필수.
A/A test 가 정상 → 본 실험 진행 OK.
→ 검증 안 한 시스템에서 실험 결과는 의심.
4.2 A/A Test 절차
1. 두 그룹 (A, A') 무작위 배정 — 동일 노출
2. 1~2 주 (정상 실험의 1/4 정도) 운영
3. 두 그룹의 baseline metric 비교:
- 표본 수 (Sample Ratio Mismatch SRM)
- Metric 평균
- Metric 분포
4. 통계 검정:
- SRM: chi-square test
- 평균: t-test
- 분포: Kolmogorov-Smirnov
5. 모두 통과하면 본 실험 진행 OK
만약 SRM 발견 → 즉시 중단, 시스템 점검.
배정이 50:50 인데 실제 표본이 49:51 또는 더 어긋나면 시스템 문제.
원인:
- Bot 트래픽
- 배정 후 일부 customer 필터링 (한쪽 그룹만)
- Logging 누락 (한 그룹만)
- Caching 문제
SRM 무시하면 실험 결과 신뢰 불가. Microsoft 의 KDD 논문 (Fabijan et al., 2019) 의 1 순위 검증.
4.3 A/A Test 시뮬레이션 코드
import numpy as np
import pandas as pd
from scipy.stats import ttest_ind, chi2_contingency
def aa_test_check(group_a, group_b, metric_col):
"""A/A Test 의 자동 검증."""
n_a = len(group_a)
n_b = len(group_b)
# 1. SRM 검정
expected = (n_a + n_b) / 2
chi2_stat = (n_a - expected)**2 / expected + (n_b - expected)**2 / expected
from scipy.stats import chi2
srm_pvalue = 1 - chi2.cdf(chi2_stat, df=1)
# 2. 평균 비교 (t-test)
t_stat, t_pvalue = ttest_ind(group_a[metric_col], group_b[metric_col])
return {
"n_a": n_a,
"n_b": n_b,
"srm_pvalue": srm_pvalue,
"t_pvalue": t_pvalue,
"srm_pass": srm_pvalue > 0.01, # 99% 신뢰
"t_pass": t_pvalue > 0.05,
}
# 시뮬레이션
np.random.seed(42)
n = 5000
group_a = pd.DataFrame({"booking_prob": np.random.binomial(1, 0.25, n)})
group_b = pd.DataFrame({"booking_prob": np.random.binomial(1, 0.25, n)})
result = aa_test_check(group_a, group_b, "booking_prob")
print("=== A/A Test 결과 ===")
print(f" N_A = {result['n_a']}, N_B = {result['n_b']}")
print(f" SRM p-value: {result['srm_pvalue']:.4f} (pass: {result['srm_pass']})")
print(f" t-test p-value: {result['t_pvalue']:.4f} (pass: {result['t_pass']})")
if result["srm_pass"] and result["t_pass"]:
print("\n→ 시스템 OK. 본 실험 진행 가능.")
else:
print("\n→ 시스템 점검 필요.")“본 실험 시작 전 항상 A/A test 1~2 주.”
이 1~2 주의 비용 (실험 지연):
- 본 실험 5 주 → A/A 추가 1 주 = 총 6 주
- 6 주 vs 사고 후 재실험 = 12 주
→ 사전 검증의 1 주가 사후 재실험의 6 주 절약.
비즈니스 분석에서 A/A test 가 default 가 되어야 하는 이유.
5 B.E.A.N. — Sample Size 의 4 가지 입력
Sample Size 결정에 필요한 4 가지 (Buisson, 2021, Ch.8):
- Beta (β): False negative 비율 (= 1 - power)
- Effect size: 검출하고 싶은 효과 (MDE)
- Alpha (α): False positive 비율 (statistical significance)
- Number: 표본 크기 (sample size)
이 4 가지가 서로 trade-off. 3 개 정하면 4 번째 결정.
분석가 default:
- α = 0.05 (또는 0.10 for 비즈니스)
- 1 - β = 0.80 (또는 0.90)
- MDE = 비즈니스 결정 (손익분기)
- N = 계산
5.1 Power 의 해석
흔한 오해: “내 실험의 power 가 80%.”
정확한 의미: “특정 효과 크기 하 의 power 가 80%.”
같은 실험이:
- 2% 효과: power 90%
- 1% 효과: power 80% (target)
- 0.5% 효과: power 50%
- 0% 효과: power = α = 5%
→ Power 는 효과 크기에 따라 다름.
분석가가 power 80% 라고 보고 시 효과 크기 명시 필수:
“1% 효과 검출 시 power 80%.”
5.2 Significance 의 비대칭
전통 α = 0.05 의 의미:
- 진짜 효과 0 일 때 false positive 비율 5%
- 100 번 실험 중 5 번 가짜 양성
비즈니스 함의:
- α = 0.05 → control 이 default, treatment 가 5% 확률로 가짜로 선택됨
- 비대칭: control 의 reject 는 더 보수적 (95% 신뢰)
만약 두 안이 동등 비교 (예: 마케팅 이메일 A vs B):
- α = 0.05 가 한쪽에 부담 → 비대칭 잘못
- α = 0.10 또는 0.20 으로 완화 가능
만약 long-running 검증된 control vs new treatment:
- Control 이 매우 좋음 (검증됨)
- α = 0.01 로 더 엄격
- “Control 을 가짜로 abandon” 의 비용 큼
→ α 는 실험 맥락에 따라. 0.05 를 default 로 받아들이지 말고 비즈니스 비용 분석.
5.3 Testing Velocity
Buisson 의 실용적 지적:
“조직이 1 년에 몇 개 실험 가능한가? 그게 sample size 결정의 숨은 변수.”
시나리오 1: 조직이 1 년에 1 실험 가능
- 3 개월 실험 + 3 개월 분석 + 6 개월 다른 일?
- 차라리 6 개월 실험으로 매우 정확하게
시나리오 2: 조직이 1 주에 1 실험 가능
- 3 개월 실험 = 12 개의 다른 실험 기회 손실
- 작은 power 라도 빠르게 + 다음 실험 진행
- 12 번의 시도 = 빅 효과 발견 가능
→ Testing velocity 가 power 의 적정값 결정. 단순 80% convention 따르지 말 것.
분석가의 sanity check:
“이 실험의 기간 vs 조직의 1 년 실험 capacity” “절대적 답이 아닌 상대적 ROI”
6 Random Assignment 의 종합 절차
1. ToC 명확화 (Ch.8.1)
↓
2. Treatment 노출 시점 식별 (production 의 mirror)
↓
3. 배정 timing 결정 (해당 시점에 도달자만)
↓
4. 배정 level 결정 (default: customer)
↓
5. Hash-based 배정 시스템 구현
↓
6. A/A Test 1~2 주 (검증)
↓
7. SRM 등 통과 시 본 실험 진행
↓
8. 본 실험 + 분석 (Sample size 분석은 다음 글)
각 단계가 다음 단계의 전제. Skip 하면 결과 신뢰성 ↓.
7 코드 예시 — 배정 시스템 구현
7.1 Hash-Based 배정
import hashlib
import numpy as np
import pandas as pd
def hash_assign(customer_id, experiment_id, n_groups=2, salt=""):
"""
Hash-based 결정론적 배정.
같은 customer_id + experiment_id 는 항상 같은 그룹 반환.
"""
seed = f"{customer_id}_{experiment_id}_{salt}"
h = int(hashlib.md5(seed.encode()).hexdigest(), 16)
return h % n_groups
# 시뮬레이션
np.random.seed(42)
customers = pd.DataFrame({"customer_id": [f"user_{i}" for i in range(10000)]})
customers["group"] = customers["customer_id"].apply(
lambda x: hash_assign(x, "1click_test")
)
customers["group_label"] = customers["group"].map({0: "control", 1: "treatment"})
print("=== Hash 배정 결과 ===")
print(customers["group_label"].value_counts())
# 같은 customer 의 재배정 일관성
print(f"\nuser_0 첫 호출: {hash_assign('user_0', '1click_test')}")
print(f"user_0 두번째 호출: {hash_assign('user_0', '1click_test')}")
print(f"user_0 세번째 호출: {hash_assign('user_0', '1click_test')}")- 결정론적: 같은 input → 같은 output
- 재현 가능: 시스템 재시작해도 동일 그룹
- 분산 가능: 여러 서버에서 일관된 배정
- 빠름: O(1) 계산
- Salt 추가: 같은 customer 가 다른 실험에서 다른 그룹 (실험 독립성)
이 5 장점이 production 시스템의 표준.
7.2 A/A Test 실행
# 1-2 주 A/A 시뮬레이션
np.random.seed(42)
n_a = 5000
n_b = 5000
# 두 그룹 동일 분포 (control 만 노출)
booking_prob_a = np.random.binomial(1, 0.25, n_a)
booking_prob_b = np.random.binomial(1, 0.25, n_b)
# 시간대별 노출 점검
hour_a = np.random.choice(24, n_a)
hour_b = np.random.choice(24, n_b)
aa_df_a = pd.DataFrame({"booked": booking_prob_a, "hour": hour_a})
aa_df_b = pd.DataFrame({"booked": booking_prob_b, "hour": hour_b})
# 검증
print("=== A/A Test 종합 검증 ===")
# 1. SRM
print(f"\n1. SRM 점검:")
print(f" N_A = {len(aa_df_a)}, N_B = {len(aa_df_b)}")
print(f" 비율: {len(aa_df_a) / (len(aa_df_a) + len(aa_df_b)):.4f} : {len(aa_df_b) / (len(aa_df_a) + len(aa_df_b)):.4f}")
# 2. Metric 비교
from scipy.stats import ttest_ind
t_stat, t_pval = ttest_ind(aa_df_a["booked"], aa_df_b["booked"])
print(f"\n2. Booking probability 비교:")
print(f" A 평균: {aa_df_a['booked'].mean():.4f}")
print(f" B 평균: {aa_df_b['booked'].mean():.4f}")
print(f" t-test p-value: {t_pval:.4f} ({'pass' if t_pval > 0.05 else 'FAIL'})")
# 3. 시간 분포 점검 (Kolmogorov-Smirnov)
from scipy.stats import ks_2samp
ks_stat, ks_pval = ks_2samp(aa_df_a["hour"], aa_df_b["hour"])
print(f"\n3. 시간 분포 비교:")
print(f" KS p-value: {ks_pval:.4f} ({'pass' if ks_pval > 0.05 else 'FAIL'})")
if t_pval > 0.05 and ks_pval > 0.05:
print("\n→ A/A 통과. 본 실험 진행 OK.")
else:
print("\n→ 시스템 점검 필요.")이 코드가 시스템에 통합되면:
- 실험 시작 전 자동 A/A 1~2 주
- 통과 시 본 실험 자동 시작
- 실패 시 alert + 사람 점검
비즈니스 분석가의 작업 절약 + 신뢰성 확보.
→ A/A 는 분석가의 sanity check. 자동화로 부담 없음.
7.3 실제 실험 배정 시뮬레이션
def simulate_experiment(n_customers=10000, treatment_effect=0.04, baseline=0.25,
seed=42):
"""
Customer-level 배정 + 효과 시뮬레이션.
"""
np.random.seed(seed)
customers = pd.DataFrame({
"customer_id": [f"user_{i}" for i in range(n_customers)],
})
# Hash 배정
customers["group"] = customers["customer_id"].apply(
lambda x: "treatment" if hash_assign(x, "exp1") == 1 else "control"
)
# 효과 시뮬레이션
customers["true_prob"] = np.where(
customers["group"] == "treatment",
baseline + treatment_effect,
baseline,
)
customers["booked"] = np.random.binomial(1, customers["true_prob"])
return customers
# 실행
exp = simulate_experiment(n_customers=10000, treatment_effect=0.04)
print("\n=== 실험 결과 ===")
print(exp.groupby("group").agg(
n=("customer_id", "count"),
booking_rate=("booked", "mean"),
))
# 효과 추정
control_rate = exp[exp["group"] == "control"]["booked"].mean()
treatment_rate = exp[exp["group"] == "treatment"]["booked"].mean()
diff = treatment_rate - control_rate
print(f"\n관측된 차이: {diff:.4f} (진짜 효과 0.04)")이 시뮬레이션이 보여주는 것:
- Hash 배정: 50:50 가까운 분포
- 효과 검출: 진짜 효과 4%p 의 추정
- 잡음: 추정 차이가 4%p 와 약간 다름 (표본 잡음)
- 표본 효율: 10,000 명 → 효과 명확
이 함수가 분석가의 실험 시뮬레이션 도구. 본 실험 전 power 점검 가능.
8 관련 주제
8.1 Ch.8 의 형제 글
- E-BUI8-0 Theory of Change overview — Ch.8 전체 흐름
- E-BUI8-1 변화 이론과 목표 지표 — 단계 1
- E-BUI8-3 검정력 분석 (부트스트랩) — 단계 3
8.2 Phase F (Kohavi A/B Testing) cross-link
- Phase F — A/A Test, SRM 의 정통 처리
8.3 카테고리 진입점
- Experimentation 학습 로드맵 — 11 Phase × 7 교재 매핑