Cute Hello Kitty 3
본문 바로가기
Data/통계

사후 검정 / 본페로니 검정 / 튜키 검정 / Post-hoc Test

by 민 채 2025. 4. 8.
 
 
 

 

왜 사후검정이 필요할까?

분산분석(ANOVA)은 평균 차이가 "어디선가" 유의미하게 난다는 건 알려주지만,
"어디 그룹끼리?", 즉 구체적으로 어떤 그룹 간에 차이가 있는지는 알려주지 않습니다.

그래서 등장하는 것이 바로 사후검정(Post-hoc Test)!

 

예시

지역 A, B, C에서 커피 소비량을 조사했더니 ANOVA 결과가 유의미했다고 합시다.

그럼 이제 질문은?

"A랑 B가 차이가 나?",
"A랑 C는 어때?",
"B랑 C는??"

➡️ 이렇게 모든 그룹쌍을 비교해봐야 하는데,
그냥 다 t-test 하면 1종 오류(Type I Error)가 엄청나게 쌓임.

 

용어 설명

용어 의미
1종 오류 귀무가설이 맞는데도 기각함 (FP)
2종 오류 귀무가설이 틀렸는데도 채택함 (FN)
검정력 대립가설이 맞을 때, 귀무가설을 올바르게 기각하는 확률
유의수준(α) 1종 오류를 허용하는 기준치 (보통 0.05)

 


실험

👉 "다중 비교(Multiple Comparisons)는 1종 오류(Type I Error)를 증가시킨다."

우리는 실제로는 집단 간 차이가 전혀 없는 3개 그룹을 생성해봅시다.
이 그룹들 사이를 여러 번 t-검정으로 비교하면
"유의미한 차이가 없는데도 유의하다고 나올 확률" (1종 오류) 이 얼마나 되는지 볼 수 있습니다.

 

✅ 실험 코드: 3개 그룹 비교에서 t-test 1000번 반복

import numpy as np
from scipy.stats import ttest_ind

np.random.seed(42)

def run_ttest_simulation(n, iterations):
    type1_error_count = 0

    for _ in range(iterations):
        # 실제로는 모두 평균 100인 동일한 그룹
        group_a = np.random.normal(loc=100, scale=10, size=n)
        group_b = np.random.normal(loc=100, scale=10, size=n)
        group_c = np.random.normal(loc=100, scale=10, size=n)

        # 3쌍 비교: A-B, A-C, B-C
        p1 = ttest_ind(group_a, group_b).pvalue
        p2 = ttest_ind(group_a, group_c).pvalue
        p3 = ttest_ind(group_b, group_c).pvalue

        # 유의수준 0.05보다 작은 p-value가 하나라도 있으면 → 1종 오류 발생
        if p1 < 0.05 or p2 < 0.05 or p3 < 0.05:
            type1_error_count += 1

    return type1_error_count / iterations

# 1000 번 실행해보자
error_rate = run_ttest_simulation(30, 1000)
print(f"1종 오류가 발생한 비율: {error_rate:.3f}")
  • 실제로는 평균이 모두 같은 3개의 그룹을 만듭니다.
  • 가설을 설정하고, t-test 를 1000번 진행합니다.
    • H0: 세 쌍의 평균이 모두 같다.
    • H1: 세 쌍의 평균이 같지 않다.
  • 실제로는 평균이 같으므로, 귀무가설이 기각되어서는 안됩니다.
  • 하지만, 귀무가설이 참인데도 기각되는 오류가 발생하기도 합니다. 이를 1종오류라고 부릅니다.

 

실행 결과

1종 오류가 발생한 비율: 0.138
  • t-test 를 여러번 진행하면서 오류의 발생 비율이 점점더 높아지고 있습니다.
  • 즉, 실제로는 차이가 전혀 없는데도,
    1000번 중 약 13.9%에서 유의미한 차이라고 판단됨

 

✅ 그럼 사후 검정을 사용하면?

from statsmodels.stats.multicomp import pairwise_tukeyhsd
import pandas as pd

# 가짜 데이터 준비
group = np.array(['A']*30 + ['B']*30 + ['C']*30)
value = np.random.normal(loc=100, scale=10, size=90)
df = pd.DataFrame({'group': group, 'value': value})

# Tukey HSD 수행
tukey = pairwise_tukeyhsd(endog=df['value'], groups=df['group'], alpha=0.05)
print(tukey)

 

실행 결과:

Multiple Comparison of Means - Tukey HSD, FWER=0.05
===================================================
group1 group2 meandiff p-adj   lower  upper  reject
---------------------------------------------------
     A      B   0.0038    1.0 -6.4535  6.461  False
     A      C   -0.405 0.9877 -6.8623 6.0523  False
     B      C  -0.4088 0.9875  -6.866 6.0485  False
---------------------------------------------------
  • 사후검정을 사용하면 p값이 엄청 큰 것을 볼 수 있습니다.
  • 당연히 같은 평균값으로 데이터들을 만들었으니 각 그룹끼리 비교했을 때도, 평균이 같다고 나와야 합니다.
  • 사후검정을 진행하면 t test를 여러번 진행한 것과 달리 p값이 정상적인 것을 볼 수 있습니다. (1에 수렴)

 

그래서 !!

이러한 다중 비교의 함정을 피하기 위해 사후검정을 사용해서 보다 상세한 결과값을 얻을 수 있습니다.

 


대표적인 사후검정 방법

1. 본페로니 보정 (Bonferroni)

  • 가장 간단하고 직관적인 방법
  • 유의수준을 비교 횟수만큼 나눠서 보수적으로 검정
  • 예: 유의수준 0.05 / 비교 3쌍 = 각 쌍당 0.0167

 

예시 데이터 생성

import numpy as np
import pandas as pd

odors = ['Lavender', 'Rosemary', 'Peppermint']

# 각 향기에서 손님이 머물다 간 시간(분) 데이터
# 향기가 머무는 시간에 영향을 미치는지 확인하자.
minutes_lavender = [10, 12, 11, 9, 8, 12, 11, 10, 10]
minutes_rose = [14, 15, 13, 16, 14, 15, 14, 13, 14, 16]
minutes_mint = [18, 17, 18, 16, 17, 19, 18, 17]
anova_data = pd.DataFrame({
    'odor': np.array(["Lavender"] * 9 + ["Rosemary"] * 10 + ["Peppermint"] * 8),
    'minutes': minutes_lavender + minutes_rose + minutes_mint
})

각 향기에 따라 손님이 머무르는 시간이 달라지는지를 확인해보는 예제 데이터를 생성해보겠습니다.

 

시각화

import seaborn as sns
import matplotlib.pyplot as plt

# 시각화
sns.boxplot(x='odor', y='minutes', data=anova_data, palette='Set2')
plt.title('향기별 머무는 시간')
plt.ylabel('머무는 시간(분)')
plt.xlabel('')
plt.show()

 

  • 시각화 한 결과값을 보면 각 향기별로 평균이 전부 다른 것을 볼 수 있습니다.

 

본페로니 검정 수행 (Python 코드)

mc = MultiComparison(anova_data['minutes'], anova_data['odor'])
result = mc.allpairtest(stats.ttest_ind, method='bonf')
print(result[0])  # Bonferroni 적용된 결과

 

출력 결과:

Test Multiple Comparison ttest_ind 
FWER=0.05 method=bonf
alphacSidak=0.02, alphacBonf=0.017
====================================================
  group1     group2     stat   pval pval_corr reject
----------------------------------------------------
  Lavender Peppermint -12.7729  0.0       0.0   True
  Lavender   Rosemary  -7.3878  0.0       0.0   True
Peppermint   Rosemary   6.4552  0.0       0.0   True
----------------------------------------------------
  • 3쌍 전부 p값이 상당히 작은 것을 볼 수 있습니다. (귀무가설 기각)
  • 즉, 세쌍 모두 평균 값이 유의미하게 다릅니다.

 


2. 튜키 검정 (Tukey's Honestly Significant Difference)

 

  • 가장 많이 쓰이는 사후검정 중 표준
  • 모든 그룹 쌍 간 평균 차이를 동시에 비교
  • 등분산성 가정이 필요하며, 전체 유의수준을 조정해서 검정함

 

튜키 검정 수행 (python 코드)

등분산성 만족여부 검사 (levene test)

minutes_lavender = [10, 12, 11, 9, 8, 12, 11, 10, 10]
minutes_rose = [14, 15, 13, 16, 14, 15, 14, 13, 14, 16]
minutes_mint = [18, 17, 18, 16, 17, 19, 18, 17]

# Levene Test 수행
stat, p = levene(minutes_lavender, minutes_mint, minutes_rose)

print(f"Levene 검정 통계량: {stat:.4f}")
print(f"p-value: {p:.4f}")

if p > 0.05:
    print("✅ 등분산성 만족 → Tukey HSD 사용 가능")
else:
    print("❌ 등분산성 불만족 → 다른 방법 고려 필요")

 

  • Levene 검정은 여러 그룹 간 분산이 같은지(등분산성)를 검정하는 통계적 방법입니다.
  • 이상치에 민감하지도 않고,
  • 정규성 가정도 필요 없기에 실무에서 자주 쓰입니다.

 

정규성 검정 결과

Levene 검정 통계량: 0.2816
p-value: 0.7570
✅ 등분산성 만족 → Tukey HSD 사용 가능
  • p-value 가 상당히 크게 나왔기 때문에
  • 등분산성을 만족한다고 볼 수 있습니다.

 

튜키 검정 수행

from statsmodels.stats.multicomp import pairwise_tukeyhsd

tukey = pairwise_tukeyhsd(
    endog=anova_data['minutes'],   # 종속변수
    groups=anova_data['odor'],     # 그룹
    alpha=0.05                     # 유의수준
)
print(tukey)

 

출력 결과

    Multiple Comparison of Means - Tukey HSD, FWER=0.05    
===========================================================
  group1     group2   meandiff p-adj  lower   upper  reject
-----------------------------------------------------------
  Lavender Peppermint   7.1667   0.0   5.801  8.5324   True
  Lavender   Rosemary   4.0667   0.0  2.7753  5.3581   True
Peppermint   Rosemary     -3.1   0.0 -4.4332 -1.7668   True
-----------------------------------------------------------

 

 

결과 해석

비교 결과 의미
라벤더 vs 페퍼민트 유의미한 차이 있음 라벤더가 평균 7분 더 길다
라벤더  vs 로즈마리 유의미한 차이 있음 라벤더가 평균 4분 더 길다
페퍼민트 vs 로즈마리 유의미한 차이 있음 페퍼민트가 평균 3분 더 짧다
  • 분산분석(ANOVA)를 사용했을 때는 세 그룹간 평균 차이가 있는지 없는지만 확인할 수 있었는데,
  • 사후 검정을 사용하면 보다 자세한 결과값을 얻을 수 있습니다.

 

앞서 시각화 한 결과값과 비교했을 때, 사후 검정을 진행하면 꽤나 많은 정보를 얻을 수 있다는 것을 알 수 있습니다.


 

결론

단계 내용
1. ANOVA 수행 평균 차이가 있는지 전체적으로 확인
2. 사후검정 수행 어디 그룹 간 차이인지 파악
사후검정이 중요한 이유 다중비교에서 발생할 수 있는 1종 오류 제어

 

2025.04.07 - [Data] - LS 빅데이터 스쿨 / 분산분석(ANOVA) / 수치형 데이터 비교 / 카이제곱 검정과의 차이점

 

LS 빅데이터 스쿨 / 분산분석(ANOVA) / 수치형 데이터 비교 / 카이제곱 검정과의 차이점

LS 빅데이터 스쿨에서 분산분석에 대해 배웠습니다.저번 시간에는 범주형 변수 분석에 유용한 카이제곱 검정에 대해 배웠으니, 이번 시간에는 수치형 데이터를 분석에 사용하는 분산분석에 대

kiminchae.tistory.com