콘텐츠로 이동

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억 원


주의사항

일반적인 함정

  1. Peeking 문제
  2. 중간에 결과 보고 조기 종료 → 거짓 양성 증가
  3. 해결: 사전에 정한 샘플/기간 채우기, Sequential Testing 사용

  4. 다중 비교 문제

  5. 여러 지표 동시 검증 → 우연히 유의한 결과 나옴
  6. 해결: 주 지표 1개 사전 지정, Bonferroni 보정

  7. 샘플 오염

  8. 한 유저가 A/B 모두 경험 → 효과 희석
  9. 해결: 유저 단위 배정, 쿠키/로그인 기반 고정

  10. 외부 효과

  11. 실험 기간 중 프로모션, 시즌 효과 → 결과 왜곡
  12. 해결: 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% ← 효과 작음
# → 모바일 타겟 개선에 집중

상호작용 효과

# 이원 분산분석 (Two-way ANOVA)
import statsmodels.api as sm
from statsmodels.formula.api import ols

model = ols('conversion ~ C(variant) * C(device)', data=results).fit()
anova_table = sm.stats.anova_lm(model, typ=2)

# 상호작용 효과가 유의하면 → 디바이스별로 다른 전략 필요