업종과 분야를 막론하고 우리는 항상 더 좋은 판단을 하기 위해 실험을 진행합니다. 특히 비즈니스 분석, 데이터 분석을 한다면 피해 갈 수 없는 업무가 있죠. 바로 A/B 테스트입니다.
사소한 궁금증과 호기심에서 시작된 것이 실험 설계로 이어지고, 이것을 검증하기 위해 생각 보다 많은 시간을 할애합니다. 비교군과 실험군을 나누고, 일정 시간 동안 실험을 진행하여 각 집단에서 보인 관측 결과에 차이가 있는지를 검정하려면 최소 하루는 필요한 것 같습니다. Braze 같은 마케팅 솔루션을 이용한다고 해도 어쨌든 캠페인 기간을 최소 몇 시간은 설정하는 편이니 시간이 꽤 필요합니다.
그런데 실험을 통해 얻은 관측치만 보고 성급한 결론을 내리는 경향이 종종 있습니다. '어? 앱 화면을 이렇게 바꾸니까 하니까 체류 시간이 더 기네? 앞으로 이렇게 계속해야겠다.'라고 결론짓는 것이죠. 하지만 실험 결과에 의한 차이가 우연에 의한 것임을 놓칠 때가 있습니다. 즉, 똑같은 실험을 여러 번 했을 때도 과연 지금과 같은 결과일 것인지 확인해 볼 필요가 있기 때문입니다.
따라서 우리의 실험 결과를 보장해 줄 수 있는, 그니까 정말 우연이 아니라 찐이라는 것을 확인하기 위해 통계적 가설검정을 거쳐야 합니다. 가설검정 방법 중 가장 많이 사용하는 것이 t-검정입니다. 다만, 이 방법을 사용하기 위해선 몇 가지 전제가 있습니다. 그중 가장 중요한 것이 각 집단의 분포가 정규분포를 띄어야 한다는 것인데요, 현업에서 문제를 정의하고 실험을 설계하다 보면 이 방법을 이용하기 어려운 경우가 많습니다. 대부분 한쪽으로 치우친 분포의 데이터가 상당히 많기 때문입니다.
이럴 때 사용할 수 있는 방법 중 하나가 바로 부트스트랩 순열 검정입니다. 이 방법은 세 가지 이유 때문에 제가 좋아하는 검정 방법인데요,
특히 마지막 이유가 이 방법을 자주 사용하게 된 계기가 되었습니다. 그래서 이번 글에서는 t-검정보다는 휴리스틱 방법인 순열 검정을 소개하고 활용 방안을 공유해 보려고 합니다.
검정 절차는 간단합니다.
순열 검정의 원리로부터 두 가지 특징을 잘 이해하면 좋습니다.
permute
입니다. '순서를 바꾼다'라는 의미로, 집합 내 순서를 섞음으로써 기존에 관측된 순서로 인해 발생할 수 있는 편향 자체를 없애는 것입니다.조금 더 파고들겠습니다. '복원 추출을 N 번 반복한다'라는 것을 이해할 필요가 있는데요, 이것은 부트스트랩(Bootstrap) 기법입니다. 현재 갖고 있는 데이터에서 발생 가능한 변동성을 알아보기 위해 반복적으로 표본을 추출하는 방법에서 출발하는 것이죠.
통계학에서는 일반적으로 우리가 보유하고 있는 데이터로 관측한 통계량은 '표본 통계량'으로 간주합니다. 실제 세계에서의 모집단은 알 수 없는 미지의 집단으로 보는 전제가 깔려있는 것입니다. 그래서 이 미지의 모집단 통계량(=모수)을 추정하기 위해 현재 보유한 데이터로 표본 통계량을 무수히 많이 생성해보는 방법을 시도하게 되는데, 이 표본 통계량 분포를 통해 모수를 추정할 수 있게 됩니다. 추론통계의 기반인 모집단 분포에 상관없이 표본평균의 분포가 정규분포로 수렴한다는 중심극한정리의 개념을 설명하는 것과 동일한 맥락입니다.
이때 표본 통계량을 생성하기 위해 무작위 복원 추출을 시도한다는 점이 중요한데, '각각의 데이터가 관측될 확률은 모두 독립적이고 동일하다'라는 가정을 만족시키기 위한 방법인 셈입니다. 따라서 현재 보유한 표본으로 많은 반복 추출을 통해 또 다른 표본 통계량을 생성한다는 것은, 현실 세계에서 한 번쯤은 발견할 만한 또 다른 표본을 만들겠다는 다소 휴리스틱 한 의도가 담겨있다고 볼 수 있습니다. (이런 점에서 표본은 다다익선입니다. 많을수록 모집단을 잘 추정할 수 있으니까요.)
이 과정을 통해 생성된 표본의 통계량 중 가장 많은 빈도로 관측된 값을 모수로 추정하는 것이고, 이 값의 변동성(or 불확실성)을 우리가 많이 들어본 신뢰구간(confidence level)으로 제공하는 것입니다. 이 신뢰구간은 표본 통계량 분포의 표준편차와 관련이 있습니다. 우리가 얻은 표본 통계량의 분포에서 이 표준편차가 작으면 작을수록 모수가 확실한 값일 테고, 클수록 불확실한 값이 되는 것이죠. 신뢰구간은 보통 분포 내의 95%로 제한하는 게 일반적인데, 표준편차의 약 2배 정도되는 구간으로 봐도 무방합니다.
마찬가지로 부트스트랩 순열 검정에서도 많은 검정 통계량을 생성하게 되는데, 실험을 통해 얻은 차이가 이 검정 통계량 분포 내의 표준편차 보다 훨씬 멀리 있다면 '우연히 관측될만한 값은 아니었구나!'라고 판단할 수 있는 것입니다. 우리가 가장 얻고 싶은 결론이기도 하지요. 😎
다시 말해 순열 검정 통계량 분포의 약 95%를 벗어난 곳에 있는 값이라면, 실험의 차이가 우연이었을 확률이 5%보다도 작다고 말할 수 있는 것입니다. 그만큼 신뢰할 만한 차이라는 것이죠. 그리고 이것이 바로 우리가 귀 따갑게 듣는 'p-value가 0.05 이하다'라는 것과 동일한 말로 해석할 수 있겠습니다.
설명이 조금 길었지만 예시를 통해 원리를 이해하면 더욱 좋을 것 같습니다.
각 그룹의 체류시간 분포를 확인해 보니 아래와 같습니다. 위에서 설명한 순열 검정 방법을 그대로 적용해 보겠습니다.
# 샘플 데이터 생성 예시
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2022)
n = 50000
a = abs(np.random.normal(loc=5, scale=4, size=n))
b = abs(np.random.normal(loc=7, scale=5, size=n))
df_sample_a = pd.DataFrame({"Group": ["A"] * n, "Duration time": a})
df_sample_b = pd.DataFrame({"Group": ["B"] * n, "Duration time": b})
df_samples = pd.concat([df_sample_a, df_sample_b], ignore_index=True)
duration_time_mean_A = (
df_samples.loc[df_samples["Group"] == "A", "Duration time"].mean().round(2)
)
duration_time_mean_B = (
df_samples.loc[df_samples["Group"] == "B", "Duration time"].mean().round(2)
)
sns.histplot(data=df_samples, x="Duration time", binwidth=0.5, hue="Group", kde=True)
plt.title(label="Time duration distribution")
plt.xlabel(xlabel="Time (minute)")
plt.ylabel(ylabel="Frequency")
plt.axvline(x=duration_time_mean_A, ymin=0, ymax=1, color="b", ls="--")
plt.axvline(x=duration_time_mean_B, ymin=0, ymax=1, color="r", ls="--")
plt.text(
x=duration_time_mean_A,
y=2500,
s=str(duration_time_mean_A),
bbox=dict(facecolor="b", alpha=0.2),
)
plt.text(
x=duration_time_mean_B,
y=2500,
s=str(duration_time_mean_B),
bbox=dict(facecolor="r", alpha=0.2),
)
plt.gca().yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter("{x:,.0f}"))
부트스트랩 순열 검정의 원리를 코드로 간단히 구현하여 분포를 확인한 결과입니다. 표본 추출 횟수는 10,000번을 시행하였으며, 표준편차에 의한 변동성도 1 sigma
, 2 sigma
로 확인할 수 있습니다.
분포 자체를 생성하는 건 어렵지 않습니다. 이 검정 통계량 분포가 의미하는 바를 이해하는 게 중요합니다!
실험 집단과 통제 집단의 차이가 한 번쯤은 나타날만한 값(검정 통계량) 10,000개를 만들어 확인한 분포
## 순열 검정 통계량 분포 생성 예시
random_state = 2022
# 데이터 순서 섞기(shuffling)
df_samples = df_samples.sample(frac=1, ignore_index=True, random_state=random_state)
# A, B 그룹의 표본 개수 생성
n_sample_A, n_sample_B = df_samples["Group"].value_counts()
# 표본 통계량을 저장할 리스트
sample_mean_list = list()
# 표본 추출 시행 횟수
n = 10000
# 순열 검정 통계량 생성
for _ in range(n):
sample_A = df_samples.sample(n=n_sample_A, replace=True)["Duration time"]
sample_B = df_samples.sample(n=n_sample_B, replace=True)["Duration time"]
sample_mean_A = np.mean(sample_A)
sample_mean_B = np.mean(sample_B)
diff = sample_mean_B - sample_mean_A
sample_mean_list.append(diff)
# 분포 확인
plt.figure(figsize=(12, 6))
ax = plt.hist(sample_mean_list, bins=20, alpha=0.8)
plt.title(label=f"distribution of sample mean differences (Number of trials: {n:,})")
plt.xlabel(xlabel="sample mean difference")
plt.ylabel(ylabel="frequency")
sigma_1_left = np.mean(sample_mean_list) - np.std(sample_mean_list)
sigma_1_right = np.mean(sample_mean_list) + np.std(sample_mean_list)
sigma_2_left = np.mean(sample_mean_list) - (np.std(sample_mean_list) * 2)
sigma_2_right = np.mean(sample_mean_list) + (np.std(sample_mean_list) * 2)
plt.axvline(x=sigma_1_left, ymin=0, ymax=1, color="g", ls="--")
plt.axvline(x=sigma_1_right, ymin=0, ymax=1, color="g", ls="--")
plt.axvline(x=sigma_2_left, ymin=0, ymax=1, color="r", ls="--")
plt.axvline(x=sigma_2_right, ymin=0, ymax=1, color="r", ls="--");
시행 횟수가 많아질수록 표본 통계량의 분포가 점점 정규분포에 가까워질 뿐만 아니라, 표준편차에 의한 변동성도 더 안정적으로 나타나는 것을 확인할 수 있습니다. 어떤 검정 방법이든 마찬가지겠지만 표본의 수가 많고 반복 횟수가 많을수록 불확실성을 줄일 수 있다는 것은 자명합니다. 요즘 시대에 회사에서 다루는 표본의 개수 아무리 많다고 한들, 컴퓨팅 리소스가 부족할 정도로(..) 많지는 않을 것이기 때문에, 이 방법을 통해 검증하는 것에는 큰 문제가 없을 것이라 생각합니다. 😅
실험 결과로 나타난 차이가 우연히 발생하였을 확률이 2.5% 보다도 낮다!
예시로 만든 실험군과 대조군의 차이가 위에서 생성한 검정 통계량의 분포로부터 얼마나 떨어져 있는지 확인해 보니 상당히 멀리 있습니다. 이것이 바로 우연히 발생하지 않을만한, 그니까 의미 있는 차이라고 볼 수 있는 근거가 된다는 것입니다. 두 집단에 차이가 있다고 판단할 수 있는 지표로 보는 p-value
가 0.05보다도 낮다고 보는 것과 동일한 맥락인 것이죠.
과연 얼마나 아슬아슬하게 우연이 아님을 피할 수 있었을까 싶었지만, 생각 보다 시시하게 너무나 우연이 아님을 확인할 수가 있었습니다. 이것은 예시로 만든 데이터이니 만큼 참고만 해주셨으면 좋겠습니다. 😋
- 순열 검정 통계량 분포 생성과 시각화까지 해줄 수 있는 모듈을 만들어 보기
- 가설 검정을 처음 접하는 동료에게 설명해야 하는 상황이라면 분포를 통해 설명해보기
순열 검정의 절차를 보시면 아시겠지만 특별한 가정을 두지 않습니다. 데이터 분포가 어떻든, 표본 개수가 어떻든 일단 표본 통계량을 많이 만들어 내는 것이죠. '아니 그러면 다른 검정 방법 다 필요 없고 얘만 쓰면 되겠네??'라고 생각할 수 있으나, 당연히 꼭 그렇진 않습니다. 😅
이와 관련한 질문을 저도 찾아봤는데요, 여기서 나온 답변을 토대로 얘기해 보자면 결국 표본이 많을수록 순열 검정의 결과가 다른 방법과 비교해도 검정력에서 문제가 없다는 의견인 것 같습니다. 이런 점에서 현시대에 데이터 표본이 부족할 일은 없기 때문에 충분히 믿고 사용할만한 방법이라고 생각합니다. 다만, 분포에 따라서 비교할 통계량을 무엇으로 볼지는 다를 수 있습니다. 예를 들어 왜도(skewness)가 심한 분포는 평균보다는 중위수로 그룹 간 그 차이를 비교하는 게 적절할 것입니다.
물론 t-검정과 같은 수식에 기반한 방법 사용하는 게 더 편할 수도 있습니다. 코드 몇 줄이면 되거든요. 혹은 정규 분포를 따르지 않는 비모수 집단의 비교에는 Wilcoxon 검정
, Mann-Whitney 검정
등의 더 잘 알려진 방법도 있습니다. 이처럼 표본 개수, 정규성, 그룹 수 등 다양한 조건을 따지긴 해야하지만 통계적 검정은 이런 원칙적인 절차를 반드시 거쳐야 하는 것은 맞습니다. 어우 사진만 봐도 머리가 아프네요. 😇
다만, 데이터와 통계에 대해 잘 아는 사람들만 모여 있는 이상적인 조직이라면 그렇게만 해도 된다고 생각합니다. 근데 이런 오르비스 같은 조직이 얼마나 될까요..😂 우리는 늘 데이터와 통계를 통해 나온 수치를 다른 동료에게 이해시켜야 하는 상황을 흔하게 마주합니다. 과거에도 그랬고 현재도 그렇고 앞으로도 그럴 겁니다. 그럴 때마다 '이거 검정 결과가 차이가 없는 걸로 나왔어요'라고 끝내는 것과 '이게 왜 유의미한 차이가 아니냐면요 이 분포를 보시면..🔥'와 같은 설명을 통해 데이터 리터러시를 높이려는 시도를 하는 것과는 분명 다를 것입니다.
이런 점에서 저는 이 방법이 모두에게 설명하기 쉬우면서도, 해석하기 쉬운, 그리고 수식에 기반한 통계학이 빠지기 쉬운 형식주의로부터 벗어날 수 있는 접근법이라 좋습니다. 코드도 간단하기 때문에 A/B 테스트 데이터셋을 바꿔가면서 검정 결과를 확인할 수 있는 모듈을 만들어 놓는다면 더 좋을 것입니다. 👏👏👏
(흐아 드디어 다 썼다!! 💪)
요새는 공룡 기업뿐만 아니라 많은 회사에서 A/B 테스트는 기본으로 하고 강화학습 기반의 MAB(multi armed bandit) 시스템이 대세인 추세입니다. 그래서 MAB를 설명하는 글을 적어보고 싶었으나.. 쪼렙인 저는 기초를 다시 다지자는 마음으로 해당 내용을 정리해봤습니다. 대부분 기초 통계와 관련된 내용이 주를 이루었음에도 직접 써보니 쉽지 않네요. A/B 테스트에 포커스를 두고 설명한 것이긴 하지만, 다양한 가설 검정에도 사용할 수 있으리라 기대합니다.
진짜 마무리로 명언 하나 스윽 던지고 끝내봅니다..ㅋㅋㅋ
인생은 하나의 실험이다. 실험이 많아질수록 당신은 더 좋은 사람이 된다. (by 랄프 왈도 에머슨)
실무하면서 정규성을 만족하는 데이터를 본 적이 없다보니 순열검정을 많이 사용하는데
통계가정을 만족하지 않아도 된다는 장점도 좋지만
뭐니뭐니해도 다른 사람한테 '설명, 해석하기 편하다' 라는 점이 제일 좋았습니다.
좋은 글 감사해요~~~