Permutation Tests — 비모수 가설 검정의 일반화

Woodward 14.8 — Fisher 의 randomization test 부터 현대 Monte Carlo permutation 까지

Woodward Ch.14.8 의 순열 검정 (permutation tests) 을 정리한다. Fisher 의 randomization test 일반화, 정확 vs Monte Carlo 변형, 두 그룹/회귀/상관에서의 적용, 부트스트랩과의 차이, 분포 가정 없는 가설 검정의 정당성을 자세히 다룬다.

Experimentation
Fundamentals
저자

Kwangmin Kim

공개

2026년 05월 08일

1 도입 — Fisher 의 1935 발견의 일반화

A-MAX2-1 에서 Fisher 의 Lady Tasting Tearandomization test 를 다뤘다. 이 글은 그 절차를 일반화permutation test 를 다룬다.

Permutation test 의 핵심 통찰: 귀무가설이 참이라면 그룹 라벨이 무작위로 배정된 것 과 동등. 따라서 라벨을 무작위로 섞은 자료의 검정 통계량 분포가 영가설 분포.

2 Permutation Test 의 정의

정의: Permutation Test

귀무가설 \(H_0\) 하에서 그룹 라벨 (또는 그에 해당하는 변수) 이 교환 가능 (exchangeable) 함을 가정. 라벨을 모든 가능한 방식으로 재배치하여 영가설 분포 직접 구성.

절차:

  1. 관측 검정 통계량 \(T_{\text{obs}}\) 계산
  2. 자료의 그룹 라벨을 무작위 셔플
  3. 셔플된 자료에서 검정 통계량 \(T^*\) 계산
  4. 2, 3 을 모든 가능한 셔플 또는 \(B\) 회 반복
  5. \(T^*\) 분포에서 \(T_{\text{obs}}\)극단성 = p 값

2.1 정확 vs Monte Carlo

  • 정확 Permutation: 모든 가능한 셔플 (\(\binom{n_1+n_2}{n_1}\) 가지) 열거. 작은 표본에서만 가능.
  • Monte Carlo Permutation: \(B\) 개 무작위 셔플만 사용. 큰 표본에 표준.

2.2 사례 — 두 그룹 평균 비교

import numpy as np

def perm_test_two_means(group_A, group_B, n_perm=5000):
    obs_diff = group_A.mean() - group_B.mean()
    combined = np.concatenate([group_A, group_B])
    n_A = len(group_A)

    perm_diffs = []
    for _ in range(n_perm):
        np.random.shuffle(combined)
        perm_diffs.append(combined[:n_A].mean() - combined[n_A:].mean())

    p_value = np.mean(np.abs(perm_diffs) >= np.abs(obs_diff))
    return p_value, perm_diffs

이 절차는 분포 가정 X. 교환 가능성 만 가정.

3 교환 가능성 (Exchangeability)

정의: 교환 가능성

자료 \((X_1, X_2, \ldots, X_n)\) 의 결합 분포가 어떤 순열 에도 불변 하다는 성질.

\[ P(X_1, X_2, \ldots, X_n) = P(X_{\pi(1)}, X_{\pi(2)}, \ldots, X_{\pi(n)}) \]

모든 순열 \(\pi\) 에 대해.

3.1 의미

  • 모든 자료점이 같은 분포에서 추출 인 경우
  • \(H_0\) 하의 두 그룹 비교: 그룹 A 와 B 가 같은 모집단에서 추출 → 라벨 교환 가능
  • 의존성 자료에서 일반적으로 깨짐

3.2 Permutation Test 의 적용 조건

  • 관측이 교환 가능
  • \(H_0\) 하에서 그룹 라벨이 무관

A/B 테스트의 무작위 배정 자료에서 자연스럽게 충족.

직관 — 교환 가능성과 i.i.d.

i.i.d. (독립 동일 분포) → 교환 가능. 그러나 역은 아님.

  • i.i.d. + 베이즈 모형 → 교환 가능 (de Finetti 정리)
  • 시계열 자기 상관 → 교환 가능 X
  • 클러스터 자료 → 교환 가능 X (클러스터 단위에서는 가능)

A/B 테스트의 일반 자료는 사용자 단위에서 i.i.d. 가까움 → 교환 가능. Permutation test 적용 OK.

반복 측정 자료에서 사용자 = 클러스터로 봐야 함. 사용자 단위 permutation 또는 cluster permutation 적용.

4 Permutation 의 수학적 정당성

4.1 정확 Permutation

모든 셔플의 비율로 p 값. 정확한 α 보장 (분포 가정 없이).

4.2 Monte Carlo Permutation

\(B\) 회 무작위 셔플. p 값 = 극단 비율 + \(1/(B+1)\) (수정).

p_mc = (1 + np.sum(np.abs(perm_diffs) >= np.abs(obs_diff))) / (n_perm + 1)

Plus 1 보정이 작은 p 값의 정확성 향상.

5 두 그룹 비교 — 자세히

5.1 평균 차이 검정

위에서 다룸. Welch t 와 거의 동등.

5.2 중앙값 차이 검정

def perm_median_diff(group_A, group_B, n_perm=5000):
    obs_diff = np.median(group_A) - np.median(group_B)
    combined = np.concatenate([group_A, group_B])
    n_A = len(group_A)

    perm_diffs = []
    for _ in range(n_perm):
        np.random.shuffle(combined)
        perm_diffs.append(np.median(combined[:n_A]) - np.median(combined[n_A:]))

    return np.mean(np.abs(perm_diffs) >= np.abs(obs_diff))

전통 중앙값 차이 검정 공식 X. Permutation 이 표준.

5.3 Mann-Whitney U 검정

순위 기반 비모수 검정. Permutation 의 특수 사례.

def perm_mann_whitney(group_A, group_B, n_perm=5000):
    combined = np.concatenate([group_A, group_B])
    ranks = np.argsort(np.argsort(combined)) + 1
    n_A = len(group_A)

    obs_U = np.sum(ranks[:n_A]) - n_A * (n_A + 1) / 2

    perm_Us = []
    for _ in range(n_perm):
        np.random.shuffle(ranks)
        perm_Us.append(np.sum(ranks[:n_A]) - n_A * (n_A + 1) / 2)

    return np.mean(np.abs(perm_Us - n_A*(n_A+len(group_B)+1)/2) >=
                   np.abs(obs_U - n_A*(n_A+len(group_B)+1)/2))

6 회귀 계수의 Permutation Test

회귀 계수 \(\beta_j\) 의 검정.

6.1 절차

def perm_regression(X, Y, var_idx, n_perm=5000):
    n = len(Y)
    X_design = np.column_stack([np.ones(n), X])
    obs_beta = np.linalg.lstsq(X_design, Y, rcond=None)[0][var_idx + 1]

    perm_betas = []
    for _ in range(n_perm):
        # X 의 var_idx 번 변수를 셔플
        X_perm = X.copy()
        X_perm[:, var_idx] = np.random.permutation(X[:, var_idx])
        X_perm_design = np.column_stack([np.ones(n), X_perm])
        beta = np.linalg.lstsq(X_perm_design, Y, rcond=None)[0][var_idx + 1]
        perm_betas.append(beta)

    return np.mean(np.abs(perm_betas) >= np.abs(obs_beta))

이 절차로 분포 가정 없이 회귀 계수 p 값.

6.2 다중 변수 회귀의 한계

다중 변수 회귀에서 한 변수의 마진 효과 를 permutation 으로 검정 시, 다른 변수와의 상관 이 영향. 부분 잔차 (partial residual) 사용 권장.

7 상관 계수 검정

def perm_correlation(X, Y, n_perm=5000):
    obs_corr = np.corrcoef(X, Y)[0, 1]

    perm_corrs = []
    for _ in range(n_perm):
        Y_perm = np.random.permutation(Y)
        perm_corrs.append(np.corrcoef(X, Y_perm)[0, 1])

    return np.mean(np.abs(perm_corrs) >= np.abs(obs_corr))

8 Permutation vs Bootstrap

측면 Permutation Bootstrap
추출 비복원 (셔플) 복원
목적 가설 검정 추정
가정 교환 가능성 i.i.d.
정확성 \(H_0\) 분포 정확 점근
출력 p 값 CI, SE

8.1 사용 시점

  • p 값 만 필요 → Permutation
  • 효과 크기 + CI 필요 → Bootstrap
  • 둘 다 필요 → 둘 다 사용

9 사례 — Permutation 의 정확성

작은 표본에서 전통 t 검정 vs permutation 검정 비교.

import numpy as np
from scipy.stats import ttest_ind

# 매우 작은 표본 + 비대칭 자료
group_A = np.array([1, 2, 3, 4, 5])
group_B = np.array([2, 3, 4, 5, 100])  # 이상치

# t 검정
t_stat, p_t = ttest_ind(group_A, group_B, equal_var=False)
print(f"Welch t: t = {t_stat:.3f}, p = {p_t:.4f}")

# Permutation
n_A = len(group_A)
combined = np.concatenate([group_A, group_B])
obs_diff = group_A.mean() - group_B.mean()

# 정확 permutation (모든 가능한 셔플)
from itertools import combinations
all_perms = []
for idx_A in combinations(range(10), n_A):
    A_perm = combined[list(idx_A)]
    B_perm = combined[[i for i in range(10) if i not in idx_A]]
    all_perms.append(A_perm.mean() - B_perm.mean())

p_exact = np.mean(np.abs(all_perms) >= np.abs(obs_diff))
print(f"정확 Permutation p = {p_exact:.4f}")

작은 표본에서 t 검정과 permutation 의 결과가 다를 수 있음. Permutation 이 분포 가정 없이 정확.

10 Genome-Wide Permutation

Genome-Wide Permutation

수백만 SNP 의 동시 검정에서 영가설 분포 직접 추정.

def gwas_permutation(genotypes, phenotype, n_perm=1000):
    """수많은 SNP 의 영가설 분포 시뮬레이션"""
    n_snps = genotypes.shape[1]

    # 관측 통계량
    obs_stats = []
    for j in range(n_snps):
        corr = np.corrcoef(genotypes[:, j], phenotype)[0, 1]
        obs_stats.append(corr**2)

    # Permutation 분포 — 표현형 셔플
    perm_max_stats = []
    for _ in range(n_perm):
        phen_perm = np.random.permutation(phenotype)
        perm_corrs = [np.corrcoef(genotypes[:, j], phen_perm)[0, 1]**2
                      for j in range(n_snps)]
        perm_max_stats.append(np.max(perm_corrs))  # 최대 통계량

    # 각 SNP 의 family-wise p 값
    fwer_p = []
    for stat in obs_stats:
        fwer_p.append(np.mean(np.array(perm_max_stats) >= stat))

    return obs_stats, fwer_p

이 절차가 family-wise error rate (FWER) 자동 통제. Bonferroni 보다 정확 (변수 간 상관 활용).

11 A/B 테스트의 응용

11.1 시나리오 1 — 두 변형의 매출 차이

import numpy as np

n_per = 1000
control = np.random.lognormal(4, 1.0, n_per)
treatment = np.random.lognormal(4.05, 1.0, n_per)

# Permutation
obs_diff = treatment.mean() - control.mean()
combined = np.concatenate([control, treatment])

n_perm = 5000
perm_diffs = []
for _ in range(n_perm):
    np.random.shuffle(combined)
    perm_diffs.append(combined[:n_per].mean() - combined[n_per:].mean())

p = np.mean(np.abs(perm_diffs) >= np.abs(obs_diff))
print(f"Permutation p = {p:.4f}")

11.2 시나리오 2 — 다중 메트릭 동시

5 개 메트릭의 동시 permutation. 최대 통계량 사용으로 family-wise α 통제.

11.3 시나리오 3 — Subgroup 분석

특정 segment 의 효과를 permutation 으로 검정. 분포 가정 없이.

12 Permutation 의 한계

12.1 한계 1 — 큰 표본 + 정확 permutation 불가

\(n = 100\) 양 그룹: \(\binom{200}{100} \approx 9 \times 10^{58}\) 가지. 정확 permutation 불가능.

해법: Monte Carlo permutation (\(B = 10000\) 충분).

12.2 한계 2 — 의존성 자료

시계열, 클러스터에서 교환 가능성 위반. 단순 permutation 부적절.

해법: Cluster permutation (클러스터 단위 셔플), Block permutation.

12.3 한계 3 — 효과 크기 추정 X

Permutation 은 p 값 만 줌. 효과 크기 + CI 는 부트스트랩 사용.

12.4 한계 4 — 다중 변수 모형의 미묘함

다중 회귀에서 한 변수의 marginal 효과 검정 시, 다른 변수의 영향 처리 어려움.

13 Permutation 검정의 정당성 시뮬레이션

import numpy as np
from scipy.stats import ttest_ind

# H_0 하의 시뮬레이션
np.random.seed(42)
n_simulations = 5000
n_per = 30

p_values_t = []
p_values_perm = []

for _ in range(n_simulations):
    # 두 그룹 모두 같은 분포 (H_0 참)
    A = np.random.normal(0, 1, n_per)
    B = np.random.normal(0, 1, n_per)

    # t 검정
    _, p_t = ttest_ind(A, B)
    p_values_t.append(p_t)

    # Permutation
    obs_diff = A.mean() - B.mean()
    combined = np.concatenate([A, B])
    perm_diffs = []
    for _ in range(500):
        np.random.shuffle(combined)
        perm_diffs.append(combined[:n_per].mean() - combined[n_per:].mean())
    p_perm = np.mean(np.abs(perm_diffs) >= np.abs(obs_diff))
    p_values_perm.append(p_perm)

# Type I error rate (α = 0.05)
print(f"t 검정 Type I error: {np.mean(np.array(p_values_t) < 0.05):.3f}")
print(f"Permutation Type I error: {np.mean(np.array(p_values_perm) < 0.05):.3f}")
# 이상적: 0.05 근처

이 시뮬레이션이 Permutation 의 정확한 α 통제 를 직접 검증.

14 후속 — Missing Values

다음 글 A-WOO14-6 는 결측 자료의 처리 (단순 대체 방법) 를 다룬다.

15 관련 주제

선행 지식

후속 주제 (Phase A)

  • A-WOO14-6 Missing Values
  • A-WOO14-7 Multiple Imputation

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: