A/B 테스트¶
왜 이 분석을?¶
문제: "이 변경이 효과가 있을까?"에 대한 답을 모른다.
A/B 테스트는 인과관계를 검증하는 유일한 방법이다:
관찰: 버튼 색을 바꿨더니 전환율이 올랐다
질문: 버튼 색 때문인가? 시즌 효과인가? 우연인가?
A/B 테스트:
- A그룹 (기존 버튼): 전환율 3.2%
- B그룹 (새 버튼): 전환율 3.8%
- p-value < 0.05: 버튼 색이 원인
→ 통계적으로 "버튼 색이 전환율을 높였다" 검증
핵심: 상관관계가 아닌 인과관계를 확인해야 의사결정에 확신이 생긴다.
어떤 가설?¶
가설 설정 원칙¶
좋은 가설의 조건: 1. 구체적: "전환율이 오른다" (X) → "결제 전환율이 5% 이상 오른다" (O) 2. 측정 가능: 명확한 지표로 검증 가능 3. 행동 기반: 왜 그런 결과가 예상되는지 논리가 있음
가설 예시¶
| 가설 | 논리 | 검증 지표 |
|---|---|---|
| CTA 버튼을 눈에 띄게 하면 클릭률이 오른다 | 시각적 주목도 → 행동 유도 | 버튼 클릭률 |
| 배송비 무료 기준을 낮추면 객단가가 오른다 | 무료 배송 달성 심리 | 평균 주문 금액 |
| 리뷰를 상단에 배치하면 구매가 늘어난다 | 사회적 증거 → 신뢰 | 상품 상세 → 구매 전환율 |
| 결제 단계를 줄이면 결제 완료율이 오른다 | 마찰 감소 → 이탈 감소 | 결제 완료율 |
실험 설계¶
1. 샘플 크기 계산¶
from statsmodels.stats.power import TTestIndPower
# 파라미터 설정
baseline_conversion = 0.03 # 기존 전환율 3%
mde = 0.1 # 최소 감지 효과 (10% 상대 개선 = 3.3%)
alpha = 0.05 # 유의수준
power = 0.8 # 검정력
# Effect size 계산
baseline = 0.03
target = baseline * (1 + mde)
pooled_p = (baseline + target) / 2
effect_size = (target - baseline) / np.sqrt(pooled_p * (1 - pooled_p))
# 필요 샘플 크기
analysis = TTestIndPower()
sample_size = analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
ratio=1.0 # A:B 비율
)
print(f"그룹당 필요 샘플: {int(sample_size):,}명")
# 예: 그룹당 약 15,000명 필요
2. 실험 기간 계산¶
daily_traffic = 10000 # 일일 방문자
test_ratio = 0.5 # 전체 트래픽 중 실험 비율
required_per_group = 15000
days_needed = (required_per_group * 2) / (daily_traffic * test_ratio)
print(f"필요 기간: {int(days_needed)}일")
# 예: 6일 필요
3. 무작위 배정¶
import hashlib
def assign_variant(user_id, experiment_name, variants=['A', 'B']):
"""결정론적 무작위 배정"""
hash_input = f"{user_id}_{experiment_name}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
variant_index = hash_value % len(variants)
return variants[variant_index]
# 사용
user_variant = assign_variant("user_123", "button_color_test")
통계적 검증¶
1. 전환율 비교 (비율 검정)¶
from scipy import stats
# 데이터
a_visitors, a_conversions = 15000, 450 # 3.0%
b_visitors, b_conversions = 15000, 510 # 3.4%
# Z-검정
a_rate = a_conversions / a_visitors
b_rate = b_conversions / b_visitors
pooled_rate = (a_conversions + b_conversions) / (a_visitors + b_visitors)
se = np.sqrt(pooled_rate * (1 - pooled_rate) * (1/a_visitors + 1/b_visitors))
z_score = (b_rate - a_rate) / se
p_value = 1 - stats.norm.cdf(z_score)
print(f"A 전환율: {a_rate:.2%}")
print(f"B 전환율: {b_rate:.2%}")
print(f"상대 개선: {(b_rate - a_rate) / a_rate:.1%}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'유의함 (B 채택)' if p_value < 0.05 else '유의하지 않음'}")
2. 연속형 지표 비교 (평균 비교)¶
from scipy import stats
# 평균 주문 금액 비교
a_values = [...] # A그룹 주문 금액 리스트
b_values = [...] # B그룹 주문 금액 리스트
# t-검정
t_stat, p_value = stats.ttest_ind(a_values, b_values)
# 또는 Mann-Whitney U (비정규분포 시)
u_stat, p_value = stats.mannwhitneyu(a_values, b_values, alternative='two-sided')
3. 신뢰구간¶
# 전환율 차이의 95% 신뢰구간
diff = b_rate - a_rate
se_diff = np.sqrt(a_rate*(1-a_rate)/a_visitors + b_rate*(1-b_rate)/b_visitors)
ci_lower = diff - 1.96 * se_diff
ci_upper = diff + 1.96 * se_diff
print(f"전환율 차이: {diff:.2%} (95% CI: {ci_lower:.2%} ~ {ci_upper:.2%})")
비즈니스 액션¶
결과 해석¶
| 결과 | 해석 | 액션 |
|---|---|---|
| p < 0.05, 개선 | 통계적으로 유의한 개선 | B 전체 적용 |
| p < 0.05, 악화 | 통계적으로 유의한 악화 | A 유지, 원인 분석 |
| p >= 0.05 | 차이 없음 (또는 샘플 부족) | 샘플 늘리거나, 다른 가설 |
실제 적용 예시¶
문제: 장바구니 → 결제 전환율이 낮다 (60%)
가설: 배송비를 장바구니에서 미리 보여주면 결제 단계 이탈이 줄어든다
실험 설계: - A: 기존 (결제 단계에서 배송비 노출) - B: 장바구니에서 배송비 미리 노출 - 지표: 장바구니 → 결제 완료 전환율 - 기간: 2주 (그룹당 20,000명 목표)
결과:
그룹 A: 20,500명 중 12,300명 전환 (60.0%)
그룹 B: 20,200명 중 13,130명 전환 (65.0%)
상대 개선: +8.3%
p-value: 0.0001 (유의함)
95% CI: +3.2% ~ +6.8%
의사결정: - B안 전체 적용 - 예상 효과: 월 결제 완료 +8%, 월 매출 +약 5억 원
주의사항¶
일반적인 함정¶
- Peeking 문제
- 중간에 결과 보고 조기 종료 → 거짓 양성 증가
-
해결: 사전에 정한 샘플/기간 채우기, Sequential Testing 사용
-
다중 비교 문제
- 여러 지표 동시 검증 → 우연히 유의한 결과 나옴
-
해결: 주 지표 1개 사전 지정, Bonferroni 보정
-
샘플 오염
- 한 유저가 A/B 모두 경험 → 효과 희석
-
해결: 유저 단위 배정, 쿠키/로그인 기반 고정
-
외부 효과
- 실험 기간 중 프로모션, 시즌 효과 → 결과 왜곡
- 해결: A/B 동시 진행, 충분한 기간, 외부 요인 기록
통계적 함정¶
| 함정 | 문제 | 해결 |
|---|---|---|
| 낮은 검정력 | 효과 있어도 못 잡음 | 사전 샘플 크기 계산 |
| p-hacking | 데이터 보고 가설 변경 | 사전 등록 |
| 평균만 비교 | 분포 차이 무시 | 분포 시각화, 세그먼트별 분석 |
고급: 세그먼트 분석¶
# 세그먼트별 효과 차이 분석
segments = ['mobile', 'desktop', 'new_user', 'returning_user']
for seg in segments:
seg_a = results[(results['variant'] == 'A') & (results['segment'] == seg)]
seg_b = results[(results['variant'] == 'B') & (results['segment'] == seg)]
a_rate = seg_a['converted'].mean()
b_rate = seg_b['converted'].mean()
print(f"{seg}: A={a_rate:.2%}, B={b_rate:.2%}, 차이={b_rate-a_rate:.2%}")
# 결과 예시:
# mobile: A=2.5%, B=3.5%, 차이=+1.0% ← 효과 큼
# desktop: A=4.0%, B=4.2%, 차이=+0.2% ← 효과 작음
# → 모바일 타겟 개선에 집중