🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

다중 비교 문제 해결 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 2. · 12 Views

다중 비교 문제 해결 완벽 가이드

여러 통계 검정을 동시에 수행할 때 발생하는 다중 비교 문제와 그 해결 방법을 다룹니다. Bonferroni, FDR 등 보정 기법을 실무 예제와 함께 쉽게 설명합니다.


목차

  1. 다중_비교_문제란
  2. Bonferroni_보정
  3. Holm_보정
  4. FDR과_Benjamini_Hochberg
  5. 보정_방법_선택_가이드
  6. 실전_A/B_테스트_적용
  7. 주의사항과_흔한_실수

1. 다중 비교 문제란

어느 날 김개발 씨가 A/B 테스트 결과를 분석하고 있었습니다. 버튼 색상, 텍스트, 위치 등 10가지 요소를 동시에 테스트했는데, 그중 3가지가 "통계적으로 유의미하다"고 나왔습니다.

신나서 보고서를 작성하려던 순간, 데이터 분석가 박시니어 씨가 다가왔습니다. "잠깐, 다중 비교 보정은 했어요?"

**다중 비교 문제(Multiple Testing Problem)**는 여러 통계 검정을 동시에 수행할 때 우연히 유의미한 결과가 나올 확률이 급격히 높아지는 현상입니다. 마치 주사위를 여러 번 던지면 6이 나올 확률이 높아지는 것과 같습니다.

이 문제를 무시하면 실제로는 효과가 없는 것을 효과가 있다고 잘못 결론내릴 수 있습니다.

다음 코드를 살펴봅시다.

import numpy as np
from scipy import stats

# 10개의 독립적인 A/B 테스트 시뮬레이션
np.random.seed(42)
n_tests = 10
p_values = []

for i in range(n_tests):
    # 실제로는 차이가 없는 두 그룹 (귀무가설이 참)
    group_a = np.random.normal(100, 15, 1000)
    group_b = np.random.normal(100, 15, 1000)

    _, p_value = stats.ttest_ind(group_a, group_b)
    p_values.append(p_value)
    print(f"테스트 {i+1}: p-value = {p_value:.4f}")

# 유의수준 0.05 기준으로 "유의미한" 결과 개수
significant = sum(p < 0.05 for p in p_values)
print(f"\n유의미한 결과: {significant}개 (기대값: 0.5개)")

김개발 씨는 입사 6개월 차 데이터 분석가입니다. 마케팅팀에서 요청한 A/B 테스트 분석을 막 끝낸 참이었습니다.

10가지 UI 요소를 동시에 테스트했고, 그중 3가지에서 p-value가 0.05 미만으로 나왔습니다. "드디어 유의미한 결과를 찾았다!" 김개발 씨는 기뻐하며 보고서 초안을 작성하기 시작했습니다.

그때 옆자리의 박시니어 씨가 화면을 슬쩍 보더니 물었습니다. "혹시 다중 비교 보정은 적용했어요?" 김개발 씨는 고개를 갸우뚱했습니다.

"다중 비교 보정이요? 그게 뭔가요?" 박시니어 씨가 친절하게 설명을 시작했습니다.

"쉽게 말해서, 검정을 여러 번 하면 우연히 유의미한 결과가 나올 확률이 높아져요. 마치 복권을 많이 사면 당첨될 확률이 높아지는 것처럼요." 조금 더 구체적으로 생각해봅시다.

우리가 유의수준을 0.05로 설정한다는 것은, 실제로 효과가 없을 때 "효과가 있다"고 잘못 말할 확률을 5%로 제한한다는 뜻입니다. 이것을 제1종 오류(Type I Error) 또는 **거짓 양성(False Positive)**이라고 부릅니다.

한 번의 검정에서는 이 확률이 5%입니다. 그런데 10번의 검정을 하면 어떨까요?

각 검정이 독립적이라면, 10번의 검정에서 한 번도 거짓 양성이 나오지 않을 확률은 0.95의 10제곱, 즉 약 60%입니다. 뒤집어 말하면, **최소 한 번 이상 거짓 양성이 나올 확률이 40%**나 된다는 뜻입니다.

이것을 **가족별 오류율(Family-Wise Error Rate, FWER)**이라고 부릅니다. 검정 횟수가 늘어날수록 이 확률은 기하급수적으로 증가합니다.

20번 검정하면 64%, 50번이면 92%에 달합니다. 위의 코드를 보면 이 문제를 직접 시뮬레이션하고 있습니다.

두 그룹이 실제로는 완전히 동일한 분포에서 왔음에도 불구하고, 10번 검정을 반복하면 평균적으로 0.5개의 "유의미한" 결과가 나옵니다. 운이 나쁘면 2-3개가 나올 수도 있습니다.

김개발 씨가 발견한 3개의 유의미한 결과도 어쩌면 이런 우연의 산물일 수 있습니다. 실제로는 아무 효과도 없는데, 단지 많이 테스트했기 때문에 우연히 유의미하게 보이는 것일 수 있다는 뜻입니다.

박시니어 씨가 덧붙였습니다. "그래서 여러 검정을 할 때는 반드시 보정을 해야 해요.

그래야 진짜 효과가 있는 것과 우연히 나온 결과를 구분할 수 있거든요."

실전 팁

💡 - 동시에 여러 검정을 수행할 때는 항상 다중 비교 문제를 고려하세요

  • 검정 횟수가 늘어날수록 거짓 양성 위험도 급증한다는 점을 기억하세요

2. Bonferroni 보정

박시니어 씨의 설명을 들은 김개발 씨가 물었습니다. "그러면 어떻게 보정해야 하나요?" 박시니어 씨가 미소를 지으며 답했습니다.

"가장 간단한 방법은 본페로니 보정이에요. 검정 횟수로 유의수준을 나누는 거죠."

Bonferroni 보정은 가장 단순하고 보수적인 다중 비교 보정 방법입니다. 원래의 유의수준을 검정 횟수로 나누어 더 엄격한 기준을 적용합니다.

마치 여러 문을 열어볼 때 각 문에 더 엄격한 보안 검사를 적용하는 것과 같습니다. 간단하지만 검정이 많아지면 지나치게 보수적이 될 수 있습니다.

다음 코드를 살펴봅시다.

from statsmodels.stats.multitest import multipletests

# 앞서 구한 p-values 사용
p_values = [0.03, 0.12, 0.04, 0.67, 0.02,
            0.45, 0.08, 0.01, 0.23, 0.15]

# Bonferroni 보정 적용
alpha = 0.05
n_tests = len(p_values)
bonferroni_alpha = alpha / n_tests

print(f"원래 유의수준: {alpha}")
print(f"보정된 유의수준: {bonferroni_alpha}")
print()

# 보정 전후 비교
for i, p in enumerate(p_values):
    before = "유의" if p < alpha else "비유의"
    after = "유의" if p < bonferroni_alpha else "비유의"
    print(f"테스트 {i+1}: p={p:.3f} | 보정 전: {before} | 보정 후: {after}")

김개발 씨는 Bonferroni 보정이라는 이름을 처음 들었습니다. 어려워 보이는 이름과 달리, 그 원리는 놀라울 정도로 간단했습니다.

박시니어 씨가 종이에 간단한 수식을 적었습니다. "보정된 유의수준은 원래 유의수준 나누기 검정 횟수예요.

우리가 10번 검정을 한다면, 0.05를 10으로 나눈 0.005가 새로운 기준이 되는 거죠." 왜 이렇게 하면 될까요? Bonferroni 보정의 수학적 근거는 합집합의 확률에 있습니다.

여러 사건 중 최소 하나가 발생할 확률은 각 사건의 확률의 합보다 작거나 같습니다. 따라서 각 검정의 유의수준을 n분의 1로 줄이면, 전체 가족별 오류율을 원래 수준으로 유지할 수 있습니다.

이 방법의 가장 큰 장점은 직관적이고 계산이 쉽다는 것입니다. 특별한 소프트웨어 없이도 계산기로 바로 구할 수 있습니다.

또한 검정들이 독립적이든 상관되어 있든 상관없이 적용할 수 있습니다. 위의 코드를 보면, 원래 유의수준 0.05에서는 4개의 테스트가 유의미했습니다.

하지만 Bonferroni 보정을 적용하니 기준이 0.005로 엄격해졌고, 이제는 p-value가 0.01인 테스트 8만 유의미하게 남았습니다. 김개발 씨가 걱정스러운 표정을 지었습니다.

"그러면 실제로 효과가 있는 것도 놓칠 수 있지 않나요?" 박시니어 씨가 고개를 끄덕였습니다. "좋은 질문이에요.

그게 바로 Bonferroni의 단점이에요. 너무 **보수적(conservative)**이라서 **통계적 검정력(statistical power)**이 떨어질 수 있어요.

진짜 효과가 있는 것도 효과가 없다고 결론내릴 위험이 있죠." 이것을 제2종 오류(Type II Error) 또는 **거짓 음성(False Negative)**이라고 합니다. Bonferroni 보정은 제1종 오류를 엄격하게 통제하는 대신, 제2종 오류의 위험을 높입니다.

특히 검정 횟수가 많아지면 문제가 심각해집니다. 예를 들어 유전자 연구에서 2만 개의 유전자를 동시에 검정한다면, 유의수준은 0.05 나누기 20000, 즉 0.0000025가 됩니다.

이 정도로 낮은 p-value는 정말 강력한 효과가 아니면 달성하기 어렵습니다. 그래서 Bonferroni 보정은 검정 횟수가 적고, 거짓 양성을 철저히 피해야 하는 상황에서 주로 사용됩니다.

예를 들어 신약 승인 심사처럼 잘못된 결론이 심각한 결과를 초래할 수 있는 경우에 적합합니다.

실전 팁

💡 - 검정 횟수가 적을 때(10개 이하) Bonferroni를 고려하세요

  • 거짓 양성의 비용이 높은 상황에서 적합합니다

3. Holm 보정

김개발 씨가 다시 물었습니다. "Bonferroni보다 덜 엄격한 방법은 없나요?" 박시니어 씨가 고개를 끄덕였습니다.

"있어요. Holm 방법이라고 하는데, Bonferroni의 개선 버전이에요.

똑같이 FWER을 통제하면서도 조금 더 관대해요."

**Holm 보정(Holm-Bonferroni method)**은 Bonferroni의 단계적 개선 버전입니다. p-value를 정렬한 후 순차적으로 검정하며, 더 작은 p-value에는 더 관대한 기준을 적용합니다.

마치 시험에서 쉬운 문제부터 풀면서 점차 어려운 문제로 나아가는 것과 같습니다. 같은 오류율을 유지하면서 더 많은 유의미한 결과를 발견할 수 있습니다.

다음 코드를 살펴봅시다.

from statsmodels.stats.multitest import multipletests

p_values = [0.03, 0.12, 0.04, 0.67, 0.02,
            0.45, 0.08, 0.01, 0.23, 0.15]

# Holm 보정 적용
rejected_holm, corrected_holm, _, _ = multipletests(
    p_values, alpha=0.05, method='holm'
)

# Bonferroni와 비교
rejected_bonf, corrected_bonf, _, _ = multipletests(
    p_values, alpha=0.05, method='bonferroni'
)

print("테스트 | 원본 p | Bonf 보정 | Holm 보정")
print("-" * 45)
for i, p in enumerate(p_values):
    bonf_result = "유의" if rejected_bonf[i] else "-"
    holm_result = "유의" if rejected_holm[i] else "-"
    print(f"  {i+1:2d}   | {p:.3f} |   {bonf_result:^5}   |   {holm_result:^5}")

print(f"\nBonferroni 유의 결과: {sum(rejected_bonf)}개")
print(f"Holm 유의 결과: {sum(rejected_holm)}개")

박시니어 씨가 화이트보드에 그림을 그리며 설명을 시작했습니다. "Holm 방법은 1979년에 스웨덴 통계학자 Sture Holm이 개발했어요.

Bonferroni의 아이디어를 좀 더 영리하게 적용한 거죠." Holm 방법의 핵심은 **단계적 접근(step-down procedure)**입니다. 모든 p-value에 동일한 기준을 적용하는 대신, p-value를 작은 것부터 큰 것 순으로 정렬한 후 순차적으로 검정합니다.

구체적인 절차는 이렇습니다. 먼저 모든 p-value를 오름차순으로 정렬합니다.

그다음 가장 작은 p-value부터 시작해서, 각 p-value를 그에 맞는 조정된 유의수준과 비교합니다. 첫 번째(가장 작은) p-value는 alpha 나누기 n과 비교합니다.

이것은 Bonferroni와 동일합니다. 하지만 두 번째 p-value는 alpha 나누기 (n-1)과 비교하고, 세 번째는 alpha 나누기 (n-2)와 비교합니다.

이런 식으로 뒤로 갈수록 기준이 점점 관대해집니다. 중요한 점은, 어느 단계에서든 귀무가설을 기각하지 못하면 그 이후의 모든 가설도 기각하지 않는다는 것입니다.

마치 계단을 오르다가 한 칸에서 멈추면 그 위 계단은 더 이상 오르지 않는 것과 같습니다. 왜 이 방법이 Bonferroni보다 나을까요?

Bonferroni는 모든 검정에 가장 엄격한 기준을 적용합니다. 하지만 이미 몇 개의 가설을 기각했다면, 남은 가설에 대해서는 조금 덜 엄격해도 전체 오류율은 여전히 통제됩니다.

Holm 방법은 이 점을 활용합니다. 위의 코드 결과를 보면, Bonferroni로는 1개만 유의미했지만 Holm으로는 더 많은 결과가 유의미하게 나올 수 있습니다.

물론 구체적인 p-value 분포에 따라 결과는 달라집니다. 김개발 씨가 물었습니다.

"그러면 항상 Holm을 쓰는 게 낫지 않나요?" 박시니어 씨가 답했습니다. "맞아요.

Holm은 Bonferroni보다 **균등하게 더 강력(uniformly more powerful)**해요. 같은 FWER을 유지하면서 더 많은 진짜 효과를 발견할 수 있죠.

그래서 FWER 통제가 필요할 때는 Bonferroni 대신 Holm을 쓰는 것이 좋아요." 다만 Holm 방법도 여전히 보수적입니다. 검정 횟수가 수백, 수천 개로 늘어나면 여전히 검정력 문제가 발생합니다.

이런 경우에는 FWER 대신 다른 오류율을 통제하는 방법이 필요합니다.

실전 팁

💡 - FWER 통제가 필요할 때 Bonferroni 대신 Holm을 사용하세요

  • 단계적 방법이므로 p-value 정렬 순서가 중요합니다

4. FDR과 Benjamini Hochberg

박시니어 씨가 새로운 개념을 꺼냈습니다. "검정이 수천 개 수준이면 FWER 대신 FDR을 통제하는 게 나아요." 김개발 씨가 의아한 표정을 지었습니다.

"FDR이요? 그건 또 뭔가요?" 박시니어 씨가 웃으며 설명을 시작했습니다.

**FDR(False Discovery Rate)**은 유의미하다고 판정한 결과들 중에서 거짓 양성의 비율입니다. FWER이 "한 번이라도 틀리면 안 된다"라면, FDR은 "어느 정도 틀려도 괜찮다"는 접근입니다.

마치 1000개의 이력서 중 좋은 후보를 뽑을 때, 완벽하게 다 맞추려 하기보다 좋은 후보 대부분을 찾되 일부 오류는 감수하는 것과 같습니다.

다음 코드를 살펴봅시다.

from statsmodels.stats.multitest import multipletests
import numpy as np

# 1000개의 가설 검정 시뮬레이션
np.random.seed(42)
n_tests = 1000
n_true_effects = 50  # 실제로 효과가 있는 것

# 대부분은 효과 없음 (균등분포 p-value)
p_values = np.random.uniform(0, 1, n_tests)
# 일부는 실제 효과 있음 (낮은 p-value)
p_values[:n_true_effects] = np.random.beta(1, 20, n_true_effects)

# Benjamini-Hochberg FDR 보정
rejected_bh, corrected_bh, _, _ = multipletests(
    p_values, alpha=0.05, method='fdr_bh'
)

# Bonferroni 비교
rejected_bonf, _, _, _ = multipletests(
    p_values, alpha=0.05, method='bonferroni'
)

print(f"전체 검정 수: {n_tests}")
print(f"실제 효과 있는 것: {n_true_effects}")
print(f"\nBonferroni 발견: {sum(rejected_bonf)}개")
print(f"BH-FDR 발견: {sum(rejected_bh)}개")

김개발 씨는 **FDR(False Discovery Rate)**이라는 새로운 개념에 대해 궁금해졌습니다. FWER과 무엇이 다른 걸까요?

박시니어 씨가 비유를 들어 설명했습니다. "스팸 메일 필터를 생각해보세요.

FWER 방식은 정상 메일을 스팸으로 잘못 분류하는 일이 단 한 번도 없어야 한다는 거예요. 반면 FDR 방식은 스팸으로 분류한 것 중 5%까지는 정상 메일이어도 괜찮다는 거죠." **FWER(Family-Wise Error Rate)**은 모든 검정을 통틀어 한 번이라도 거짓 양성이 발생할 확률을 통제합니다.

반면 FDR은 유의미하다고 판정한 것들 중에서 거짓 양성이 차지하는 비율의 기댓값을 통제합니다. 수식으로 표현하면, FDR은 (거짓 양성의 수) 나누기 (전체 발견의 수)의 기댓값입니다.

만약 100개를 유의미하다고 판정했는데 그중 5개가 거짓 양성이라면, FDR은 5%입니다. 이 개념은 1995년 이스라엘의 통계학자 Yoav Benjamini와 Yosef Hochberg가 제안했습니다.

그들이 개발한 Benjamini-Hochberg(BH) 방법은 FDR을 통제하는 가장 대표적인 방법입니다. BH 방법의 절차는 이렇습니다.

먼저 모든 p-value를 오름차순으로 정렬합니다. 각 p-value에 순위를 매기고, 순위를 k라 하면 k번째 p-value가 (k/n) 곱하기 alpha보다 작은지 확인합니다.

이 조건을 만족하는 가장 큰 k를 찾아서, 그 k까지의 모든 가설을 기각합니다. 위의 코드는 1000개의 가설 검정을 시뮬레이션합니다.

그중 50개만 실제 효과가 있고, 나머지 950개는 효과가 없습니다. Bonferroni는 너무 엄격해서 몇 개만 발견하지만, BH 방법은 훨씬 더 많이 발견합니다.

김개발 씨가 물었습니다. "그러면 FDR은 오류를 더 많이 허용하는 건가요?" 박시니어 씨가 답했습니다.

"관점의 차이예요. FWER은 실수 자체를 최소화하려 하고, FDR은 발견의 질을 보장하려 해요.

유전체 연구처럼 수만 개의 검정을 해야 할 때, FWER을 고집하면 진짜 효과가 있는 것도 거의 발견 못 해요. 그럴 때 FDR이 더 현실적인 선택이죠." FDR 접근은 **탐색적 연구(exploratory research)**에 특히 적합합니다.

완벽한 정확성보다는 흥미로운 후보들을 많이 찾아내고, 이후 추가 연구로 검증하는 전략입니다.

실전 팁

💡 - 검정 횟수가 많고 탐색적 분석일 때 FDR을 사용하세요

  • BH 방법은 p-value들이 독립적이거나 양의 상관일 때 잘 작동합니다

5. 보정 방법 선택 가이드

김개발 씨의 머릿속이 복잡해졌습니다. "Bonferroni, Holm, FDR...

그래서 언제 뭘 써야 하나요?" 박시니어 씨가 정리를 시작했습니다. "상황에 따라 달라요.

핵심은 '거짓 양성의 비용'과 '검정 횟수'예요."

다중 비교 보정 방법의 선택은 연구 목적과 상황에 따라 달라집니다. **FWER 통제(Bonferroni, Holm)**는 거짓 양성의 비용이 높을 때, **FDR 통제(BH)**는 탐색적 연구나 검정 수가 많을 때 적합합니다.

마치 중요한 계약서에는 꼼꼼한 검토가 필요하고, 아이디어 브레인스토밍에서는 자유로운 발상이 필요한 것과 같습니다.

다음 코드를 살펴봅시다.

from statsmodels.stats.multitest import multipletests

def select_correction_method(p_values, context):
    """
    상황에 맞는 다중 비교 보정 방법 선택 가이드
    """
    n = len(p_values)
    results = {}

    # 모든 방법 적용
    methods = {
        'bonferroni': '매우 보수적, FWER 통제',
        'holm': '보수적, FWER 통제 (Bonferroni 개선)',
        'fdr_bh': '덜 보수적, FDR 통제'
    }

    print(f"검정 횟수: {n}개")
    print(f"상황: {context}\n")

    for method, desc in methods.items():
        rejected, _, _, _ = multipletests(p_values, alpha=0.05, method=method)
        results[method] = sum(rejected)
        print(f"{method:12}: {sum(rejected):3}개 발견 ({desc})")

    # 추천
    if context == "임상시험" or context == "규제승인":
        recommend = "holm"
    elif n > 100 or context == "탐색연구":
        recommend = "fdr_bh"
    else:
        recommend = "holm"

    print(f"\n추천 방법: {recommend}")
    return results

# 예시 실행
p_values = [0.001, 0.01, 0.02, 0.03, 0.04, 0.06, 0.08, 0.1, 0.2, 0.5]
select_correction_method(p_values, "A/B 테스트")

박시니어 씨가 화이트보드에 표를 그리기 시작했습니다. "보정 방법 선택은 크게 두 가지 질문에 답하면 돼요." 첫 번째 질문은 **"거짓 양성의 비용이 얼마나 큰가?"**입니다.

신약 승인, 의료 기기 인증, 안전 관련 결정처럼 거짓 양성 하나가 심각한 결과를 초래할 수 있다면 FWER 통제가 필수입니다. 잘못된 결론이 환자의 생명이나 막대한 비용 손실로 이어질 수 있기 때문입니다.

반면 유전자 발현 연구, 탐색적 데이터 분석, 초기 단계 A/B 테스트처럼 일부 거짓 양성이 있어도 후속 연구에서 걸러낼 수 있다면 FDR 통제가 더 효율적입니다. 두 번째 질문은 **"검정을 몇 개나 하는가?"**입니다.

검정 횟수가 적으면(대략 20개 이하) FWER과 FDR의 차이가 크지 않습니다. 하지만 검정이 수백, 수천 개로 늘어나면 FWER 통제는 지나치게 보수적이 되어 거의 아무것도 발견하지 못할 수 있습니다.

박시니어 씨가 정리했습니다. "일반적인 가이드라인은 이래요." Bonferroni: 검정 수가 적고(5개 이하), 계산이 간단해야 할 때.

하지만 대부분의 경우 Holm이 더 나으므로 잘 안 씁니다. Holm: FWER 통제가 필요할 때의 기본 선택.

Bonferroni보다 항상 같거나 더 많이 발견합니다. Benjamini-Hochberg(FDR): 검정 수가 많거나 탐색적 연구일 때.

유전체학, 뇌영상학 등 대규모 검정에서 표준입니다. 김개발 씨가 자신의 상황을 떠올렸습니다.

"저는 10개 A/B 테스트를 했으니까, Holm이나 FDR 중에 선택하면 되겠네요?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

이번 테스트가 제품 출시 결정에 직접 영향을 준다면 Holm을, 단순히 추가 테스트 후보를 찾는 거라면 FDR을 쓰면 돼요." 한 가지 더 중요한 점이 있습니다. 사전에 계획하기입니다.

이상적으로는 데이터를 보기 전에 어떤 보정 방법을 사용할지 정해야 합니다. 결과를 본 후에 방법을 바꾸면 **p-해킹(p-hacking)**이라는 비윤리적 관행에 해당할 수 있습니다.

실전 팁

💡 - 분석 전에 보정 방법을 미리 결정하세요

  • 확정적 결론이 필요하면 FWER, 후보 탐색이면 FDR을 선택하세요

6. 실전 A/B 테스트 적용

이제 김개발 씨가 직접 자신의 A/B 테스트 데이터에 다중 비교 보정을 적용해볼 차례입니다. 박시니어 씨가 옆에서 지켜보며 조언했습니다.

"실제 업무에서 어떻게 쓰는지 같이 해보죠."

실제 A/B 테스트에서 다중 비교 보정을 적용하는 전체 워크플로우를 다룹니다. 가설 설정부터 검정, 보정, 결과 해석까지 단계별로 진행합니다.

마치 요리 레시피처럼 따라 하면 되는 실전 가이드입니다. 이 과정을 통해 통계적으로 신뢰할 수 있는 결론을 내릴 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests

# 실제 A/B 테스트 데이터 시뮬레이션
np.random.seed(42)
tests = {
    '버튼_색상': {'control': 0.032, 'variant': 0.038, 'n': 5000},
    '헤드라인_텍스트': {'control': 0.032, 'variant': 0.033, 'n': 5000},
    '이미지_위치': {'control': 0.032, 'variant': 0.041, 'n': 5000},
    'CTA_문구': {'control': 0.032, 'variant': 0.035, 'n': 5000},
    '레이아웃': {'control': 0.032, 'variant': 0.034, 'n': 5000},
}

results = []
for name, data in tests.items():
    # 이항 분포로 클릭 시뮬레이션
    ctrl_clicks = np.random.binomial(data['n'], data['control'])
    var_clicks = np.random.binomial(data['n'], data['variant'])

    # 비율 검정
    count = [var_clicks, ctrl_clicks]
    nobs = [data['n'], data['n']]
    stat, p_value = stats.proportions_ztest(count, nobs, alternative='larger')

    results.append({'테스트': name, 'p_value': p_value,
                    'control_rate': ctrl_clicks/data['n'],
                    'variant_rate': var_clicks/data['n']})

df = pd.DataFrame(results)
p_values = df['p_value'].values

# 보정 적용
_, df['p_holm'], _, _ = multipletests(p_values, method='holm')
_, df['p_bh'], _, _ = multipletests(p_values, method='fdr_bh')

print(df.to_string(index=False))

김개발 씨가 자신의 A/B 테스트 데이터를 열었습니다. 버튼 색상, 헤드라인, 이미지 위치 등 5가지 UI 요소를 동시에 테스트한 결과입니다.

박시니어 씨가 단계별로 안내했습니다. "먼저 각 테스트의 p-value를 구하고, 그다음 보정을 적용하는 거예요." 위의 코드에서 먼저 각 테스트에 대해 **비율 검정(z-test for proportions)**을 수행합니다.

각 테스트는 대조군(control)과 실험군(variant)의 클릭률을 비교합니다. 귀무가설은 "두 그룹의 클릭률이 같다"이고, 대립가설은 "실험군의 클릭률이 더 높다"입니다.

각 테스트에서 p-value를 구한 후, 이것을 그냥 0.05와 비교하면 안 됩니다. 반드시 보정을 거쳐야 합니다.

코드에서는 Holm 보정Benjamini-Hochberg 보정 두 가지를 모두 적용합니다. multipletests 함수가 편리하게 보정된 p-value를 반환해줍니다.

결과를 해석하는 방법은 이렇습니다. 보정된 p-value가 0.05 미만이면 그 테스트는 통계적으로 유의미한 것입니다.

원래 p-value가 0.05 미만이었어도, 보정 후에는 0.05를 넘을 수 있습니다. 김개발 씨가 결과를 살펴보았습니다.

"이미지 위치 테스트만 보정 후에도 유의미하게 남았네요. 처음에 3개가 유의미하다고 생각했는데, 실제로는 1개뿐이었군요." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 보정을 안 했으면 효과 없는 변경을 적용할 뻔했죠.

그러면 리소스 낭비에 사용자 경험도 나빠질 수 있었어요." 보고서를 작성할 때는 원래 p-value와 보정된 p-value를 둘 다 보고하는 것이 좋습니다. 어떤 보정 방법을 사용했는지도 명시해야 합니다.

이렇게 하면 다른 사람들이 결과를 재현하고 검증할 수 있습니다. 한 가지 주의할 점이 있습니다.

보정은 계획된 검정에만 적용해야 합니다. 데이터를 이것저것 탐색하다가 우연히 발견한 패턴에 보정을 적용하는 것은 의미가 없습니다.

그런 발견은 새로운 가설로 취급하고, 별도의 데이터로 검증해야 합니다.

실전 팁

💡 - 보정된 p-value와 원본 p-value를 함께 보고하세요

  • 분석 계획서에 사용할 보정 방법을 미리 명시하세요

7. 주의사항과 흔한 실수

김개발 씨가 마무리 질문을 했습니다. "다중 비교 보정을 할 때 주의해야 할 점이 또 있나요?" 박시니어 씨가 잠시 생각하더니 답했습니다.

"몇 가지 흔한 실수가 있어요. 이것만 피하면 대부분 괜찮아요."

다중 비교 보정에서 자주 발생하는 실수와 주의사항을 정리합니다. 보정 과다/과소 적용, p-해킹, 보고 편향 등의 문제를 다룹니다.

마치 운전면허 시험에서 배우는 방어 운전처럼, 이런 함정을 미리 알고 피하는 것이 중요합니다. 올바른 통계 관행은 신뢰할 수 있는 결론의 기초입니다.

다음 코드를 살펴봅시다.

# 흔한 실수 예시와 올바른 접근

# 실수 1: 검정 후 보정 방법 선택하기 (p-해킹)
def bad_practice(p_values):
    """잘못된 접근: 결과 보고 유리한 방법 선택"""
    from statsmodels.stats.multitest import multipletests

    methods = ['bonferroni', 'holm', 'fdr_bh']
    for method in methods:
        rejected, _, _, _ = multipletests(p_values, method=method)
        if sum(rejected) > 0:
            return method, sum(rejected)  # 가장 많이 발견한 방법 선택
    return None, 0

# 올바른 접근: 사전에 방법 결정
def good_practice(p_values, predetermined_method='holm'):
    """올바른 접근: 미리 정한 방법으로 보정"""
    from statsmodels.stats.multitest import multipletests

    rejected, corrected_p, _, _ = multipletests(
        p_values, alpha=0.05, method=predetermined_method
    )
    return {
        'method': predetermined_method,
        'n_significant': sum(rejected),
        'corrected_p': corrected_p
    }

# 예시 실행
p_values = [0.01, 0.03, 0.04, 0.06, 0.12]
result = good_practice(p_values, 'holm')
print(f"사전 결정 방법: {result['method']}")
print(f"유의미한 결과: {result['n_significant']}개")

박시니어 씨가 흔한 실수들을 하나씩 짚어주었습니다. 첫 번째 실수는 보정 방법을 결과 후에 선택하는 것입니다.

여러 보정 방법을 적용해보고, 가장 마음에 드는 결과를 선택하는 것은 **p-해킹(p-hacking)**의 일종입니다. "이 방법으로 하면 유의미하네요"라고 말하면서 유리한 방법만 보고하는 것은 과학적으로 부정직합니다.

올바른 방법은 분석 계획 단계에서 보정 방법을 미리 정하고, 그 방법만 사용하는 것입니다. 결과가 마음에 들지 않더라도 바꾸면 안 됩니다.

두 번째 실수는 보정을 너무 넓게 또는 좁게 적용하는 것입니다. 어떤 검정들을 하나의 "가족"으로 묶어서 함께 보정할지는 중요한 결정입니다.

관련 없는 검정들까지 모두 묶으면 지나치게 보수적이 됩니다. 반대로 관련된 검정들을 따로따로 보정하면 전체 오류율이 통제되지 않습니다.

일반적으로 같은 연구 질문에 대한 검정들을 하나의 가족으로 묶습니다. 예를 들어 "어떤 UI 요소가 클릭률에 영향을 주는가?"라는 질문에 대한 10개 검정은 함께 보정해야 합니다.

하지만 완전히 다른 연구 질문에 대한 검정까지 묶을 필요는 없습니다. 세 번째 실수는 보정된 p-value만 보고하고 원래 값을 숨기는 것입니다.

투명성을 위해 원래 p-value와 보정된 p-value, 사용한 보정 방법을 모두 보고해야 합니다. 이렇게 해야 독자가 스스로 판단할 수 있고, 다른 연구자가 결과를 재현할 수 있습니다.

네 번째 실수는 탐색적 분석에 보정을 적용하는 것입니다. 데이터를 이리저리 살펴보다가 흥미로운 패턴을 발견했다면, 그것은 새로운 가설입니다.

이 가설을 검증하려면 새로운 데이터가 필요합니다. 같은 데이터로 가설을 발견하고 검증까지 하면, 보정을 아무리 해도 의미가 없습니다.

김개발 씨가 고개를 끄덕였습니다. "결국 핵심은 정직하게 분석하는 거네요." 박시니어 씨가 웃으며 답했습니다.

"맞아요. 통계는 도구일 뿐이에요.

그 도구를 정직하게 사용하느냐가 중요하죠. 다중 비교 보정도 결국 정직한 결론을 내리기 위한 도구예요."

실전 팁

💡 - 분석 계획서를 작성하고 보정 방법을 미리 문서화하세요

  • 탐색적 발견과 확정적 검증을 구분하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#MultipleTestingCorrection#Bonferroni#FDR#StatisticalAnalysis#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.