가설검정 (Hypothesis Testing)
0. 사전 준비 (Setup)
다양한 데이터셋을 활용하여 가설검정을 실습합니다.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
from scipy.stats import (
ttest_ind, ttest_rel, ttest_1samp,
mannwhitneyu, wilcoxon, kruskal,
chi2_contingency, fisher_exact,
f_oneway, shapiro, levene, bartlett,
spearmanr, pearsonr, kendalltau,
kstest, normaltest, anderson
)
import warnings
warnings.filterwarnings('ignore')
# 1. Titanic Dataset
titanic = sns.load_dataset('titanic')
print(f"Titanic: {titanic.shape}")
# 2. Iris Dataset
iris = sns.load_dataset('iris')
print(f"Iris: {iris.shape}")
# 3. Tips Dataset
tips = sns.load_dataset('tips')
print(f"Tips: {tips.shape}")
# 4. Diamonds Dataset (샘플링)
diamonds = sns.load_dataset('diamonds').sample(n=1000, random_state=42)
print(f"Diamonds: {diamonds.shape}")Titanic: (891, 15) Iris: (150, 5) Tips: (244, 7) Diamonds: (1000, 10)
1. 가설검정의 기초
핵심 개념
| 용어 | 설명 |
|---|---|
| 귀무가설 (H₀) | “차이/효과가 없다” - 기본 가정 |
| 대립가설 (H₁) | “차이/효과가 있다” - 증명하고 싶은 것 |
| p-value | 귀무가설이 참일 때, 현재 결과가 나올 확률 |
| 유의수준 (α) | 보통 0.05 (5%) 사용 |
| 검정력 (Power) | 실제 효과가 있을 때 이를 탐지할 확률 |
검정 선택 가이드
데이터 유형?
├── 연속형 (Continuous)
│ ├── 정규분포 → 모수적 검정 (t-test, ANOVA)
│ └── 비정규분포 → 비모수적 검정 (Mann-Whitney, Kruskal-Wallis)
│
└── 범주형 (Categorical)
├── 2×2 표 (소표본) → Fisher's Exact Test
└── 그 외 → Chi-Square Test2. 정규성 검정 (Normality Tests)
🎯 언제 사용하나요?
t-검정, ANOVA 같은 모수적 검정을 수행하기 전에 데이터가 정규분포를 따르는지 확인할 때 사용합니다. 정규성이 만족되지 않으면 비모수적 검정(Mann-Whitney, Kruskal-Wallis 등)을 사용해야 합니다.
Shapiro-Wilk Test
📌 사용 상황 예시
- 신약 임상시험에서 혈압 측정값이 정규분포를 따르는지 확인
- A/B 테스트 전, 사용자 체류 시간 데이터의 분포 확인
- 제조 공정에서 제품 무게 데이터가 정상 분포인지 품질 검사
💡 특징: 가장 검정력이 높은 정규성 검정. 단, n < 5000일 때 사용 권장.
# Titanic: 나이 분포의 정규성 검정
ages = titanic['age'].dropna()
stat, p_value = shapiro(ages)
print("=== Shapiro-Wilk 정규성 검정 ===")
print(f"데이터: Titanic 승객 나이 (n={len(ages)})")
print(f"통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'정규분포를 따름 ✓' if p_value >= 0.05 else '정규분포를 따르지 않음 ✗'}")
# 시각화
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].hist(ages, bins=30, edgecolor='black', alpha=0.7)
axes[0].set_title('Age Distribution')
axes[0].set_xlabel('Age')
stats.probplot(ages, dist="norm", plot=axes[1])
axes[1].set_title('Q-Q Plot')
plt.tight_layout()
plt.show()=== Shapiro-Wilk 정규성 검정 === 데이터: Titanic 승객 나이 (n=714) 통계량: 0.9816 p-value: 0.0000 결론: 정규분포를 따르지 않음 ✗
D’Agostino-Pearson Test
📌 사용 상황 예시
- 금융 데이터의 수익률 분포가 정규분포인지 확인 (왜도/첨도가 중요한 경우)
- 설문조사 점수의 분포 형태 검정
- 시험 점수 분포의 비대칭성 확인
💡 특징: **왜도(skewness)와 첨도(kurtosis)**를 함께 고려. 분포의 형태가 중요할 때 유용. n ≥ 20일 때 사용 가능.
# Iris: 꽃잎 길이의 정규성 검정
petal_length = iris['petal_length']
stat, p_value = normaltest(petal_length)
print("=== D'Agostino-Pearson 정규성 검정 ===")
print(f"데이터: Iris 꽃잎 길이 (n={len(petal_length)})")
print(f"통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'정규분포를 따름 ✓' if p_value >= 0.05 else '정규분포를 따르지 않음 ✗'}")=== D'Agostino-Pearson 정규성 검정 === 데이터: Iris 꽃잎 길이 (n=150) 통계량: 31.5324 p-value: 0.0000 결론: 정규분포를 따르지 않음 ✗
Kolmogorov-Smirnov Test
📌 사용 상황 예시
- 대용량 데이터(n > 5000)의 정규성 검정
- 정규분포 외에 다른 이론적 분포(지수분포, 균등분포 등)와 비교
- 두 표본의 분포가 동일한지 비교 (2-sample KS test)
💡 특징: 표본 크기에 덜 민감하여 대용량 데이터에 적합. 다양한 분포와 비교 가능.
# Tips: 팁 금액의 정규성 검정
tip_values = tips['tip']
# 표준화 후 검정
tip_standardized = (tip_values - tip_values.mean()) / tip_values.std()
stat, p_value = kstest(tip_standardized, 'norm')
print("=== Kolmogorov-Smirnov 정규성 검정 ===")
print(f"데이터: Tips 팁 금액 (n={len(tip_values)})")
print(f"통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'정규분포를 따름 ✓' if p_value >= 0.05 else '정규분포를 따르지 않음 ✗'}")=== Kolmogorov-Smirnov 정규성 검정 === 데이터: Tips 팁 금액 (n=244) 통계량: 0.0975 p-value: 0.0186 결론: 정규분포를 따르지 않음 ✗
Anderson-Darling Test
📌 사용 상황 예시
- 극단값(이상치)이 중요한 분석에서 정규성 확인 (위험 관리, 보험 등)
- 분포의 꼬리 부분이 정규분포와 얼마나 다른지 확인
- 여러 유의수준에서 동시에 판단이 필요할 때
💡 특징: 분포의 꼬리(tail) 부분에 더 민감. 극단값이 중요한 금융/보험 분야에서 선호.
# Diamonds: 가격의 정규성 검정
prices = diamonds['price']
result = anderson(prices, dist='norm')
print("=== Anderson-Darling 정규성 검정 ===")
print(f"데이터: Diamonds 가격 (n={len(prices)})")
print(f"통계량: {result.statistic:.4f}")
print("\n유의수준별 임계값:")
for i, (cv, sl) in enumerate(zip(result.critical_values, result.significance_level)):
result_str = "기각" if result.statistic > cv else "채택"
print(f" {sl}%: 임계값 = {cv:.4f} → H₀ {result_str}")=== Anderson-Darling 정규성 검정 === 데이터: Diamonds 가격 (n=1000) 통계량: 47.8932 유의수준별 임계값: 15.0%: 임계값 = 0.5740 → H₀ 기각 10.0%: 임계값 = 0.6540 → H₀ 기각 5.0%: 임계값 = 0.7850 → H₀ 기각 2.5%: 임계값 = 0.9150 → H₀ 기각 1.0%: 임계값 = 1.0890 → H₀ 기각
3. 등분산 검정 (Homogeneity of Variance)
🎯 언제 사용하나요?
독립표본 t-검정이나 ANOVA를 수행하기 전, 비교하려는 그룹들의 분산이 동일한지 확인할 때 사용합니다. 등분산 가정이 위배되면 Welch’s t-test나 Games-Howell 사후검정을 사용해야 합니다.
Levene’s Test
📌 사용 상황 예시
- A/B 테스트에서 실험군과 대조군의 구매금액 분산이 같은지 확인
- 남녀 그룹의 시험 점수 분산이 동일한지 검정
- 여러 공장에서 생산된 제품 품질의 산포도가 동일한지 확인
💡 특징: 정규성 가정이 필요 없어 로버스트함. 비정규 데이터에서도 사용 가능.
# Titanic: 생존 여부에 따른 나이 분산 비교
survived_ages = titanic[titanic['survived'] == 1]['age'].dropna()
died_ages = titanic[titanic['survived'] == 0]['age'].dropna()
stat, p_value = levene(survived_ages, died_ages)
print("=== Levene's 등분산 검정 ===")
print(f"생존자 나이 분산: {survived_ages.var():.2f}")
print(f"사망자 나이 분산: {died_ages.var():.2f}")
print(f"통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'등분산 가정 충족 ✓' if p_value >= 0.05 else '등분산 가정 불충족 ✗'}")=== Levene's 등분산 검정 === 생존자 나이 분산: 207.03 사망자 나이 분산: 199.41 통계량: 0.1557 p-value: 0.6933 결론: 등분산 가정 충족 ✓
Bartlett’s Test
📌 사용 상황 예시
- 정규분포가 확인된 데이터에서 그룹 간 분산 비교
- 임상시험에서 여러 용량 그룹의 반응 변동성 비교
- 품질 관리에서 여러 생산 라인의 분산 동일성 검정
💡 특징: 정규분포를 따를 때 가장 강력한 등분산 검정. 정규성이 위배되면 Levene 사용 권장.
# Iris: 품종별 꽃받침 길이 분산 비교
setosa = iris[iris['species'] == 'setosa']['sepal_length']
versicolor = iris[iris['species'] == 'versicolor']['sepal_length']
virginica = iris[iris['species'] == 'virginica']['sepal_length']
stat, p_value = bartlett(setosa, versicolor, virginica)
print("=== Bartlett's 등분산 검정 ===")
print(f"Setosa 분산: {setosa.var():.4f}")
print(f"Versicolor 분산: {versicolor.var():.4f}")
print(f"Virginica 분산: {virginica.var():.4f}")
print(f"통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"결론: {'등분산 가정 충족 ✓' if p_value >= 0.05 else '등분산 가정 불충족 ✗'}")=== Bartlett's 등분산 검정 === Setosa 분산: 0.1242 Versicolor 분산: 0.2664 Virginica 분산: 0.4043 통계량: 16.0057 p-value: 0.0003 결론: 등분산 가정 불충족 ✗
4. T-검정 (T-Tests)
단일표본 t-검정 (One-Sample T-Test)
📌 사용 상황 예시
- 품질 검사: 공장에서 생산된 배터리 평균 수명이 공칭 수명 1000시간과 같은지 검증
- 마케팅: 고객 평균 만족도가 목표치 4.0점에 도달했는지 확인
- 교육: 학생들의 평균 점수가 전국 평균 75점과 다른지 검정
- 서비스: 평균 응답 시간이 SLA 기준 3초 이내인지 확인
💡 핵심 질문: “우리 표본의 평균이 특정 기준값과 같은가/다른가?”
# Tips: 평균 팁이 $3인지 검정
# 상황: 레스토랑 매니저가 "우리 가게 평균 팁은 $3이다"라고 주장. 이게 맞는지 검증.
tip_values = tips['tip']
hypothesized_mean = 3.0
stat, p_value = ttest_1samp(tip_values, hypothesized_mean)
print("=== 단일표본 t-검정 ===")
print(f"H₀: 평균 팁 = ${hypothesized_mean:.2f}")
print(f"H₁: 평균 팁 ≠ ${hypothesized_mean:.2f}")
print(f"\n표본 평균: ${tip_values.mean():.2f}")
print(f"표본 표준편차: ${tip_values.std():.2f}")
print(f"t-통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'평균 팁은 $3와 다름' if p_value < 0.05 else '평균 팁은 $3와 같다고 볼 수 있음'}")=== 단일표본 t-검정 === H₀: 평균 팁 = $3.00 H₁: 평균 팁 ≠ $3.00 표본 평균: $3.00 표본 표준편차: $1.38 t-통계량: -0.0363 p-value: 0.9711 결론: 평균 팁은 $3와 같다고 볼 수 있음
독립표본 t-검정 (Independent Samples T-Test)
📌 사용 상황 예시
- A/B 테스트: 새 웹사이트 디자인(B)이 기존 디자인(A)보다 전환율이 높은지 검증
- 의학: 신약 투여군과 위약군의 혈압 변화 비교
- 교육: 온라인 수업과 오프라인 수업의 성적 차이 비교
- HR: 재택근무자와 사무실 근무자의 생산성 차이 분석
- 마케팅: 남성 고객과 여성 고객의 평균 구매 금액 비교
💡 핵심 질문: “서로 다른 두 그룹의 평균이 같은가/다른가?”
⚠️ 주의: 두 그룹은 서로 독립적이어야 함 (같은 사람이 두 그룹에 속하면 안 됨)
# Titanic: 성별에 따른 운임 비교
# 상황: 타이타닉호에서 남성과 여성이 지불한 운임에 차이가 있었는지 분석
male_fare = titanic[titanic['sex'] == 'male']['fare'].dropna()
female_fare = titanic[titanic['sex'] == 'female']['fare'].dropna()
# 등분산 가정 검정
_, levene_p = levene(male_fare, female_fare)
equal_var = levene_p >= 0.05
# t-검정 (Welch's t-test if unequal variance)
stat, p_value = ttest_ind(male_fare, female_fare, equal_var=equal_var)
print("=== 독립표본 t-검정 ===")
print(f"H₀: 남성 평균 운임 = 여성 평균 운임")
print(f"H₁: 남성 평균 운임 ≠ 여성 평균 운임")
print(f"\n남성 평균 운임: ${male_fare.mean():.2f} (n={len(male_fare)})")
print(f"여성 평균 운임: ${female_fare.mean():.2f} (n={len(female_fare)})")
print(f"차이: ${female_fare.mean() - male_fare.mean():.2f}")
print(f"\n등분산 가정: {'충족 (Student t-test)' if equal_var else '불충족 (Welch t-test 사용)'}")
print(f"t-통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'성별에 따른 운임 차이가 유의함' if p_value < 0.05 else '성별에 따른 운임 차이 없음'}")=== 독립표본 t-검정 === H₀: 남성 평균 운임 = 여성 평균 운임 H₁: 남성 평균 운임 ≠ 여성 평균 운임 남성 평균 운임: $25.52 (n=577) 여성 평균 운임: $44.48 (n=314) 차이: $18.95 등분산 가정: 불충족 (Welch t-test 사용) t-통계량: -4.7994 p-value: 0.0000 결론: 성별에 따른 운임 차이가 유의함
대응표본 t-검정 (Paired Samples T-Test)
📌 사용 상황 예시
- 다이어트 효과: 같은 사람들의 다이어트 전후 체중 비교
- 교육 효과: 같은 학생들의 수업 전후 시험 점수 비교
- 약물 효과: 같은 환자의 투약 전후 혈압 비교
- UX 개선: 같은 사용자가 구버전/신버전 앱을 사용했을 때 작업 완료 시간 비교
- 마케팅: 같은 매장의 프로모션 전후 매출 비교
💡 핵심 질문: “같은 대상의 처치 전후 값이 달라졌는가?”
⚠️ 독립표본과의 차이: 대응표본은 같은 대상을 두 번 측정하므로 개인차를 통제할 수 있어 더 민감하게 변화를 탐지
# 시뮬레이션: A/B 테스트 전환율 (같은 사용자가 두 가지 UI를 경험)
# 상황: 100명의 사용자에게 구버전 UI와 신버전 UI를 순차적으로 보여주고 클릭률 측정
np.random.seed(42)
n_users = 100
# Before: 기존 UI 클릭률
before = np.random.beta(2, 8, n_users) # 평균 약 20%
# After: 새 UI 클릭률 (약간의 개선)
after = before + np.random.normal(0.05, 0.03, n_users)
after = np.clip(after, 0, 1)
stat, p_value = ttest_rel(before, after)
print("=== 대응표본 t-검정 ===")
print(f"H₀: 전환율 변화 없음 (새 UI 효과 없음)")
print(f"H₁: 전환율 변화 있음 (새 UI 효과 있음)")
print(f"\nBefore (기존 UI) 평균: {before.mean():.4f} ({before.mean()*100:.1f}%)")
print(f"After (새 UI) 평균: {after.mean():.4f} ({after.mean()*100:.1f}%)")
print(f"평균 차이: {(after - before).mean():.4f} (+{(after - before).mean()*100:.1f}%p)")
print(f"\nt-통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'새 UI가 유의미하게 개선됨' if p_value < 0.05 else '유의미한 변화 없음'}")=== 대응표본 t-검정 === H₀: 전환율 변화 없음 (새 UI 효과 없음) H₁: 전환율 변화 있음 (새 UI 효과 있음) Before (기존 UI) 평균: 0.2037 (20.4%) After (새 UI) 평균: 0.2503 (25.0%) 평균 차이: 0.0466 (+4.7%p) t-통계량: -14.7221 p-value: 0.0000 결론: 새 UI가 유의미하게 개선됨
5. 비모수 검정 (Non-parametric Tests)
🎯 언제 사용하나요?
- 데이터가 정규분포를 따르지 않을 때
- 표본 크기가 작을 때 (n < 30)
- 순위 데이터나 서열 척도 데이터일 때
- 이상치(outlier)가 많을 때 (비모수 검정은 이상치에 덜 민감)
Mann-Whitney U Test
📌 사용 상황 예시
- 전자상거래: 프리미엄 회원과 일반 회원의 주문 금액 분포 비교 (금액 데이터는 보통 오른쪽 꼬리가 긴 비정규 분포)
- 게임: 과금 유저와 무과금 유저의 플레이 시간 비교
- 의학: 두 치료법의 통증 척도(1-10) 비교
- 만족도: 두 제품의 고객 평점 분포 비교
💡 핵심 질문: “두 독립 그룹의 **분포(순위)**가 다른가?”
⚠️ 독립표본 t-검정의 비모수 대안. 평균이 아닌 중앙값/순위를 비교한다고 생각하면 됨.
# Diamonds: 커팅 품질에 따른 가격 비교 (Ideal vs Good)
# 상황: 다이아몬드 커팅 품질이 Ideal인 것과 Good인 것의 가격 분포 비교
# 가격 데이터는 일반적으로 정규분포를 따르지 않으므로 비모수 검정 사용
ideal_price = diamonds[diamonds['cut'] == 'Ideal']['price']
good_price = diamonds[diamonds['cut'] == 'Good']['price']
stat, p_value = mannwhitneyu(ideal_price, good_price, alternative='two-sided')
print("=== Mann-Whitney U 검정 ===")
print(f"H₀: Ideal 커팅과 Good 커팅의 가격 분포가 같다")
print(f"H₁: 가격 분포가 다르다")
print(f"\nIdeal 중앙값: ${ideal_price.median():,.2f} (n={len(ideal_price)})")
print(f"Good 중앙값: ${good_price.median():,.2f} (n={len(good_price)})")
print(f"\nU-통계량: {stat:,.2f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'가격 분포가 유의하게 다름' if p_value < 0.05 else '가격 분포 차이 없음'}")=== Mann-Whitney U 검정 === H₀: Ideal 커팅과 Good 커팅의 가격 분포가 같다 H₁: 가격 분포가 다르다 Ideal 중앙값: $1,810.00 (n=393) Good 중앙값: $3,086.50 (n=96) U-통계량: 14,985.00 p-value: 0.0031 결론: 가격 분포가 유의하게 다름
Wilcoxon Signed-Rank Test
📌 사용 상황 예시
- 체중 감량: 다이어트 프로그램 전후 체중 비교 (체중 변화가 정규분포가 아닐 때)
- 설문조사: 같은 응답자의 정책 변경 전후 만족도(1-5점) 비교
- 교육: 같은 학생의 특강 전후 자신감 점수 비교
- 앱 평점: 업데이트 전후 같은 사용자의 평점 변화
💡 핵심 질문: “같은 대상의 전후 값 분포가 달라졌는가?”
⚠️ 대응표본 t-검정의 비모수 대안. 차이값의 부호와 순위를 사용.
# Tips: 점심 vs 저녁 팁률 비교 (같은 웨이터)
# 상황: 50명의 웨이터가 점심과 저녁에 받는 팁 비율이 다른지 검정
np.random.seed(42)
n_waiters = 50
lunch_tip_rate = np.random.uniform(0.12, 0.22, n_waiters)
dinner_tip_rate = lunch_tip_rate + np.random.normal(0.02, 0.03, n_waiters)
stat, p_value = wilcoxon(lunch_tip_rate, dinner_tip_rate)
print("=== Wilcoxon Signed-Rank 검정 ===")
print(f"H₀: 점심과 저녁 팁률 차이 없음")
print(f"H₁: 점심과 저녁 팁률 차이 있음")
print(f"\n점심 팁률 중앙값: {np.median(lunch_tip_rate)*100:.1f}%")
print(f"저녁 팁률 중앙값: {np.median(dinner_tip_rate)*100:.1f}%")
print(f"\nW-통계량: {stat:.2f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'저녁 팁률이 유의하게 높음' if p_value < 0.05 else '차이 없음'}")=== Wilcoxon Signed-Rank 검정 === H₀: 점심과 저녁 팁률 차이 없음 H₁: 점심과 저녁 팁률 차이 있음 점심 팁률 중앙값: 16.9% 저녁 팁률 중앙값: 19.1% W-통계량: 304.00 p-value: 0.0004 결론: 저녁 팁률이 유의하게 높음
Kruskal-Wallis H Test
📌 사용 상황 예시
- 마케팅: 3가지 광고 유형(TV, 온라인, SNS)별 브랜드 인지도 점수 비교
- 제품: 여러 브랜드 스마트폰의 고객 만족도 비교
- 교육: 3개 학교의 학생 성적 분포 비교
- 의학: 여러 치료법의 회복 기간 비교 (비정규 데이터)
💡 핵심 질문: “3개 이상 그룹의 분포가 모두 같은가, 아니면 적어도 하나가 다른가?”
⚠️ One-Way ANOVA의 비모수 대안. 어떤 그룹이 다른지는 사후검정 필요.
# Iris: 품종별 꽃잎 너비 비교
# 상황: 세 가지 붓꽃 품종(setosa, versicolor, virginica)의 꽃잎 너비 분포가 다른지 검정
setosa_pw = iris[iris['species'] == 'setosa']['petal_width']
versicolor_pw = iris[iris['species'] == 'versicolor']['petal_width']
virginica_pw = iris[iris['species'] == 'virginica']['petal_width']
stat, p_value = kruskal(setosa_pw, versicolor_pw, virginica_pw)
print("=== Kruskal-Wallis H 검정 ===")
print(f"H₀: 모든 품종의 꽃잎 너비 분포가 같다")
print(f"H₁: 적어도 하나의 품종이 다르다")
print(f"\nSetosa 중앙값: {setosa_pw.median():.2f}cm")
print(f"Versicolor 중앙값: {versicolor_pw.median():.2f}cm")
print(f"Virginica 중앙값: {virginica_pw.median():.2f}cm")
print(f"\nH-통계량: {stat:.4f}")
print(f"p-value: {p_value:.6f}")
print(f"\n결론: {'품종 간 유의한 차이 있음' if p_value < 0.05 else '차이 없음'}")=== Kruskal-Wallis H 검정 === H₀: 모든 품종의 꽃잎 너비 분포가 같다 H₁: 적어도 하나의 품종이 다르다 Setosa 중앙값: 0.20cm Versicolor 중앙값: 1.30cm Virginica 중앙값: 2.00cm H-통계량: 130.0111 p-value: 0.000000 결론: 품종 간 유의한 차이 있음
6. 분산분석 (ANOVA)
일원분산분석 (One-Way ANOVA)
📌 사용 상황 예시
- 마케팅: 4가지 프로모션 유형(할인, 적립, 사은품, 무료배송)의 평균 구매액 비교
- 제조: 3개 공장에서 생산된 제품의 평균 품질 점수 비교
- HR: 부서별(개발, 마케팅, 영업, 지원) 직원 만족도 비교
- 교육: 여러 교수법의 학습 효과 비교
💡 핵심 질문: “3개 이상 그룹의 평균이 모두 같은가?”
⚠️ 전제조건: 정규성, 등분산성. 위배 시 Kruskal-Wallis 사용.
# Tips: 요일별 총 결제 금액 비교
# 상황: 요일에 따라 고객들의 평균 결제 금액이 다른지 분석
thur = tips[tips['day'] == 'Thur']['total_bill']
fri = tips[tips['day'] == 'Fri']['total_bill']
sat = tips[tips['day'] == 'Sat']['total_bill']
sun = tips[tips['day'] == 'Sun']['total_bill']
stat, p_value = f_oneway(thur, fri, sat, sun)
print("=== One-Way ANOVA ===")
print(f"H₀: 모든 요일의 평균 결제 금액이 같다")
print(f"H₁: 적어도 하나의 요일이 다르다")
print(f"\n요일별 평균 결제 금액:")
print(f" 목요일: ${thur.mean():.2f} (n={len(thur)})")
print(f" 금요일: ${fri.mean():.2f} (n={len(fri)})")
print(f" 토요일: ${sat.mean():.2f} (n={len(sat)})")
print(f" 일요일: ${sun.mean():.2f} (n={len(sun)})")
print(f"\nF-통계량: {stat:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'요일별 유의한 차이 있음 → 사후검정 필요' if p_value < 0.05 else '요일별 차이 없음'}")=== One-Way ANOVA === H₀: 모든 요일의 평균 결제 금액이 같다 H₁: 적어도 하나의 요일이 다르다 요일별 평균 결제 금액: 목요일: $17.68 (n=62) 금요일: $17.15 (n=19) 토요일: $20.44 (n=87) 일요일: $21.41 (n=76) F-통계량: 2.7675 p-value: 0.0424 결론: 요일별 유의한 차이 있음 → 사후검정 필요
이원분산분석 (Two-Way ANOVA)
📌 사용 상황 예시
- 마케팅: **광고 채널(TV/온라인)**과 **타겟 연령대(20대/30대/40대)**가 구매 의향에 미치는 영향 분석
- 제조: 기계 종류와 작업자 숙련도가 생산량에 미치는 영향
- 교육: 교수법과 학급 규모가 학습 성과에 미치는 영향
- 의학: 약물 종류와 투약 용량이 치료 효과에 미치는 영향
💡 핵심 질문:
- 요인 A의 주효과가 있는가?
- 요인 B의 주효과가 있는가?
- A와 B의 상호작용 효과가 있는가? (예: 특정 조합에서만 효과가 있는지)
import statsmodels.api as sm
from statsmodels.formula.api import ols
# Tips: 성별과 흡연 여부가 팁에 미치는 영향
# 상황: 팁 금액이 성별과 흡연 여부에 따라 달라지는지, 그리고 이 둘의 조합 효과가 있는지 분석
model = ols('tip ~ C(sex) + C(smoker) + C(sex):C(smoker)', data=tips).fit()
anova_table = sm.stats.anova_lm(model, typ=2)
print("=== Two-Way ANOVA ===")
print(f"종속변수: 팁 금액")
print(f"요인 1: 성별 (sex)")
print(f"요인 2: 흡연 여부 (smoker)")
print(f"\n{anova_table.round(4)}")
print("\n해석:")
for idx, row in anova_table.iterrows():
if idx != 'Residual':
sig = "유의함 ***" if row['PR(>F)'] < 0.001 else \
"유의함 **" if row['PR(>F)'] < 0.01 else \
"유의함 *" if row['PR(>F)'] < 0.05 else "유의하지 않음"
print(f" {idx}: p = {row['PR(>F)']:.4f} → {sig}")=== Two-Way ANOVA ===
종속변수: 팁 금액
요인 1: 성별 (sex)
요인 2: 흡연 여부 (smoker)
sum_sq df F PR(>F)
C(sex) 1.0554 1.0 0.5596 0.4551
C(smoker) 0.1477 1.0 0.0783 0.7799
C(sex):C(smoker) 0.2077 1.0 0.1101 0.7404
Residual 452.5604 240.0 NaN NaN
해석:
C(sex): p = 0.4551 → 유의하지 않음
C(smoker): p = 0.7799 → 유의하지 않음
C(sex):C(smoker): p = 0.7404 → 유의하지 않음7. 카이제곱 검정 (Chi-Square Tests)
독립성 검정 (Test of Independence)
📌 사용 상황 예시
- 마케팅: 연령대(20대/30대/40대)와 선호 브랜드(A/B/C)가 연관되어 있는지 분석
- 의학: 흡연 여부와 폐암 발생이 연관되어 있는지 검정
- 교육: 성별과 전공 선택이 연관되어 있는지 분석
- HR: 학력과 이직 여부가 연관되어 있는지 분석
- 선거: 지역과 지지 정당이 연관되어 있는지 분석
💡 핵심 질문: “두 범주형 변수가 서로 독립인가, 연관되어 있는가?”
# Titanic: 성별과 생존 여부의 관계
# 상황: 타이타닉호 침몰 시 성별에 따라 생존율이 달랐는지 분석 ("여성과 아이 먼저" 규칙)
contingency = pd.crosstab(titanic['sex'], titanic['survived'])
print("교차표:")
print(contingency)
print()
chi2, p_value, dof, expected = chi2_contingency(contingency)
print("=== 카이제곱 독립성 검정 ===")
print(f"H₀: 성별과 생존 여부는 독립적이다 (관련 없음)")
print(f"H₁: 성별과 생존 여부는 연관이 있다")
print(f"\nχ² 통계량: {chi2:.4f}")
print(f"자유도: {dof}")
print(f"p-value: {p_value:.6f}")
print(f"\n기대빈도 (독립이라면 이 정도가 예상됨):")
print(pd.DataFrame(expected,
index=contingency.index,
columns=contingency.columns).round(1))
print(f"\n결론: {'성별과 생존은 강하게 연관됨 (여성 생존율이 높음)' if p_value < 0.05 else '독립적'}")교차표: survived 0 1 sex female 81 233 male 468 109 === 카이제곱 독립성 검정 === H₀: 성별과 생존 여부는 독립적이다 (관련 없음) H₁: 성별과 생존 여부는 연관이 있다 χ² 통계량: 260.7170 자유도: 1 p-value: 0.000000 기대빈도 (독립이라면 이 정도가 예상됨): survived 0 1 sex female 193.5 120.5 male 355.5 221.5 결론: 성별과 생존은 강하게 연관됨 (여성 생존율이 높음)
적합도 검정 (Goodness of Fit)
📌 사용 상황 예시
- 품질관리: 불량품 발생이 균등하게(1/5씩) 각 요일에 분포하는지 검정
- 마케팅: 고객 분포가 **기대한 비율(40:35:25)**과 일치하는지 확인
- 유전학: 관측된 유전형 비율이 **멘델의 법칙(9:3:3:1)**과 맞는지 검정
- 설문: 응답 분포가 균등분포를 따르는지 확인
💡 핵심 질문: “관측된 빈도가 기대한 이론적 분포와 일치하는가?”
# Titanic: 객실 등급 분포가 균등한지 검정
# 상황: 타이타닉호 승객이 1,2,3등급에 균등하게 분포되어 있었는지 확인
observed = titanic['pclass'].value_counts().sort_index()
n = len(titanic)
expected = np.array([n/3, n/3, n/3]) # 균등 분포 기대
chi2, p_value = stats.chisquare(observed, expected)
print("=== 카이제곱 적합도 검정 ===")
print(f"H₀: 객실 등급은 균등하게 분포되어 있다 (각 33.3%)")
print(f"H₁: 균등하지 않다")
print(f"\n관측 빈도:")
for cls, count in observed.items():
print(f" {cls}등석: {count}명 ({count/n*100:.1f}%)")
print(f"\n기대 빈도 (균등 분포): 각 {n/3:.0f}명 (33.3%)")
print(f"\nχ² 통계량: {chi2:.4f}")
print(f"p-value: {p_value:.6f}")
print(f"\n결론: {'균등하지 않음 - 3등석이 과반' if p_value < 0.05 else '균등 분포'}")=== 카이제곱 적합도 검정 === H₀: 객실 등급은 균등하게 분포되어 있다 (각 33.3%) H₁: 균등하지 않다 관측 빈도: 1등석: 216명 (24.2%) 2등석: 184명 (20.7%) 3등석: 491명 (55.1%) 기대 빈도 (균등 분포): 각 297명 (33.3%) χ² 통계량: 110.8417 p-value: 0.000000 결론: 균등하지 않음 - 3등석이 과반
Fisher’s Exact Test
📌 사용 상황 예시
- 임상시험: 소규모 파일럿 연구(n < 20)에서 치료 효과 검정
- 희귀 질환: 발생 빈도가 낮은 희귀 질환과 유전자 변이의 연관성
- 품질관리: 기대빈도가 5 미만인 희귀 불량 유형 분석
- 역학조사: 소규모 집단에서 감염 여부와 특정 행동의 연관성
💡 핵심 질문: “2×2 교차표에서 소표본일 때 두 변수가 연관되어 있는가?”
⚠️ 카이제곱 검정의 대안: 기대빈도가 5 미만인 셀이 있으면 Fisher’s Exact 사용 권장
# 소규모 임상시험 시뮬레이션
# 상황: 20명 대상 파일럿 연구. 신약 투여군(10명)과 위약군(10명)의 완치율 비교
contingency = np.array([[8, 2], # 치료군: 완치 8, 미완치 2
[3, 7]]) # 대조군: 완치 3, 미완치 7
odds_ratio, p_value = fisher_exact(contingency)
print("=== Fisher's Exact Test ===")
print("상황: 소규모 임상시험 (n=20)")
print("\n교차표:")
print(" 완치 미완치")
print(f"치료군 {contingency[0,0]} {contingency[0,1]}")
print(f"대조군 {contingency[1,0]} {contingency[1,1]}")
print(f"\n치료군 완치율: {contingency[0,0]/contingency[0].sum()*100:.0f}%")
print(f"대조군 완치율: {contingency[1,0]/contingency[1].sum()*100:.0f}%")
print(f"\nOdds Ratio: {odds_ratio:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'치료 효과 있음' if p_value < 0.05 else '치료 효과 없음'}")
print(f"\n해석: 치료군이 대조군보다 완치 odds가 {odds_ratio:.1f}배 높음")=== Fisher's Exact Test ===
상황: 소규모 임상시험 (n=20)
교차표:
완치 미완치
치료군 8 2
대조군 3 7
치료군 완치율: 80%
대조군 완치율: 30%
Odds Ratio: 9.3333
p-value: 0.0350
결론: 치료 효과 있음
해석: 치료군이 대조군보다 완치 odds가 9.3배 높음McNemar’s Test
📌 사용 상황 예시
- 마케팅: 광고 캠페인 전후 같은 고객의 브랜드 인지 변화 (인지O→인지O, 인지X→인지O 등)
- 의학: 같은 환자의 치료 전후 증상 유무 변화
- 정치: 같은 유권자의 선거 전후 지지 정당 변화
- 교육: 같은 학생의 수업 전후 특정 개념 이해 여부 변화
💡 핵심 질문: “같은 대상의 범주형 반응이 전후로 달라졌는가?”
⚠️ 대응표본 + 범주형 데이터일 때 사용. 연속형이면 Wilcoxon/대응 t-검정 사용.
from statsmodels.stats.contingency_tables import mcnemar
# 마케팅 캠페인 전후 구매 행동 변화
# 상황: 100명의 고객을 대상으로 캠페인 전후 구매 여부를 추적
# [캠페인 전 구매O/후 구매O, 전 구매O/후 구매X]
# [캠페인 전 구매X/후 구매O, 전 구매X/후 구매X]
table = np.array([[45, 15], # 전에도 구매, 후에도 구매 / 전에 구매, 후에 미구매
[35, 5]]) # 전에 미구매, 후에 구매 / 전에도 미구매, 후에도 미구매
result = mcnemar(table, exact=True)
print("=== McNemar's Test ===")
print("상황: 100명 고객의 캠페인 전후 구매 행동 변화")
print("\n대응표:")
print(" 캠페인 후 구매O 캠페인 후 구매X")
print(f"캠페인 전 구매O {table[0,0]} {table[0,1]}")
print(f"캠페인 전 구매X {table[1,0]} {table[1,1]}")
print(f"\n변화 분석:")
print(f" ✓ 구매X → 구매O (신규 구매): {table[1,0]}명")
print(f" ✗ 구매O → 구매X (이탈): {table[0,1]}명")
print(f" = 변화 없음: {table[0,0] + table[1,1]}명")
print(f"\np-value: {result.pvalue:.4f}")
print(f"\n결론: {'캠페인이 구매 행동을 유의하게 변화시킴 (신규 구매 > 이탈)' if result.pvalue < 0.05 else '유의한 변화 없음'}")=== McNemar's Test ===
상황: 100명 고객의 캠페인 전후 구매 행동 변화
대응표:
캠페인 후 구매O 캠페인 후 구매X
캠페인 전 구매O 45 15
캠페인 전 구매X 35 5
변화 분석:
✓ 구매X → 구매O (신규 구매): 35명
✗ 구매O → 구매X (이탈): 15명
= 변화 없음: 50명
p-value: 0.0066
결론: 캠페인이 구매 행동을 유의하게 변화시킴 (신규 구매 > 이탈)8. 상관분석 (Correlation Analysis)
Pearson 상관계수
📌 사용 상황 예시
- 마케팅: 광고비 지출과 매출의 선형적 관계 분석
- HR: 근속 연수와 연봉의 상관관계
- 교육: 공부 시간과 시험 점수의 관계
- 금융: 금리와 주가의 관계
💡 핵심 질문: “두 연속형 변수 간에 선형적 관계가 있는가?”
⚠️ 전제조건: 두 변수 모두 정규분포, 선형 관계. 비선형 관계는 감지 못함.
# Diamonds: 캐럿과 가격의 상관관계
# 상황: 다이아몬드 캐럿(무게)과 가격 사이에 선형적 관계가 있는지 분석
carat = diamonds['carat']
price = diamonds['price']
r, p_value = pearsonr(carat, price)
print("=== Pearson 상관분석 ===")
print(f"H₀: 캐럿과 가격은 상관이 없다 (ρ = 0)")
print(f"H₁: 캐럿과 가격은 상관이 있다 (ρ ≠ 0)")
print(f"\nPearson r: {r:.4f}")
print(f"p-value: {p_value:.6f}")
print(f"결정계수 (R²): {r**2:.4f} → 가격 변동의 {r**2*100:.1f}%를 캐럿으로 설명")
print(f"\n상관 강도 해석:")
print(f" |r| < 0.3: 약한 상관")
print(f" 0.3 ≤ |r| < 0.7: 중간 상관")
print(f" |r| ≥ 0.7: 강한 상관")
print(f"\n현재 |r| = {abs(r):.4f} → 강한 양의 상관 (캐럿↑ → 가격↑)")=== Pearson 상관분석 === H₀: 캐럿과 가격은 상관이 없다 (ρ = 0) H₁: 캐럿과 가격은 상관이 있다 (ρ ≠ 0) Pearson r: 0.9209 p-value: 0.000000 결정계수 (R²): 0.8481 → 가격 변동의 84.8%를 캐럿으로 설명 상관 강도 해석: |r| < 0.3: 약한 상관 0.3 ≤ |r| < 0.7: 중간 상관 |r| ≥ 0.7: 강한 상관 현재 |r| = 0.9209 → 강한 양의 상관 (캐럿↑ → 가격↑)
Spearman 순위상관계수
📌 사용 상황 예시
- 설문조사: 만족도 순위와 재구매 의향 순위의 관계
- 경제: GDP 순위와 행복지수 순위의 관계
- 교육: 학업 성적 순위와 취업률 순위의 관계
- 스포츠: 연봉 순위와 성적 순위의 관계
💡 핵심 질문: “두 변수 간에 단조적(monotonic) 관계가 있는가?”
⚠️ Pearson의 비모수 대안: 정규분포 불필요, 비선형이지만 단조적인 관계도 감지. 예: y = x² (단조 증가 구간에서는 Spearman이 높음)
# Tips: 총 결제 금액과 팁의 순위 상관
# 상황: 결제 금액이 높을수록 팁도 높은 경향이 있는지 (정확한 비례가 아니더라도)
total_bill = tips['total_bill']
tip = tips['tip']
rho, p_value = spearmanr(total_bill, tip)
r_pearson, _ = pearsonr(total_bill, tip)
print("=== Spearman 순위 상관분석 ===")
print(f"H₀: 결제 금액과 팁은 단조적 관계가 없다")
print(f"H₁: 결제 금액과 팁은 단조적 관계가 있다")
print(f"\nSpearman ρ: {rho:.4f}")
print(f"(비교) Pearson r: {r_pearson:.4f}")
print(f"p-value: {p_value:.6f}")
print(f"\n결론: {'유의한 단조 관계 - 결제액이 높으면 팁도 높은 경향' if p_value < 0.05 else '관계 없음'}")=== Spearman 순위 상관분석 === H₀: 결제 금액과 팁은 단조적 관계가 없다 H₁: 결제 금액과 팁은 단조적 관계가 있다 Spearman ρ: 0.8264 (비교) Pearson r: 0.6757 p-value: 0.000000 결론: 유의한 단조 관계 - 결제액이 높으면 팁도 높은 경향
Kendall’s Tau
📌 사용 상황 예시
- 순위 데이터: 두 심사위원의 순위 평가 일치도 (예: 맛집 순위)
- 서열 척도: 교육 수준(초졸/중졸/고졸/대졸)과 소득 수준(하/중/상)의 관계
- 동순위가 많을 때: 5점 척도 설문처럼 같은 값이 많은 데이터
💡 핵심 질문: “순위 데이터에서 두 변수의 **일치도(concordance)**는 얼마인가?”
⚠️ Spearman보다 보수적. 동순위(ties)가 많을 때 더 정확.
# Titanic: 객실 등급과 나이의 관계
# 상황: 1등석 승객이 더 나이가 많은 경향이 있는지 (서열 vs 연속)
pclass = titanic['pclass'].dropna()
age = titanic['age'].dropna()
# 인덱스 맞추기
common_idx = pclass.index.intersection(age.index)
pclass_aligned = pclass.loc[common_idx]
age_aligned = age.loc[common_idx]
tau, p_value = kendalltau(pclass_aligned, age_aligned)
print("=== Kendall's Tau 상관분석 ===")
print(f"H₀: 객실 등급과 나이는 관계가 없다")
print(f"H₁: 객실 등급과 나이는 관계가 있다")
print(f"\nKendall τ: {tau:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'유의한 관계' if p_value < 0.05 else '관계 없음'}")
if tau < 0:
print(f"해석: τ < 0 이므로 객실 등급↓(1등급) → 나이↑ (고급 객실에 나이 많은 승객)")=== Kendall's Tau 상관분석 === H₀: 객실 등급과 나이는 관계가 없다 H₁: 객실 등급과 나이는 관계가 있다 Kendall τ: -0.1080 p-value: 0.0000 결론: 유의한 관계 해석: τ < 0 이므로 객실 등급↓(1등급) → 나이↑ (고급 객실에 나이 많은 승객)
9. 효과 크기 (Effect Size)
🎯 왜 효과 크기가 중요한가요?
p-value는 “차이가 있는가?”만 알려주고, “얼마나 큰 차이인가?”는 알려주지 않습니다. 표본이 크면 아주 작은 차이도 유의하게 나올 수 있어요.
예시: 100만 명 대상 A/B 테스트에서 전환율 0.01%p 차이도 p < 0.05가 될 수 있지만, 이 차이가 실제로 비즈니스에 의미 있는 차이인지는 효과 크기로 판단해야 합니다.
Cohen’s d
📌 사용 상황: 두 그룹 평균 차이의 실질적 의미 해석
해석 기준 (Cohen, 1988):
- |d| < 0.2: 작은 효과 (무시해도 될 수준)
- 0.2 ≤ |d| < 0.5: 중간 효과
- 0.5 ≤ |d| < 0.8: 큰 효과
- |d| ≥ 0.8: 매우 큰 효과
def cohens_d(group1, group2):
n1, n2 = len(group1), len(group2)
var1, var2 = group1.var(), group2.var()
pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
return (group1.mean() - group2.mean()) / pooled_std
# Titanic: 생존자 vs 사망자 나이 차이의 효과 크기
# 상황: 나이와 생존의 관계. p-value가 유의해도 실제로 의미 있는 차이인지?
survived_ages = titanic[titanic['survived'] == 1]['age'].dropna()
died_ages = titanic[titanic['survived'] == 0]['age'].dropna()
d = cohens_d(survived_ages, died_ages)
t_stat, p_value = ttest_ind(survived_ages, died_ages)
print("=== 효과 크기 분석 ===")
print(f"생존자 평균 나이: {survived_ages.mean():.2f}세")
print(f"사망자 평균 나이: {died_ages.mean():.2f}세")
print(f"차이: {abs(survived_ages.mean() - died_ages.mean()):.2f}세")
print(f"\nt-통계량: {t_stat:.4f}")
print(f"p-value: {p_value:.4f} → {'유의함' if p_value < 0.05 else '유의하지 않음'}")
print(f"Cohen's d: {d:.4f}")
print(f"\n효과 크기 해석:")
print(f" |d| < 0.2: 작은 효과")
print(f" 0.2 ≤ |d| < 0.5: 중간 효과")
print(f" 0.5 ≤ |d| < 0.8: 큰 효과")
print(f" |d| ≥ 0.8: 매우 큰 효과")
print(f"\n현재: |d| = {abs(d):.4f} → ", end="")
if abs(d) >= 0.8:
print("매우 큰 효과")
elif abs(d) >= 0.5:
print("큰 효과")
elif abs(d) >= 0.2:
print("중간 효과")
else:
print("작은 효과 → 통계적으로 유의하지만 실질적 의미는 제한적!")=== 효과 크기 분석 === 생존자 평균 나이: 28.34세 사망자 평균 나이: 30.63세 차이: 2.29세 t-통계량: -2.0551 p-value: 0.0402 → 유의함 Cohen's d: -0.1616 효과 크기 해석: |d| < 0.2: 작은 효과 0.2 ≤ |d| < 0.5: 중간 효과 0.5 ≤ |d| < 0.8: 큰 효과 |d| ≥ 0.8: 매우 큰 효과 현재: |d| = 0.1616 → 작은 효과 → 통계적으로 유의하지만 실질적 의미는 제한적!
Cramér’s V
📌 사용 상황: 범주형 변수 간 연관성 강도 측정 (카이제곱 검정 후)
해석 기준:
- V < 0.1: 무시할 수준
- 0.1 ≤ V < 0.3: 약한 연관
- 0.3 ≤ V < 0.5: 중간 연관
- V ≥ 0.5: 강한 연관
def cramers_v(contingency_table):
chi2 = chi2_contingency(contingency_table)[0]
n = contingency_table.sum().sum()
min_dim = min(contingency_table.shape) - 1
return np.sqrt(chi2 / (n * min_dim))
# Titanic: 성별과 생존의 연관 강도
contingency = pd.crosstab(titanic['sex'], titanic['survived'])
v = cramers_v(contingency)
chi2, p_value, _, _ = chi2_contingency(contingency)
print("=== Cramér's V (연관성 강도) ===")
print(f"χ² = {chi2:.4f}, p-value = {p_value:.6f}")
print(f"Cramér's V = {v:.4f}")
print(f"\n해석 기준:")
print(f" V < 0.1: 무시할 수준")
print(f" 0.1 ≤ V < 0.3: 약한 연관")
print(f" 0.3 ≤ V < 0.5: 중간 연관")
print(f" V ≥ 0.5: 강한 연관")
print(f"\n현재: V = {v:.4f} → ", end="")
if v >= 0.5:
print("강한 연관 → 성별이 생존에 매우 큰 영향!")
elif v >= 0.3:
print("중간 연관")
elif v >= 0.1:
print("약한 연관")
else:
print("무시할 수준")=== Cramér's V (연관성 강도) === χ² = 260.7170, p-value = 0.000000 Cramér's V = 0.5410 해석 기준: V < 0.1: 무시할 수준 0.1 ≤ V < 0.3: 약한 연관 0.3 ≤ V < 0.5: 중간 연관 V ≥ 0.5: 강한 연관 현재: V = 0.5410 → 강한 연관 → 성별이 생존에 매우 큰 영향!
10. 다중검정 보정 (Multiple Testing Correction)
🎯 왜 보정이 필요한가요?
여러 검정을 동시에 수행하면 **1종 오류(거짓 양성)**가 누적됩니다.
예시: α = 0.05로 20개 검정을 하면
- 적어도 1개 거짓 양성 확률 = 1 - (0.95)^20 = 64%!
이를 **다중 비교 문제(Multiple Comparison Problem)**라고 합니다.
Bonferroni Correction
📌 사용 상황:
- 여러 A/B 테스트를 동시에 분석할 때
- ANOVA 후 사후검정에서 여러 쌍을 비교할 때
- 유전체 연구에서 수천 개의 유전자를 검정할 때
💡 방법: α를 검정 횟수(k)로 나눔. 예: 5개 검정 시 0.05/5 = 0.01
⚠️ 매우 보수적. 실제 효과도 놓칠 수 있음 (2종 오류 증가)
from statsmodels.stats.multitest import multipletests
# 여러 A/B 테스트 결과
# 상황: 5가지 UI 요소를 동시에 테스트. 어떤 것이 진짜 효과가 있는지?
p_values = [0.03, 0.04, 0.01, 0.08, 0.002]
test_names = ['버튼 색상', '헤드라인', 'CTA 위치', '이미지', '가격 표시']
# Bonferroni 보정
rejected, corrected_p, _, _ = multipletests(p_values, method='bonferroni')
print("=== 다중검정 보정 (Bonferroni) ===")
print(f"검정 수: {len(p_values)}")
print(f"보정된 유의수준: 0.05 / {len(p_values)} = {0.05/len(p_values):.3f}")
print(f"\n{'테스트':<12} {'원래 p-value':<15} {'보정 p-value':<15} {'결론'}")
print("-" * 60)
for name, p, cp, rej in zip(test_names, p_values, corrected_p, rejected):
result = "✓ 유의함" if rej else "✗ 유의하지 않음"
print(f"{name:<12} {p:<15.4f} {min(cp, 1.0):<15.4f} {result}")=== 다중검정 보정 (Bonferroni) === 검정 수: 5 보정된 유의수준: 0.05 / 5 = 0.010 테스트 원래 p-value 보정 p-value 결론 ------------------------------------------------------------ 버튼 색상 0.0300 0.1500 ✗ 유의하지 않음 헤드라인 0.0400 0.2000 ✗ 유의하지 않음 CTA 위치 0.0100 0.0500 ✗ 유의하지 않음 이미지 0.0800 0.4000 ✗ 유의하지 않음 가격 표시 0.0020 0.0100 ✓ 유의함
Benjamini-Hochberg (FDR)
📌 사용 상황:
- 많은 검정을 수행하지만 일부 거짓 양성은 감수할 수 있을 때
- 탐색적 분석에서 후보를 선별할 때
- 유전체 연구에서 Bonferroni가 너무 보수적일 때
💡 방법: False Discovery Rate (FDR)를 제어. “유의하다고 판정한 것 중 거짓 양성 비율”을 5%로 제어
# Benjamini-Hochberg 보정
rejected_bh, corrected_p_bh, _, _ = multipletests(p_values, method='fdr_bh')
print("=== 다중검정 보정 (Benjamini-Hochberg FDR) ===")
print(f"\n{'테스트':<12} {'원래 p-value':<15} {'보정 p-value':<15} {'결론'}")
print("-" * 60)
for name, p, cp, rej in zip(test_names, p_values, corrected_p_bh, rejected_bh):
result = "✓ 유의함" if rej else "✗ 유의하지 않음"
print(f"{name:<12} {p:<15.4f} {cp:<15.4f} {result}")
print(f"\n비교:")
print(f" Bonferroni로 유의: {sum(rejected)}개")
print(f" FDR(BH)로 유의: {sum(rejected_bh)}개")
print(f"\n→ FDR이 덜 보수적이어서 더 많은 발견 가능")
print(f" 단, 이 중 약 5%는 거짓 양성일 수 있음")=== 다중검정 보정 (Benjamini-Hochberg FDR) === 테스트 원래 p-value 보정 p-value 결론 ------------------------------------------------------------ 버튼 색상 0.0300 0.0500 ✓ 유의함 헤드라인 0.0400 0.0500 ✓ 유의함 CTA 위치 0.0100 0.0250 ✓ 유의함 이미지 0.0800 0.0800 ✗ 유의하지 않음 가격 표시 0.0020 0.0100 ✓ 유의함 비교: Bonferroni로 유의: 1개 FDR(BH)로 유의: 4개 → FDR이 덜 보수적이어서 더 많은 발견 가능 단, 이 중 약 5%는 거짓 양성일 수 있음
11. 검정력 분석 (Power Analysis)
🎯 언제 사용하나요?
실험 설계 단계에서 “몇 명의 표본이 필요한가?”를 계산할 때 사용합니다.
표본이 너무 적으면 실제 효과가 있어도 탐지 못하고 (2종 오류), 표본이 너무 많으면 자원 낭비입니다.
from statsmodels.stats.power import TTestIndPower
power_analysis = TTestIndPower()
# 시나리오: 효과 크기 0.3, 검정력 80%, 유의수준 5%
# 상황: "새 UI가 전환율을 중간 정도(d=0.3) 높일 것으로 예상.
# 80% 확률로 이 효과를 탐지하려면 몇 명이 필요한가?"
effect_size = 0.3
alpha = 0.05
power = 0.8
n = power_analysis.solve_power(effect_size=effect_size,
alpha=alpha,
power=power,
ratio=1.0,
alternative='two-sided')
print("=== 표본 크기 계산 ===")
print(f"목표 효과 크기 (Cohen's d): {effect_size} (중간 효과)")
print(f"유의수준 (α): {alpha}")
print(f"목표 검정력 (1-β): {power} (80% 확률로 효과 탐지)")
print(f"\n필요 표본 크기: 그룹당 {n:.0f}명")
print(f"총 필요 인원: {n*2:.0f}명")
# 다양한 효과 크기에 따른 필요 표본 수
print("\n효과 크기별 필요 표본 수 (검정력 80%):")
for es, desc in [(0.2, '작은 효과'), (0.3, '중간 효과'), (0.5, '큰 효과'), (0.8, '매우 큰 효과')]:
n = power_analysis.solve_power(effect_size=es, alpha=0.05, power=0.8, ratio=1.0)
print(f" d = {es} ({desc}): 그룹당 {n:.0f}명 (총 {n*2:.0f}명)")=== 표본 크기 계산 === 목표 효과 크기 (Cohen's d): 0.3 (중간 효과) 유의수준 (α): 0.05 목표 검정력 (1-β): 0.8 (80% 확률로 효과 탐지) 필요 표본 크기: 그룹당 176명 총 필요 인원: 352명 효과 크기별 필요 표본 수 (검정력 80%): d = 0.2 (작은 효과): 그룹당 394명 (총 787명) d = 0.3 (중간 효과): 그룹당 176명 (총 352명) d = 0.5 (큰 효과): 그룹당 64명 (총 128명) d = 0.8 (매우 큰 효과): 그룹당 26명 (총 51명)
12. 검정 선택 요약표
데이터 유형별 검정 선택
| 상황 | 모수적 검정 | 비모수적 검정 |
|---|---|---|
| 1개 표본 평균 vs 기준값 | One-sample t-test | Wilcoxon signed-rank |
| 2개 독립 표본 비교 | Independent t-test | Mann-Whitney U |
| 2개 대응 표본 비교 | Paired t-test | Wilcoxon signed-rank |
| 3개+ 독립 표본 비교 | One-way ANOVA | Kruskal-Wallis H |
| 2×2 범주 (소표본) | - | Fisher’s exact |
| 범주 독립성 | - | Chi-square |
| 대응 범주 전후 비교 | - | McNemar’s |
| 상관관계 | Pearson r | Spearman ρ, Kendall τ |
상황별 빠른 가이드
Q: 두 그룹 평균 비교?
├── 같은 대상의 전후? → 대응표본 t-test (정규) / Wilcoxon (비정규)
└── 다른 대상? → 독립표본 t-test (정규) / Mann-Whitney (비정규)
Q: 3개 이상 그룹 비교?
├── 정규분포? → One-way ANOVA
└── 비정규분포? → Kruskal-Wallis
Q: 두 범주형 변수 관계?
├── 기대빈도 < 5 있음? → Fisher's exact
└── 기대빈도 ≥ 5? → Chi-square
Q: 두 연속형 변수 관계?
├── 선형 관계? → Pearson r
└── 단조 관계? → Spearman ρ퀴즈
문제 1
Titanic 데이터에서 객실 등급(pclass)에 따라 생존율에 유의한 차이가 있는지 적절한 검정을 수행하세요.
정답 보기
# 범주형 vs 범주형 → 카이제곱 검정
contingency = pd.crosstab(titanic['pclass'], titanic['survived'])
chi2, p_value, dof, expected = chi2_contingency(contingency)
print("교차표:")
print(contingency)
print(f"\nχ² = {chi2:.4f}, p-value = {p_value:.6f}")
print(f"\n결론: {'객실 등급과 생존율은 연관됨' if p_value < 0.05 else '연관 없음'}")
# 효과 크기
v = cramers_v(contingency)
print(f"Cramér's V = {v:.4f} (중간 정도의 연관)")문제 2
Tips 데이터에서 흡연자와 비흡연자의 팁 금액 분포가 다른지 적절한 검정으로 확인하세요.
정답 보기
smoker_tip = tips[tips['smoker'] == 'Yes']['tip']
nonsmoker_tip = tips[tips['smoker'] == 'No']['tip']
# 정규성 검정
_, p_smoker = shapiro(smoker_tip)
_, p_nonsmoker = shapiro(nonsmoker_tip)
print(f"정규성 (흡연): p = {p_smoker:.4f}")
print(f"정규성 (비흡연): p = {p_nonsmoker:.4f}")
# 정규성 불충족 → Mann-Whitney U 사용
stat, p_value = mannwhitneyu(smoker_tip, nonsmoker_tip)
print(f"\nMann-Whitney U: {stat:.2f}")
print(f"p-value: {p_value:.4f}")
print(f"\n결론: {'팁 분포가 다름' if p_value < 0.05 else '팁 분포 차이 없음'}")