가설 검정과 신뢰 구간: 우연인지 진짜인지 가려내기
A/B 테스트에서 클릭률이 25% 올랐는데 왜 아직 "효과 있다"고 말할 수 없을까요? 귀무가설, 순열 검정, z 검정과 t 검정, p값의 정확한 뜻, 신뢰 구간, 검정력과 표본 크기를 익히고, 여러 번 검정하기와 중간에 엿보기처럼 실무에서 가장 흔한 함정을 시뮬레이션으로 확인해요. 마지막에는 AI 모델 두 개의 성능 차이가 진짜인지 판단해요.
웹사이트의 구매 버튼을 새 디자인으로 바꿔 봤어요. 기존 버튼(A)은 1,000명 중 100명이 눌렀고, 새 버튼(B)은 1,000명 중 125명이 눌렀어요. 클릭률이 10%에서 12.5%로, 무려 25%나 올랐어요. 새 디자인으로 바꾸면 될까요?
아직 몰라요. 같은 버튼을 두 그룹에 보여 줘도 클릭 수는 우연히 다르게 나오거든요. 동전을 1,000번씩 두 번 던지면 앞면 수가 똑같이 나오지 않는 것처럼요. 이번 강의의 질문은 하나예요. "이 차이가 우연으로 설명될 만큼 작은가, 아닌가?"
질문을 뒤집어 생각하기
가설 검정은 수학 기초 트랙에서 배운 귀류법과 같은 구조예요. 증명하고 싶은 것의 반대를 가정하고, 그 가정이 이상한 결과로 이어지는지 봐요.
- 귀무가설 : "두 버튼의 진짜 클릭률은 같다. 보이는 차이는 우연이다."
- 대립가설 : "진짜 클릭률이 다르다."
그리고 묻는 거예요. "귀무가설이 맞다면, 지금만큼 큰 차이가 우연히 나올 확률은 얼마인가?" 이 확률이 p값이에요. p값이 아주 작으면 "우연이라고 보기엔 너무 드문 일이 일어났다"고 판단하고 귀무가설을 기각해요.
섞어 보기: 순열 검정
공식보다 먼저 시뮬레이션으로 감을 잡아 볼게요. 귀무가설이 맞다면, 즉 버튼이 아무 상관 없다면 "A 그룹", "B 그룹"이라는 이름표는 의미가 없어요. 그러면 2,000명을 한데 섞어서 무작위로 1,000명씩 다시 나눠도 비슷한 결과가 나와야 해요.
import numpy as np
rng = np.random.default_rng(42)
clicks = np.array([1] * 225 + [0] * 1775) # 두 그룹 합쳐 2000명, 클릭 225번
observed = 0.125 - 0.100 # 실제로 관찰한 차이
diffs = []
for _ in range(10_000):
rng.shuffle(clicks) # 그룹 이름표를 무작위로 섞기
diffs.append(clicks[1000:].mean() - clicks[:1000].mean())
print(np.mean(np.abs(diffs) >= observed)) # 0.0855
이름표를 아무렇게나 섞기만 했는데도, 1만 번 중 약 855번은 2.5%p 이상의 차이가 났어요. p값은 약 0.086이에요. 버튼이 아무 효과가 없어도 이 정도 차이는 열두 번에 한 번꼴로 그냥 생긴다는 뜻이에요. "25% 상승"은 아직 우연과 구별되지 않아요.
이 방법을 순열 검정이라고 해요. 분포에 대한 가정이 거의 없고, 원리가 투명해서 실무에서도 많이 써요.
공식으로 계산하기: z 검정
중심 극한 정리 덕분에, 표본이 충분히 크면 두 클릭률의 차이는 정규 분포를 따라요. 그래서 시뮬레이션 없이 공식으로도 계산할 수 있어요.
- 차이가 없다고 가정하고 공통 클릭률을 구해요:
- 그 가정 아래에서 차이의 표준오차를 구해요:
- 관찰한 차이가 표준오차의 몇 배인지 봐요:
from scipy import stats
def two_prop_z(x1, n1, x2, n2):
p1, p2 = x1 / n1, x2 / n2
p = (x1 + x2) / (n1 + n2) # 차이가 없다고 가정한 공통 비율
se = np.sqrt(p * (1 - p) * (1 / n1 + 1 / n2))
z = float((p2 - p1) / se)
return round(z, 3), round(float(2 * stats.norm.sf(abs(z))), 4) # 양쪽 꼬리 p값
print(two_prop_z(100, 1000, 125, 1000)) # (1.769, 0.0769)
print(two_prop_z(200, 2000, 250, 2000)) # (2.502, 0.0124)
1,000명씩일 때는 , p값은 0.077이에요. 순열 검정(0.086)과 조금 다른 건, 클릭 수가 정수라서 생기는 차이를 정규 분포로 근사했기 때문이에요. 둘 다 "아직 판단하기 이르다"는 같은 결론이에요.
똑같은 클릭률(10%, 12.5%)을 2,000명씩 관찰했다면 , p값은 0.012로 떨어져요. 같은 차이라도 표본이 크면 우연으로 설명하기 어려워져요. 표준오차가 로 줄어드니까요.
p값이 말하는 것과 말하지 않는 것
p값은 통계학에서 가장 많이 오해받는 숫자예요. 정확한 정의는 이거예요.
p값: 귀무가설이 맞다고 가정할 때, 지금 관찰한 것만큼 또는 그보다 더 극단적인 결과가 나올 확률
그래서 다음은 모두 틀린 해석이에요.
- "p값이 0.03이니 귀무가설이 맞을 확률은 3%다." 아니에요. p값은 귀무가설이 맞다고 가정하고 계산한 값이라, 귀무가설이 맞을 확률을 알려 주지 않아요. 확률의 기초 강의에서 본 와 의 혼동과 똑같은 실수예요.
- "p값이 작을수록 효과가 크다." 아니에요. 표본이 크면 아주 작은 효과도 p값이 작아져요. 아래에서 확인해요.
- "p값이 0.05보다 크니 효과가 없다." 아니에요. "효과가 있다는 증거가 부족하다"일 뿐이에요. 증거의 부재는 부재의 증거가 아니에요.
2016년 미국통계학회는 이런 오용을 경고하는 공식 성명을 냈어요. p값은 "데이터가 귀무가설과 얼마나 어울리지 않는가"를 재는 하나의 지표일 뿐, 그것만으로 결론을 내리면 안 된다는 내용이에요.
유의수준과 두 가지 오류
보통 미리 기준을 정해요. 이면 기각한다는 식이에요. 이 기준 0.05를 유의수준 라고 해요. 기준을 정하면 두 종류의 실수가 생길 수 있어요.
| 실제로 효과 없음 | 실제로 효과 있음 | |
|---|---|---|
| 효과 있다고 판단 | 1종 오류 (헛경보) | 올바른 발견 |
| 효과 없다고 판단 | 올바른 판단 | 2종 오류 (놓침) |
- 1종 오류의 확률이 바로 예요. 효과가 없는데도 20번에 1번은 "효과 있다"고 잘못 말해요.
- 진짜 효과가 있을 때 그것을 잡아낼 확률을 검정력이라고 해요. 보통 80% 이상을 목표로 해요.
양성 판정의 함정과 똑같은 구조예요. 1종 오류는 위양성, 검정력은 민감도예요. 기준을 엄격하게 하면 헛경보는 줄지만 놓치는 게 늘어요.
신뢰 구간
p값은 "있다, 없다"만 말해 주지만, 실무에서는 "얼마나?"가 더 궁금해요. 신뢰 구간은 추정값과 함께 불확실성의 범위를 보여 줘요.
A/B 테스트의 차이 2.5%p에 대해 계산하면, 이라서 95% 신뢰 구간은 약 -0.3%p에서 5.3%p예요. 구간이 0을 포함하니 "차이가 없을 수도 있다"는 뜻이고, p값이 0.05보다 큰 것과 같은 결론이에요. 하지만 신뢰 구간은 정보가 더 많아요. "효과가 있다면 최대 5%p 정도일 것"이라는 것도 알려 주거든요.
95%의 정확한 뜻
"진짜 값이 이 구간 안에 있을 확률이 95%"라고 말하고 싶겠지만, 엄밀히는 이래요.
이 방법으로 구간을 100번 만들면, 그중 약 95개가 진짜 값을 포함한다.
진짜 평균 키가 170cm인 모집단에서 100명씩 표본을 100번 뽑아 각각 95% 신뢰 구간을 만들어 볼게요.
rng = np.random.default_rng(1)
S = rng.normal(170, 6, size=(100, 100)) # 100명짜리 표본을 100번
m = S.mean(axis=1)
h = stats.t.ppf(0.975, 99) * S.std(axis=1, ddof=1) / 10
print(((m - h <= 170) & (170 <= m + h)).sum()) # 94
100개 중 94개가 진짜 값 170을 포함했어요. 10만 번으로 늘리면 정확히 95% 근처가 나와요. 구간이 맞을지 틀릴지는 그 구간을 만든 표본의 운에 달려 있고, 우리는 우리 구간이 운 나쁜 5%에 속하는지 알 수 없어요. 이 해석의 차이는 베이즈 추론 강의에서 다시 다뤄요.
t 검정: 두 평균 비교하기
클릭률 같은 비율이 아니라 시험 점수 같은 평균을 비교할 때는 t 검정을 써요. 모집단의 표준편차를 모르고 표본에서 추정해야 하니, 정규 분포보다 꼬리가 조금 두꺼운 t 분포를 써서 그 불확실성까지 반영해요. 표본이 커지면 t 분포는 정규 분포와 거의 같아져요.
새 공부법의 효과를 30명씩 비교해 볼게요. 시뮬레이션이니 진짜 효과가 5점이라는 걸 우리는 알고 있어요.
rng = np.random.default_rng(2028)
A = rng.normal(70, 10, 30) # 기존 공부법 30명
B = rng.normal(75, 10, 30) # 새 공부법 30명, 진짜 효과 5점
print(A.mean().round(1), B.mean().round(1)) # 69.7 76.1
r = stats.ttest_ind(B, A)
print(r.statistic.round(2), r.pvalue.round(4)) # 2.14 0.0365
이라 0.05 기준으로는 "효과 있음"이에요. 여기까지는 좋아요. 그런데 똑같은 실험을 20번 반복하면 어떻게 될까요?
rng = np.random.default_rng(0)
ps = [stats.ttest_ind(rng.normal(75, 10, 30), rng.normal(70, 10, 30)).pvalue for _ in range(20)]
print(sum(p < 0.05 for p in ps), "번 유의") # 9 번 유의
print(np.round(sorted(ps), 3)) # 0.000부터 0.657까지
진짜 효과가 분명히 있는데도, 20번 중 11번은 "유의하지 않음"이 나와요. p값은 0.001도 안 되는 값부터 0.66까지 크게 출렁여요. 이 실험 설계의 검정력이 **약 48%**밖에 안 되기 때문이에요. 효과를 놓칠 확률이 동전 던지기와 비슷한 실험이었던 거죠. p값 하나는 생각보다 훨씬 불안정한 숫자예요.
검정력과 표본 크기
그래서 실험 전에 "몇 명이 필요한가"를 먼저 계산해요. 이를 검정력 분석이라고 해요. 필요한 표본 크기는 세 가지로 정해져요.
- 잡아내고 싶은 효과의 크기 (작을수록 많이 필요)
- 유의수준 (엄격할수록 많이 필요)
- 원하는 검정력 (높을수록 많이 필요)
def n_per_group(p1, p2, alpha=0.05, power=0.8):
za, zb = stats.norm.ppf(1 - alpha / 2), stats.norm.ppf(power)
pbar = (p1 + p2) / 2
top = za * np.sqrt(2 * pbar * (1 - pbar)) + zb * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))
return int(np.ceil(top ** 2 / (p1 - p2) ** 2))
print(n_per_group(0.10, 0.12)) # 3841
print(n_per_group(0.10, 0.105)) # 57763
클릭률 10%를 12%로 올리는 효과를 80% 확률로 잡아내려면 그룹당 3,841명이 필요해요. 0.5%p 차이를 잡으려면 그룹당 5만 7천 명이 넘게 필요해요. 효과가 4분의 1로 작아지면 필요한 표본은 약 15배로 늘어요. 처음의 1,000명짜리 실험은 애초에 판단하기엔 작았던 거예요. 공부법 실험도 검정력 80%를 원했다면 30명이 아니라 그룹당 64명이 필요했어요.
실무에서 가장 흔한 함정들
여러 번 검정하기
효과가 전혀 없는 실험도 면 20번에 한 번은 "유의"하게 나와요. 그러니 검정을 20번 하면 적어도 하나가 우연히 유의할 확률은
64%예요. 버튼 색 20가지를 테스트하고 "초록색이 유의미하게 좋았다!"고 발표하면, 그건 발견이 아니라 거의 예정된 우연일 수 있어요. 가장 간단한 대책은 본페로니 보정이에요. 검정을 번 한다면 기준을 으로 낮춰요(20번이면 0.0025).
중간에 엿보기
A/B 테스트를 돌리면서 매일 p값을 확인하다가, 0.05 아래로 떨어지는 순간 멈추고 "효과 있음"을 선언하는 경우가 정말 많아요. 효과가 전혀 없는 A/B 테스트로 이게 얼마나 위험한지 확인해 볼게요.
rng = np.random.default_rng(5)
ever = 0
for _ in range(2000): # 차이가 전혀 없는 A/B 테스트 2000번
a = rng.random(5000) < 0.1
b = rng.random(5000) < 0.1
for n in range(100, 5001, 100): # 100명 들어올 때마다 p값 확인
if two_prop_z(a[:n].sum(), n, b[:n].sum(), n)[1] < 0.05:
ever += 1
break
print(ever / 2000) # 0.3
5%여야 할 헛경보가 **30%**가 됐어요. p값은 효과가 없어도 계속 위아래로 오르내리기 때문에, 자주 들여다보면 언젠가 우연히 0.05 아래로 내려가는 순간을 만나게 돼요. 대책은 두 가지예요. 표본 크기를 미리 정하고 끝까지 기다리거나, 중간 확인을 전제로 설계된 순차 검정 방법을 쓰는 거예요.
유의하다고 중요한 건 아니다
print(two_prop_z(100_000, 1_000_000, 101_000, 1_000_000)) # (2.352, 0.0187)
100만 명씩 비교하면 10.0%와 10.1%의 차이도 로 "유의"해요. 하지만 0.1%p 차이가 디자인을 바꾸는 비용만큼 가치가 있는지는 통계가 아니라 비즈니스가 판단할 문제예요. 그래서 p값만 보고하지 말고 효과의 크기와 신뢰 구간을 함께 보고해야 해요.
p 해킹
원하는 결과가 나올 때까지 분석 방법을 바꾸는 것도 같은 문제예요. 이상치를 뺐다 넣었다 하고, 기간을 바꾸고, 하위 그룹을 골라 보다가 유의한 조합 하나를 찾아 보고하는 거죠. 하나하나는 그럴듯해 보여도, 결국 여러 번 검정하기와 똑같아요. 2015년 심리학 논문 100편을 다시 실험한 대규모 프로젝트에서, 원래 논문은 거의 다 유의한 결과였지만 재현 실험에서 유의하게 나온 건 약 36%였어요. 이런 관행이 원인 중 하나로 꼽혀요.
AI 모델 비교에 쓰기
새로 학습한 모델 A가 기존 모델 B보다 테스트 문제 1,000개에서 정확도가 87%로, B의 85%보다 2%p 높아요. 정말 더 좋은 모델일까요?
같은 문제를 두 모델이 푼 것이니 짝지은 비교를 해야 해요. 중요한 건 두 모델의 답이 엇갈린 문제예요. 둘 다 맞히거나 둘 다 틀린 문제는 차이에 대해 아무 정보도 주지 않거든요. 여기서는 A만 맞힌 문제가 70개, B만 맞힌 문제가 50개라고 해 볼게요.
어떤 분포 공식도 쓰지 않고 신뢰 구간을 구하는 강력한 방법이 부트스트랩이에요. 가진 데이터에서 복원 추출로 같은 크기의 표본을 계속 다시 뽑아, 통계량이 얼마나 흔들리는지 직접 보는 거예요.
a = np.array([1] * 800 + [1] * 70 + [0] * 50 + [0] * 80) # 모델 A가 맞혔는지
b = np.array([1] * 800 + [0] * 70 + [1] * 50 + [0] * 80) # 모델 B가 맞혔는지
print(a.mean(), b.mean()) # 0.87 0.85
rng = np.random.default_rng(0)
boots = []
for _ in range(10_000):
idx = rng.integers(0, 1000, 1000) # 문제 1000개를 복원 추출
boots.append(a[idx].mean() - b[idx].mean())
print(np.percentile(boots, [2.5, 97.5])) # [-0.002 0.042]
정확도 차이의 95% 신뢰 구간이 약 -0.2%p에서 4.2%p라서 0을 포함해요. 엇갈린 문제 120개만 보는 맥니마 검정을 해도 p값이 약 0.07이에요. 테스트 문제 1,000개로는 2%p 차이를 확신할 수 없어요. 모델 순위표에서 1, 2%p 차이로 순위가 갈리는 걸 볼 때마다 이 계산을 떠올려 보세요. 머신러닝 트랙의 평가 강의에서 교차 검증과 함께 다시 써요.
실습
- 순열 검정 코드에서 그룹 크기를 2,000명씩(클릭 200, 250)으로 바꿔 p값을 구하고, z 검정 결과(0.012)와 비교해 보세요.
- 효과가 없는 A/B 테스트 1,000번을 시뮬레이션하고 p값의 히스토그램을 그려 보세요. 0과 1 사이에 고르게 퍼지는지 확인하세요. 효과가 있을 때는 모양이 어떻게 바뀌나요?
- 중간에 엿보기 코드에서 확인 횟수를 50번에서 10번, 5번으로 줄이면 헛경보 비율이 어떻게 변하나요?
- 공부법 실험에서 그룹당 64명으로 늘려 20번 반복하면 유의한 결과가 몇 번 나오는지 세어 보세요.
자주 하는 실수
- p값을 "귀무가설이 맞을 확률"로 읽기. 조건부 확률의 방향이 반대예요.
- "유의하지 않음"을 "효과 없음"으로 읽기. 검정력이 낮은 실험에서는 진짜 효과도 자주 놓쳐요.
- 결과를 보면서 실험 종료 시점 정하기. 헛경보가 몇 배로 늘어요.
- 여러 지표, 여러 그룹을 검정하고 유의한 것만 보고하기. 보정하지 않으면 우연을 발견으로 착각해요.
- 효과 크기 없이 p값만 보고하기. 큰 표본에서는 무의미하게 작은 차이도 유의해요.
점검 문제
- 효과가 전혀 없는 실험을 100번 하면, 기준으로 대략 몇 번 "유의"하게 나올까요?
- A/B 테스트 결과 p값이 0.2였어요. "새 디자인은 효과가 없다"고 결론 내려도 되나요?
- 어떤 차이의 95% 신뢰 구간이 1.2%p에서 3.8%p예요. 양측 검정의 결론은?
- 잡아내고 싶은 효과의 크기를 절반으로 줄이면, 필요한 표본 크기는 대략 몇 배가 되나요?
- 두 모델의 정확도를 같은 테스트셋에서 비교할 때, 둘 다 맞힌 문제가 많다는 사실이 차이를 판단하는 데 도움이 되나요?
정답
- 약 5번이에요.
- 아니에요. 효과가 있다는 증거가 부족할 뿐이에요. 표본이 작아 검정력이 낮았을 수 있으니, 신뢰 구간의 범위와 표본 크기를 함께 봐야 해요.
- 구간이 0을 포함하지 않으니 귀무가설을 기각해요. 차이가 있다고 판단해요.
- 표준오차가 로 줄어드니, 효과가 절반이면 표본은 약 4배가 필요해요.
- 아니에요. 차이에 대한 정보는 두 모델의 답이 엇갈린 문제에서만 나와요.
다음 강의
지금까지는 "차이가 있는가"를 물었어요. 다음 강의에서는 "공부 시간이 1시간 늘면 점수가 몇 점 오르나"처럼 변수 사이의 관계를 숫자로 표현하는 회귀 분석을 배워요. 상관관계와 인과관계의 차이, 결론이 뒤집히는 심슨의 역설, 평균으로의 회귀까지 다뤄요.