A/B 테스트
A/B Testing
두 버전을 비교하는 실험 설계. 통계적 유의성 검증.
A/B Testing
두 버전을 비교하는 실험 설계. 통계적 유의성 검증.
A/B 테스트는 두 개의 버전(A: Control, B: Variant)을 동시에 실험하여 어떤 것이 더 나은 성과를 내는지 통계적으로 비교하는 방법론입니다. 사용자를 무작위로 두 그룹으로 나누어 각각 다른 버전을 보여주고, 전환율, 클릭률, 매출 등 핵심 지표의 차이를 측정합니다.
A/B 테스트의 기원은 1920년대 R.A. Fisher의 농업 통계 실험으로 거슬러 올라갑니다. 디지털 분야에서는 2000년 Google이 검색 결과 페이지에서 10개 vs 30개 결과 표시를 테스트하면서 본격화되었습니다. 현재 Amazon은 매일 수백 개의 A/B 테스트를 동시 운영하고, Booking.com은 연간 25,000개 이상의 실험을 수행합니다.
A/B 테스트의 핵심은 귀무가설(H0: 두 버전 간 차이 없음)을 검정하는 것입니다. t-test 또는 카이제곱 검정으로 p-value를 계산하고, 일반적으로 p < 0.05일 때 "통계적으로 유의미하다"고 판단합니다. 검정력(Power)은 보통 80% 이상을 목표로 하며, 이를 달성하기 위한 최소 샘플 사이즈를 사전에 계산해야 합니다.
실무에서 A/B 테스트는 직관과 의견 대신 데이터로 의사결정하는 문화의 기반입니다. "HiPPO(Highest Paid Person's Opinion)" 대신 실험 결과로 결정하면 실패 비용을 줄이고, 작은 개선이라도 누적되면 큰 비즈니스 임팩트를 만들 수 있습니다. Microsoft의 한 실험에서 검색 결과 지연을 100ms 늘렸더니 수익이 1% 감소한 사례는 A/B 테스트의 민감도를 보여줍니다.
import numpy as np
from scipy import stats
import math
def calculate_sample_size(baseline_rate, mde, alpha=0.05, power=0.8):
"""
A/B 테스트에 필요한 최소 샘플 사이즈 계산
Args:
baseline_rate: 기존 전환율 (예: 0.10 = 10%)
mde: 최소 탐지 효과 크기 (예: 0.02 = 2%p 상승)
alpha: 유의수준 (기본 0.05)
power: 검정력 (기본 0.80)
"""
p1 = baseline_rate
p2 = baseline_rate + mde
pooled_p = (p1 + p2) / 2
z_alpha = stats.norm.ppf(1 - alpha / 2) # 양측검정
z_beta = stats.norm.ppf(power)
numerator = (z_alpha * math.sqrt(2 * pooled_p * (1 - pooled_p)) +
z_beta * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2
denominator = (p2 - p1) ** 2
n_per_group = math.ceil(numerator / denominator)
return n_per_group
def ab_test_analysis(control_conversions, control_total,
variant_conversions, variant_total):
"""A/B 테스트 결과 분석"""
# 전환율 계산
control_rate = control_conversions / control_total
variant_rate = variant_conversions / variant_total
# 상대적 상승률
lift = (variant_rate - control_rate) / control_rate * 100
# Z-검정 (2-proportion z-test)
pooled_rate = (control_conversions + variant_conversions) / (control_total + variant_total)
se = math.sqrt(pooled_rate * (1 - pooled_rate) * (1/control_total + 1/variant_total))
z_score = (variant_rate - control_rate) / se
p_value = 2 * (1 - stats.norm.cdf(abs(z_score))) # 양측검정
# 신뢰구간 (95%)
se_diff = math.sqrt(control_rate*(1-control_rate)/control_total +
variant_rate*(1-variant_rate)/variant_total)
ci_lower = (variant_rate - control_rate) - 1.96 * se_diff
ci_upper = (variant_rate - control_rate) + 1.96 * se_diff
return {
"control_rate": f"{control_rate:.2%}",
"variant_rate": f"{variant_rate:.2%}",
"lift": f"{lift:+.1f}%",
"p_value": p_value,
"significant": p_value < 0.05,
"confidence_interval": f"[{ci_lower:+.2%}, {ci_upper:+.2%}]"
}
# 사용 예시: 체크아웃 버튼 색상 테스트
sample_size = calculate_sample_size(baseline_rate=0.10, mde=0.01)
print(f"필요 샘플 사이즈 (그룹당): {sample_size:,}명")
# 실제 실험 결과 분석
results = ab_test_analysis(
control_conversions=520, control_total=5000, # 10.4%
variant_conversions=580, variant_total=5000 # 11.6%
)
print(f"\nControl 전환율: {results['control_rate']}")
print(f"Variant 전환율: {results['variant_rate']}")
print(f"상승률: {results['lift']}")
print(f"p-value: {results['p_value']:.4f}")
print(f"통계적 유의미: {'예' if results['significant'] else '아니오'}")
print(f"95% 신뢰구간: {results['confidence_interval']}")
"랜딩페이지 A/B 테스트 결과가 나왔습니다. 새로운 헤드라인으로 전환율이 8.2%에서 9.7%로 올랐고, p-value가 0.023이라 통계적으로 유의미합니다. 다음 주부터 승자 버전으로 100% 트래픽을 돌려도 될까요?"
"A/B 테스트에서 Type I Error는 실제로 차이가 없는데 있다고 판단하는 거짓 양성이고, Type II Error는 차이가 있는데 없다고 판단하는 거짓 음성입니다. 보통 Type I Error를 5%로 제한하고, Type II Error를 20% 이하로 유지해서 검정력 80%를 확보합니다."
"지금 결과를 보면 Variant가 2% 더 좋아 보이지만, 아직 샘플 사이즈가 부족해서 신뢰구간이 0을 포함하고 있습니다. 최소 일주일 더 실험을 유지해서 MDE 1%를 탐지할 수 있는 충분한 데이터를 모아야 합니다."
여러 지표를 동시에 테스트하면 거짓 양성 확률이 증가합니다. 5개 지표를 테스트하면 거짓 양성 확률이 23%까지 올라갑니다. 주요 지표를 하나 정하거나 Bonferroni 보정을 적용하세요.
50:50으로 분배했는데 실제 비율이 48:52처럼 틀어지면 실험 인프라에 버그가 있다는 신호입니다. 결과를 분석하기 전에 반드시 SRM 체크를 해야 하며, 문제가 있으면 실험을 폐기해야 합니다.
휴일, 마케팅 캠페인, 시스템 장애 등 외부 요인이 결과를 왜곡할 수 있습니다. 실험 기간 중 주요 이벤트를 기록하고, 필요시 해당 기간 데이터를 제외하거나 세그먼트 분석을 수행하세요.