Skip to Content

가설검정 (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 Test

2. 정규성 검정 (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대)**가 구매 의향에 미치는 영향 분석
  • 제조: 기계 종류작업자 숙련도가 생산량에 미치는 영향
  • 교육: 교수법학급 규모가 학습 성과에 미치는 영향
  • 의학: 약물 종류투약 용량이 치료 효과에 미치는 영향

💡 핵심 질문:

  1. 요인 A의 주효과가 있는가?
  2. 요인 B의 주효과가 있는가?
  3. 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-testWilcoxon signed-rank
2개 독립 표본 비교Independent t-testMann-Whitney U
2개 대응 표본 비교Paired t-testWilcoxon signed-rank
3개+ 독립 표본 비교One-way ANOVAKruskal-Wallis H
2×2 범주 (소표본)-Fisher’s exact
범주 독립성-Chi-square
대응 범주 전후 비교-McNemar’s
상관관계Pearson rSpearman ρ, 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 '팁 분포 차이 없음'}")

다음 단계

  • 회귀분석에서 변수 간 관계를 모델링하는 방법을 배워보세요.
  • A/B 테스트에서 실험 설계를 배워보세요.
Last updated on

🤖AI 모의면접실전처럼 연습하기