A/B 테스트 분석 실습: 통계적 검정으로 데이터 기반 의사결정 내리기

들어가며

앞선 4편까지 EDA, 머신러닝 파이프라인, 시계열 예측까지 다뤘다면, 이제는 실무에서 가장 많이 마주치는 의사결정 상황인 A/B 테스트를 다룰 차례입니다. 신규 기능을 배포할지, 광고 문구를 바꿀지, UI 디자인을 변경할지 등 데이터로 검증해야 하는 순간, A/B 테스트가 등장합니다.

이번 편에서는 Kaggle의 Mobile Games A/B Testing 데이터셋을 활용해 통계적 검정의 전체 프로세스를 실습하고, 실무에서 주의해야 할 함정들을 살펴보겠습니다.

A/B 테스트란 무엇인가

A/B 테스트는 두 가지 버전(A: Control, B: Treatment)을 무작위로 나눠 보여주고, 통계적으로 유의미한 차이가 있는지 검증하는 실험 방법입니다.

핵심 개념

용어 설명
귀무가설(H0) A와 B는 차이가 없다
대립가설(H1) A와 B는 차이가 있다
p-value 귀무가설이 참일 때 현재 결과가 나올 확률 (보통 0.05 기준)
검정력(Power) 실제 차이가 있을 때 이를 감지하는 능력 (보통 0.8 목표)
표본 크기 충분한 검정력을 위해 필요한 최소 데이터 수

실습 1: 데이터 로드 및 EDA

import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns

# 데이터 로드
df = pd.read_csv('cookie_cats.csv')

print(df.head())
print(df.info())
print(df['version'].value_counts())

데이터 구조 확인

  • userid: 고유 사용자 ID
  • version: gate_30 (Control) vs gate_40 (Treatment)
  • sum_gamerounds: 설치 후 플레이한 총 라운드 수
  • retention_1: 설치 1일 후 재방문 여부 (True/False)
  • retention_7: 설치 7일 후 재방문 여부

핵심 질문: 게임의 첫 번째 장애물(gate)을 레벨 30에서 40으로 이동시키면 유저 리텐션이 개선될까?

# 그룹별 기초 통계
df.groupby('version').agg({
    'sum_gamerounds': ['mean', 'median', 'std'],
    'retention_1': 'mean',
    'retention_7': 'mean'
})

실습 2: 리텐션 A/B 테스트 (비율 검정)

1일 리텐션 비교

# Control vs Treatment 분리
control = df[df['version'] == 'gate_30']['retention_1']
treatment = df[df['version'] == 'gate_40']['retention_1']

# 비율 계산
control_rate = control.sum() / len(control)
treatment_rate = treatment.sum() / len(treatment)

print(f"Control 리텐션: {control_rate:.4f}")
print(f"Treatment 리텐션: {treatment_rate:.4f}")
print(f"차이: {(treatment_rate - control_rate) * 100:.2f}%p")

카이제곱 검정 (Chi-square Test)

from scipy.stats import chi2_contingency

# 분할표 생성
contingency_table = pd.crosstab(
    df['version'], 
    df['retention_1']
)

chi2, p_value, dof, expected = chi2_contingency(contingency_table)

print(f"Chi-square: {chi2:.4f}")
print(f"p-value: {p_value:.4f}")
print("결론:", "유의미한 차이 있음" if p_value < 0.05 else "차이 없음")

실무 팁: 비율 검정 시 표본 크기가 충분한지 확인하세요. 각 셀의 기대빈도가 5 이상이어야 카이제곱 검정이 유효합니다.

실습 3: 게임 라운드 수 비교 (t-test)

# 플레이 라운드 분포 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

for ax, version in zip(axes, ['gate_30', 'gate_40']):
    data = df[df['version'] == version]['sum_gamerounds']
    ax.hist(data, bins=50, edgecolor='black', alpha=0.7)
    ax.set_title(f'{version} 라운드 분포')
    ax.set_xlabel('게임 라운드 수')
    ax.set_xlim(0, 200)

plt.tight_layout()
plt.show()

t-test 수행

control_rounds = df[df['version'] == 'gate_30']['sum_gamerounds']
treatment_rounds = df[df['version'] == 'gate_40']['sum_gamerounds']

# Welch's t-test (등분산 가정 불필요)
t_stat, p_value = stats.ttest_ind(
    control_rounds, 
    treatment_rounds, 
    equal_var=False
)

print(f"t-statistic: {t_stat:.4f}")
print(f"p-value: {p_value:.4f}")

이상치 처리 후 재검정

# 99 백분위수 이상 제거
threshold = df['sum_gamerounds'].quantile(0.99)
df_filtered = df[df['sum_gamerounds'] <= threshold]

# 재검정
control_f = df_filtered[df_filtered['version'] == 'gate_30']['sum_gamerounds']
treatment_f = df_filtered[df_filtered['version'] == 'gate_40']['sum_gamerounds']

t_stat_f, p_value_f = stats.ttest_ind(control_f, treatment_f, equal_var=False)
print(f"필터링 후 p-value: {p_value_f:.4f}")

실습 4: 부트스트랩 신뢰구간

통계적 검정 외에도 효과 크기의 불확실성을 표현하는 것이 중요합니다.

def bootstrap_diff(control, treatment, n_bootstrap=10000):
    diffs = []
    for _ in range(n_bootstrap):
        c_sample = np.random.choice(control, size=len(control), replace=True)
        t_sample = np.random.choice(treatment, size=len(treatment), replace=True)
        diffs.append(t_sample.mean() - c_sample.mean())
    return np.array(diffs)

# 부트스트랩 실행
boot_diffs = bootstrap_diff(
    control_rounds.values, 
    treatment_rounds.values
)

# 95% 신뢰구간
ci_lower = np.percentile(boot_diffs, 2.5)
ci_upper = np.percentile(boot_diffs, 97.5)

print(f"리텐션 차이 95% CI: [{ci_lower:.4f}, {ci_upper:.4f}]")

# 시각화
plt.hist(boot_diffs, bins=50, edgecolor='black', alpha=0.7)
plt.axvline(ci_lower, color='red', linestyle='--', label='95% CI')
plt.axvline(ci_upper, color='red', linestyle='--')
plt.axvline(0, color='black', linestyle='-', label='차이 없음')
plt.legend()
plt.xlabel('리텐션 차이')
plt.title('부트스트랩 신뢰구간')
plt.show()

실무에서 주의할 점

1. 다중 비교 문제 (Multiple Testing)

여러 지표를 동시에 검정하면 우연히 유의한 결과가 나올 확률이 높아집니다.

# Bonferroni 보정
alpha = 0.05
n_tests = 3  # retention_1, retention_7, sum_gamerounds
adjusted_alpha = alpha / n_tests
print(f"보정된 유의수준: {adjusted_alpha:.4f}")

2. 표본 크기 계산

실험 전에 필요한 표본 크기를 계산하세요.

from statsmodels.stats.power import zt_ind_solve_power

# 효과 크기 0.02 (2%p 차이), 검정력 0.8
required_n = zt_ind_solve_power(
    effect_size=0.02, 
    alpha=0.05, 
    power=0.8
)
print(f"그룹당 필요 표본: {required_n:.0f}명")

3. 조기 종료의 위험

실험 중간에 p-value를 확인하고 멈추면 1종 오류율이 증가합니다. Sequential Testing 기법을 사용하거나, 사전에 정한 종료 시점을 지키세요.

4. 실무 의사결정 프레임워크

단계 체크리스트
설계 가설 명확화, KPI 선정, 표본 크기 계산
실행 무작위 배정 확인, AA 테스트로 시스템 검증
분석 이상치 처리, 다중 비교 보정, 신뢰구간 계산
해석 통계적 유의성 + 실무적 의미(Effect Size)

마무리

이번 편에서는 A/B 테스트의 전체 파이프라인을 실습했습니다. 단순히 p-value만 보는 것이 아니라, 효과 크기, 신뢰구간, 실무적 의미를 종합적으로 판단하는 것이 진짜 데이터 기반 의사결정입니다.

다음 6편에서는 지금까지 진행한 모든 프로젝트를 포트폴리오로 정리하는 방법과, 채용 담당자가 주목하는 어필 포인트를 다루겠습니다. 취업 준비의 마지막 퍼즐 조각을 함께 맞춰봅시다!

데이터분석가를 꿈꾸는 취준생을 위한 실제 업무와 유사한 kaggle데이터를 활용한 실무데이터분석 프로젝트 시리즈 (5/6편)
데이터분석가를 꿈꾸는 취준생을 위한 실제 업무와 유사한 kaggle데이터를 활용한 실무데이터분석 프로젝트 시리즈 (5/6편)

이 글이 도움이 되셨나요?

Buy me a coffee

코멘트

“A/B 테스트 분석 실습: 통계적 검정으로 데이터 기반 의사결정 내리기” 에 하나의 답글

  1. […] 데이터분석가를 꿈꾸는 취준생을 위한 실제 업무와 유사한 kaggle데이터를 활용한 실무데이터분석 프로젝트 시리즈 (6/6편) ← 이전: A/B 테스트 분석 실습: 통계적 검정으로 데이터 기반 의사결정 내… […]

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

TODAY 136 | TOTAL 136