A/B 테스트 분석
온라인 쇼핑몰에서 "결제하기" 버튼 색상을 파란색에서 초록색으로 바꾸면 구매율이 높아질까요. 이메일 제목을 바꾸면 오픈율이 올라갈까요. 이런 질문에 데이터로 답하는 방법이 A/B 테스트입니다.
A/B 테스트의 핵심은 단순합니다. 사용자를 두 집단으로 나누고, 한 집단(대조군 A)에는 기존 버전을, 다른 집단(실험군 B)에는 새 버전을 보여준 뒤 지표를 비교합니다. "우연히 달라 보이는 것인가, 실제로 차이가 있는 것인가"를 통계로 판단하는 것이 전부입니다.
분석 목표와 가설 설정
이커머스 사이트에서 상품 상세 페이지의 CTA(Call-to-Action) 버튼 디자인을 변경했다고 가정합니다.
시나리오
- 대조군(A): 기존 파란색 "장바구니 담기" 버튼
- 실험군(B): 새 초록색 "지금 구매하기" 버튼
- 측정 지표: 전환율(클릭 후 구매 완료 비율)
가설
- H0: 두 버전의 전환율이 같다 (p_A = p_B).
- H1: 두 버전의 전환율이 다르다 (p_A ≠ p_B, 양측 검정).
유의수준은 α = 0.05, 검정력은 80%로 설정합니다.
표본 크기 계산 — 실험 전에 먼저
A/B 테스트에서 가장 많이 저지르는 실수는 "조금 모였으니 확인해보자"는 충동입니다. 충분한 표본 없이 중간 점검을 반복하면 1종 오류가 급격히 높아집니다. 실험 시작 전에 필요한 표본 크기를 반드시 계산해야 합니다.
# 표본 크기 계산
# 기존 전환율(대조군 기대값): 5%
# 탐지하고 싶은 최소 효과: 1.5%p 향상 → 6.5%
# 유의수준: 0.05, 검정력: 0.80
sample_calc <- power.prop.test(
p1 = 0.05, # 대조군 전환율
p2 = 0.065, # 실험군 목표 전환율
sig.level = 0.05,
power = 0.80,
alternative = "two.sided"
)
print(sample_calc)
cat("\n집단당 필요 표본 수:", ceiling(sample_calc$n), "명\n")
cat("총 필요 표본 수:", ceiling(sample_calc$n) * 2, "명\n")
ceiling()으로 반올림하는 것을 잊지 마세요. 사람 수는 소수점이 없습니다.
데이터 시뮬레이션
실험을 통해 수집된 데이터를 시뮬레이션합니다.
set.seed(2024)
# 집단당 표본 수 (계산 결과에 따라)
n_per_group <- ceiling(sample_calc$n)
# 대조군 A: 실제 전환율 5%
group_a_conversions <- rbinom(1, n_per_group, prob = 0.050)
# 실험군 B: 실제 전환율 6.8% (약간 높게 설정)
group_b_conversions <- rbinom(1, n_per_group, prob = 0.068)
# 데이터 정리
ab_data <- data.frame(
group = c("A (대조군)", "B (실험군)"),
n = c(n_per_group, n_per_group),
conversions = c(group_a_conversions, group_b_conversions)
) %>%
dplyr::mutate(
non_conversions = n - conversions,
rate = conversions / n
)
print(ab_data)
cat("\n대조군 전환율:", round(ab_data$rate[1] * 100, 2), "%\n")
cat("실험군 전환율:", round(ab_data$rate[2] * 100, 2), "%\n")
cat("절대 증가량:", round((ab_data$rate[2] - ab_data$rate[1]) * 100, 2), "%p\n")
cat("상대 증가량:", round((ab_data$rate[2] / ab_data$rate[1] - 1) * 100, 1), "%\n")
비율 비교 검정 — prop.test()
두 전환율의 차이가 통계적으로 유의미한지 검정합니다.
# 비율 검정 (2-proportion z-test)
prop_result <- prop.test(
x = ab_data$conversions,
n = ab_data$n,
alternative = "two.sided",
correct = FALSE # 연속성 수정 없이 (대표본)
)
print(prop_result)
# 결과 해석
cat("\n--- 검정 결과 요약 ---\n")
cat("검정통계량 (χ²):", round(prop_result$statistic, 4), "\n")
cat("p값:", round(prop_result$p.value, 4), "\n")
cat("95% 신뢰구간 (차이):", round(prop_result$conf.int * 100, 3), "%p\n")
if (prop_result$p.value < 0.05) {
cat("결론: 귀무가설을 기각합니다. 두 버전의 전환율에 유의미한 차이가 있습니다.\n")
} else {
cat("결론: 귀무가설을 기각하지 못합니다. 차이가 우연일 수 있습니다.\n")
}
카이제곱 검정으로 교차 분석
prop.test()와 동일한 결과를 카이제곱 검정으로도 확인할 수 있습니다. 교차표를 먼저 만들면 집단별 빈도가 한눈에 보입니다.
# 교차표 생성
contingency_table <- matrix(
c(ab_data$conversions[1], ab_data$non_conversions[1],
ab_data$conversions[2], ab_data$non_conversions[2]),
nrow = 2,
byrow = TRUE,
dimnames = list(
group = c("A (대조군)", "B (실험군)"),
outcome = c("전환", "비전환")
)
)
print(contingency_table)
# 카이제곱 검정
chi_result <- chisq.test(contingency_table, correct = FALSE)
print(chi_result)
# 효과크기 — Cramer's V
# 작음: 0.1, 중간: 0.3, 큼: 0.5
n_total <- sum(contingency_table)
cramers_v <- sqrt(chi_result$statistic / (n_total * (min(dim(contingency_table)) - 1)))
cat("Cramer's V (효과크기):", round(cramers_v, 4), "\n")
결과 시각화
library(ggplot2)
# 전환율 비교 막대 그래프
ggplot(ab_data, aes(x = group, y = rate, fill = group)) +
geom_col(width = 0.5, alpha = 0.85) +
geom_text(aes(label = paste0(round(rate * 100, 2), "%")),
vjust = -0.5, size = 5, fontface = "bold") +
scale_fill_manual(values = c("A (대조군)" = "#4A90D9", "B (실험군)" = "#27AE60")) +
scale_y_continuous(labels = scales::percent, limits = c(0, 0.12)) +
labs(
title = "A/B 테스트 전환율 비교",
subtitle = paste0("p값 = ", round(prop_result$p.value, 4),
" | ", ifelse(prop_result$p.value < 0.05, "통계적으로 유의미함", "유의미하지 않음")),
x = "그룹",
y = "전환율"
) +
theme_minimal(base_family = "AppleGothic") +
theme(legend.position = "none")
# 신뢰구간 시각화 — 차이(B - A)
diff_estimate <- diff(rev(ab_data$rate))
ci_low <- prop_result$conf.int[1]
ci_high <- prop_result$conf.int[2]
ci_df <- data.frame(
estimate = diff_estimate,
ci_low = ci_low,
ci_high = ci_high,
label = "B - A 전환율 차이"
)
ggplot(ci_df, aes(x = label, y = estimate)) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
geom_point(size = 4, color = "#E74C3C") +
geom_errorbar(aes(ymin = ci_low, ymax = ci_high),
width = 0.1, color = "#E74C3C", linewidth = 1) +
scale_y_continuous(labels = scales::percent) +
labs(
title = "전환율 차이의 95% 신뢰구간",
subtitle = "0을 포함하지 않으면 유의미한 차이",
x = NULL, y = "B - A 전환율 차이"
) +
theme_minimal(base_family = "AppleGothic")
신뢰구간이 0을 포함하지 않으면 두 전환율은 유의수준 5%에서 통계적으로 다릅니다.
비즈니스 의사결정 프레임워크
통계 검정이 전부가 아닙니다. p값이 0.04라고 해서 무조건 버전 B를 선택하면 안 됩니다. 비즈니스 의사결정에서는 다음 네 가지를 함께 봐야 합니다.
# 비즈니스 영향 추정
daily_visitors <- 10000 # 일일 방문자 수
conversion_a <- ab_data$rate[1]
conversion_b <- ab_data$rate[2]
avg_order_value <- 85000 # 평균 주문 금액 (원)
daily_revenue_a <- daily_visitors * conversion_a * avg_order_value
daily_revenue_b <- daily_visitors * conversion_b * avg_order_value
daily_uplift <- daily_revenue_b - daily_revenue_a
annual_uplift <- daily_uplift * 365
cat("=== 비즈니스 영향 추정 ===\n")
cat("일일 방문자:", format(daily_visitors, big.mark = ","), "명\n")
cat("대조군(A) 일일 매출:", format(round(daily_revenue_a), big.mark = ","), "원\n")
cat("실험군(B) 일일 매출:", format(round(daily_revenue_b), big.mark = ","), "원\n")
cat("일일 매출 증가:", format(round(daily_uplift), big.mark = ","), "원\n")
cat("연간 예상 매출 증가:", format(round(annual_uplift), big.mark = ","), "원\n")
# 의사결정 체크리스트 출력
cat("\n=== 의사결정 체크리스트 ===\n")
cat("1. 통계적 유의성:", ifelse(prop_result$p.value < 0.05, "충족 (p <", "미충족 (p ="),
round(prop_result$p.value, 4), ")\n")
cat("2. 실질적 효과크기:", round(cramers_v, 4),
"(", ifelse(cramers_v < 0.1, "소", ifelse(cramers_v < 0.3, "중", "대")), ")\n")
cat("3. 표본 크기:", n_per_group * 2, "명 (목표:", ceiling(sample_calc$n) * 2, "명)\n")
cat("4. 연간 비즈니스 가치:", format(round(annual_uplift), big.mark = ","), "원\n")
의사결정 시 고려할 추가 요소를 정리합니다.
실험군 B를 선택할 조건
- 통계적으로 유의미하다 (p < 0.05).
- 효과크기가 비즈니스 비용을 정당화할 만큼 크다.
- 표본 크기가 계획한 수만큼 확보됐다.
- 주요 세그먼트(기기, 국가, 신규/기존 고객)에서도 일관된 결과가 나온다.
실험군 B를 선택하지 않을 조건
- p값은 작지만 절대 전환율 차이가 0.1%p처럼 무시할 수준이다.
- 버전 B 구현 비용이 예상 수익 증가를 초과한다.
- 일부 세그먼트에서는 오히려 전환율이 떨어진다 (이질성 효과).
정리
A/B 테스트는 직관이 아니라 데이터로 결정하는 도구입니다. 핵심 흐름은 이렇습니다. 가설을 세우고, 표본 크기를 계산하고, 데이터를 수집하고, prop.test()로 검정하고, 비즈니스 맥락에서 해석합니다. 통계 유의성과 실질 중요성을 구분하는 습관이 좋은 데이터 분석가의 조건입니다.