순열 p-값 (Permuted p-Value)

이론적 귀무 분포 없이 p-값 계산하기 — 순열 검정의 원리, 수식, 직관

이론적 귀무 분포를 가정할 수 없거나 가정이 위반될 때 사용하는 순열 기반 p-값을 다룬다. 귀무 가설 하에서 관측값 교환 가능성(exchangeability)이 순열의 수학적 근거임을 보이고, 순열 p-값 공식과 알고리즘을 단계별로 설명한다. 이론적 p-값과의 비교, 다중 검정에서 FDR 추정으로의 확장, 적용 조건과 한계까지 직관적으로 서술한다.

Statistics
저자

Kwangmin Kim

공개

2026년 04월 09일

1 개요

p-값의 이론과 실무에서 p-값은 “\(H_0\) 하에서 관측값보다 더 극단적인 검정 통계량이 나올 확률”로 정의된다. 이를 계산하려면 귀무 분포(null distribution) 를 알아야 한다.

전통적 방법은 t-분포, \(\chi^2\)-분포, F-분포 등 이론적 귀무 분포를 사용한다. 하지만 다음 상황에서 이론적 분포는 신뢰할 수 없다:

  • 표본이 너무 작아 점근 이론이 성립하지 않는다
  • 데이터가 정규성을 심하게 위반하거나 극단적 이상치가 있다
  • 검정 통계량이 특이하여 이론적 분포 자체가 없다

순열 p-값(permuted p-value) 은 이론적 분포 없이 데이터 자체로 귀무 분포를 근사한다 (James et al., 2021, ISLR 2nd ed., Ch.13.5).


2 핵심 직관: 왜 순열이 귀무 분포를 만드는가

2.1 교환 가능성 (Exchangeability)

두 집단 \(X\), \(Y\) 의 평균이 같은지 검정한다고 하자:

\[H_0: E(X) = E(Y) \quad \text{vs} \quad H_a: E(X) \neq E(Y)\]

\(H_0\) 가 참이고 두 집단의 분포가 동일하다면, “어떤 관측값이 \(X\) 집단이고 어떤 관측값이 \(Y\) 집단인지”라는 레이블은 임의적이다. 레이블을 무작위로 뒤섞어도 검정 통계량의 분포가 바뀌지 않는다.

직관: 두 봉지에서 구슬을 꺼냈을 때 두 봉지의 구슬이 같은 종류라면, 어느 봉지에서 꺼낸 구슬인지를 랜덤하게 재배정해도 무방하다. 레이블이 의미를 잃는다 — 이것이 \(H_0\) 가 참일 때의 상황이다.

반대로 \(H_a\) 가 참이라면(두 집단의 분포가 다르다), 레이블을 뒤섞으면 집단 간 차이가 희석된다. 실제 데이터에서 계산한 검정 통계량은 순열 데이터보다 더 극단적이다 — 이것이 작은 p-값으로 나타난다.

교환 가능성 조건 (Exchangeability under \(H_0\))

\(H_0\): 두 집단 \(X\), \(Y\) 의 분포가 동일하다면,

\[ (X_1, \ldots, X_{n_X}, Y_1, \ldots, Y_{n_Y}) \overset{d}{=} (\text{어떤 순열도}) \]

즉, \(n_X + n_Y\) 개 관측값의 어떤 순열도 원래 데이터와 동일한 결합 분포를 가진다.


3 순열 p-값의 수식

3.1 검정 통계량

두 집단 검정에서 흔히 사용하는 이표본 t-통계량:

\[ T = \frac{\hat{\mu}_X - \hat{\mu}_Y}{s\sqrt{\frac{1}{n_X} + \frac{1}{n_Y}}} \tag{13.11} \]

여기서 \(s = \sqrt{\dfrac{(n_X-1)s_X^2 + (n_Y-1)s_Y^2}{n_X + n_Y - 2}}\) 는 풀링된 표준편차 추정량이다. 큰 \(|T|\)\(H_a\) 에 대한 증거를 제공한다.

3.2 순열 p-값 공식

순열 p-값 (ISLR 식 13.12)

원래 데이터에서 계산한 검정 통계량을 \(T\), \(B\) 번 순열에서 계산한 검정 통계량을 \(T^{*1}, \ldots, T^{*B}\) 라 하면:

\[ p\text{-value} = \frac{\sum_{b=1}^{B} \mathbf{1}_{(|T^{*b}| \geq |T|)}}{B} \tag{13.12} \]

즉, 순열 검정 통계량 중 실제 관측값만큼 또는 더 극단적인 것의 비율이다.

수식 해석:

  • \(\mathbf{1}_{(|T^{*b}| \geq |T|)}\): \(b\) 번째 순열의 검정 통계량이 원래 통계량보다 극단적이면 1, 아니면 0
  • 분자: 극단적인 순열의 수
  • 분모 \(B\): 전체 순열 수 (통상 \(B = 10{,}000\))
  • 결과: \([0, 1]\) 사이의 값 → 이것이 순열 p-값

4 알고리즘 — 단계별 절차

Algorithm 13.3 순열 p-값 계산 절차

입력: \(X\) 집단의 \(n_X\) 개 관측값, \(Y\) 집단의 \(n_Y\) 개 관측값, 반복 횟수 \(B\)

  1. 원래 데이터에서 검정 통계량 \(T\) 를 계산한다.

  2. \(b = 1, \ldots, B\) 에 대해:

      1. \(n_X + n_Y\) 개 관측값을 무작위로 섞는다.
        처음 \(n_X\) 개를 \(x_1^*, \ldots, x_{n_X}^*\), 나머지 \(n_Y\) 개를 \(y_1^*, \ldots, y_{n_Y}^*\) 로 지정한다.
      1. 섞인 데이터에서 검정 통계량 \(T^{*b}\) 를 계산한다.
  3. 순열 p-값 = \(\dfrac{\sum_{b=1}^{B} \mathbf{1}_{(|T^{*b}| \geq |T|)}}{B}\)

\(n_X + n_Y\) 개를 한꺼번에 섞는가?
레이블(“\(X\) 집단인가 \(Y\) 집단인가”)을 무작위로 재배정하는 것이기 때문이다. 순열 후 처음 \(n_X\) 개는 새로운 “\(X\) 집단”, 나머지 \(n_Y\) 개는 새로운 “\(Y\) 집단”이 된다. 이 과정이 \(H_0\) 하에서 데이터가 어떻게 보일지를 시뮬레이션한다.


5 이론적 p-값과 순열 p-값 비교

5.1 언제 동일한가

표본이 크고 데이터가 이론적 가정(정규성, 등분산 등)을 잘 만족하면 두 p-값은 거의 같다.

예: ISLR Khan 데이터셋 11번째 유전자 (James et al., 2021, Ch.13.5) - 검정 통계량: \(T = -2.09\) - 이론적 p-값: \(0.041\) (\(t_{52}\) 분포 사용) - 순열 p-값 (\(B = 10{,}000\)): \(0.042\) - 결론: 두 방법이 거의 동일 — 이론적 분포가 타당한 경우

5.2 언제 다른가

데이터에 극단적 이상치가 있거나 표본이 작아 정규성 가정이 위반되면 크게 달라진다.

예: Khan 데이터셋 877번째 유전자 - 검정 통계량: \(T = -0.57\) - 이론적 p-값: \(0.571\) - 순열 p-값: \(0.673\) - 원인: 877번째 유전자에 극단적 이상치가 하나 있어 분포가 심하게 치우침 - 결론: 이 경우 순열 p-값이 더 신뢰할 수 있다

언제 순열 p-값을 써야 하는가
  1. 이론적 귀무 분포가 없는 경우: 비표준 검정 통계량 (예: 두 집단의 분산비 이외의 함수)
  2. 이론적 분포의 가정이 위반된 경우:
    • 표본이 너무 작아 CLT가 적용되지 않는다 (\(n < 30\))
    • 데이터가 정규성을 심하게 위반한다 (이상치, 치우침)
    • 등분산 가정이 의심된다
  3. 두 방법 결과가 크게 다를 경우: 순열 p-값을 우선 신뢰한다

6 다중 검정으로의 확장 — 순열 FDR

6.1 문제 설정

\(m\) 개 귀무 가설 \(H_{01}, \ldots, H_{0m}\) 을 동시에 검정할 때 (예: 유전체 연구에서 수천 개 유전자), 각 가설의 순열 p-값을 계산하면 거짓 발견율(FDR) 을 추정할 수 있다.

6.2 핵심 아이디어

임계값 \(c\) 를 설정하고 \(|T_j| \geq c\) 인 귀무 가설을 기각한다고 하면:

\[ \widehat{FDR} = \frac{\hat{V}}{R}, \quad \text{where} \quad R = \sum_{j=1}^m \mathbf{1}_{(|T_j| \geq c)} \]

  • \(R\): 실제 데이터에서 기각된 귀무 가설의 수 (쉽게 계산)
  • \(\hat{V}\): 거짓 양성(false positive)의 기대값 → 순열로 추정

순열 데이터에서는 모든 \(H_{0j}\) 가 참이므로, 순열 데이터에서 임계값을 넘는 통계량의 수가 \(\hat{V}\) 의 추정량이 된다:

\[ \hat{V} = \frac{1}{B}\sum_{b=1}^B \sum_{j=1}^m \mathbf{1}_{(|T_j^{*b}| \geq c)} \]

6.3 풀링 순열 p-값 (ISLR 식 13.14)

\(m\) 개 가설을 동시에 고려하는 풀링 순열 p-값:

\[ p_j = \frac{\sum_{j'=1}^{m}\sum_{b=1}^{B} \mathbf{1}_{(|T_{j'}^{*b}| \geq |T_j|)}}{Bm} \tag{13.14} \]

이것은 단순 순열 p-값(식 13.12)과 달리 모든 \(m\) 개 가설의 순열 통계량을 함께 사용하여 귀무 분포를 추정한다. 추정이 더 안정적이고, 이 p-값에 BH 절차를 적용하면 알고리즘 13.4와 동치가 된다.

왜 풀링이 유리한가? 가설별로 \(B\) 개 순열만 사용하면 귀무 분포 추정이 불안정하다. \(m\) 개 가설 \(\times B\) 번 순열 = \(Bm\) 개 통계량을 한꺼번에 사용하면 분포 추정이 훨씬 안정적이다.


7 적용 조건과 한계

7.1 핵심 가정: 교환 가능성

순열 p-값이 유효하려면 \(H_0\) 하에서 관측값이 교환 가능(exchangeable) 해야 한다. 즉, 두 집단의 분포가 완전히 동일해야 한다.

만약 분산은 다른데 평균만 같다면? ( \(H_0: \mu_X = \mu_Y\) 이지만 \(\sigma_X \neq \sigma_Y\) )
이 경우 레이블을 섞으면 실제 데이터의 이분산성이 희석된다 — 순열 p-값의 귀무 분포가 올바르지 않을 수 있다. Welch t-검정처럼 이분산을 허용하는 검정 통계량을 사용하거나, 순열 방식을 조정해야 한다.

7.2 계산 비용

\(B = 10{,}000\) 번 순열은 간단한 경우 빠르지만, \(m = 10{,}000\) 개 유전자에 대해 각각 \(B\) 번 반복하면 \(10^8\) 번 통계량 계산이 필요하다. 같은 순열 집합을 모든 \(m\) 개 가설에 재사용하는 것이 효율적이다.

7.3 단측 vs 양측

위 공식은 양측 검정 ( \(|T^{*b}| \geq |T|\) ) 기준이다. 단측 검정에서는:

\[ p_{\text{단측}} = \frac{\sum_{b=1}^{B} \mathbf{1}_{(T^{*b} \geq T)}}{B} \]


8 이론적 p-값과의 관계 정리

항목 이론적 p-값 순열 p-값
귀무 분포 이론적 (t, \(\chi^2\), F 등) 데이터에서 직접 근사
가정 정규성, 등분산, 대표본 교환 가능성 (\(H_0\) 하)
표본 크기 클수록 정확 소표본에서도 유효
계산 비용 낮음 (해석적) 높음 (\(B\) 번 반복)
이상치 취약 강건
표준 소프트웨어 쉽게 사용 직접 구현 필요
결과 일치 가정 충족 시 동일 가정 위반 시 더 신뢰

9 코드 구현

9.1 Step 1: 순수 Python 구현 (알고리즘 13.3)

import math
import random

def two_sample_t(x: list, y: list) -> float:
    """이표본 t-통계량 (풀링 분산)"""
    nx, ny = len(x), len(y)
    mu_x = sum(x) / nx
    mu_y = sum(y) / ny
    s2_x = sum((xi - mu_x)**2 for xi in x) / (nx - 1)
    s2_y = sum((yi - mu_y)**2 for yi in y) / (ny - 1)
    s_pool = math.sqrt(((nx-1)*s2_x + (ny-1)*s2_y) / (nx + ny - 2))
    return (mu_x - mu_y) / (s_pool * math.sqrt(1/nx + 1/ny))

def permuted_pvalue(x: list, y: list, B: int = 10000, seed: int = 42) -> dict:
    """
    순열 p-값 계산 (알고리즘 13.3)
    H0: E(X) = E(Y) vs Ha: E(X) != E(Y)
    """
    random.seed(seed)
    T_obs = two_sample_t(x, y)
    all_data = x + y
    nx = len(x)
    
    count_extreme = 0
    for _ in range(B):
        shuffled = all_data[:]
        random.shuffle(shuffled)
        x_perm = shuffled[:nx]
        y_perm = shuffled[nx:]
        T_perm = two_sample_t(x_perm, y_perm)
        if abs(T_perm) >= abs(T_obs):
            count_extreme += 1
    
    p_value = count_extreme / B
    return {
        "T 관측값": T_obs,
        "순열 p-값": p_value,
        "극단적 순열 수": count_extreme,
        "총 순열 수": B,
    }

# 예시 1: H0 참에 가까운 경우 (두 집단 평균 비슷)
x_null = [5.1, 4.9, 5.3, 4.8, 5.2, 5.0, 4.7, 5.4]
y_null = [5.0, 5.1, 4.9, 5.2, 5.3, 4.8, 5.1, 4.9]
result_null = permuted_pvalue(x_null, y_null, B=5000)

print("=== H0에 가까운 경우 ===")
for k, v in result_null.items():
    print(f"  {k}: {v:.4f}" if isinstance(v, float) else f"  {k}: {v}")

# 예시 2: H1에 가까운 경우 (두 집단 평균 차이 큼)
x_alt = [5.1, 4.9, 5.3, 4.8, 5.2, 5.0, 4.7, 5.4]
y_alt = [7.0, 7.1, 6.9, 7.2, 7.3, 6.8, 7.1, 6.9]
result_alt = permuted_pvalue(x_alt, y_alt, B=5000)

print("\n=== H1에 가까운 경우 (평균 차이 ~2) ===")
for k, v in result_alt.items():
    print(f"  {k}: {v:.4f}" if isinstance(v, float) else f"  {k}: {v}")

9.2 Step 2: numpy/scipy 구현 및 이론적 p-값과 비교

import numpy as np
from scipy import stats

def permuted_vs_theoretical(x: np.ndarray, y: np.ndarray,
                            B: int = 10000, seed: int = 42) -> None:
    """순열 p-값과 이론적 p-값 비교"""
    rng = np.random.default_rng(seed)
    
    # 이론적 p-값 (t-분포 기반)
    t_obs, p_theoretical = stats.ttest_ind(x, y, equal_var=True)
    
    # 순열 p-값
    all_data = np.concatenate([x, y])
    nx = len(x)
    count_extreme = 0
    
    for _ in range(B):
        shuffled = rng.permutation(all_data)
        x_p, y_p = shuffled[:nx], shuffled[nx:]
        t_p, _ = stats.ttest_ind(x_p, y_p, equal_var=True)
        if abs(t_p) >= abs(t_obs):
            count_extreme += 1
    
    p_permuted = count_extreme / B
    
    print(f"n_X={len(x)}, n_Y={len(y)}")
    print(f"  T 관측값:       {t_obs:.4f}")
    print(f"  이론적 p-값:    {p_theoretical:.4f}")
    print(f"  순열 p-값:      {p_permuted:.4f}")
    print(f"  차이:           {abs(p_theoretical - p_permuted):.4f}")

np.random.seed(42)

print("=== 케이스 1: 정규 데이터, 대표본 (이론/순열 거의 같음) ===")
x1 = np.random.normal(5.0, 1.0, 50)
y1 = np.random.normal(5.3, 1.0, 50)
permuted_vs_theoretical(x1, y1)

print("\n=== 케이스 2: 정규 데이터, 소표본 (약간 차이) ===")
x2 = np.random.normal(5.0, 1.0, 8)
y2 = np.random.normal(5.3, 1.0, 8)
permuted_vs_theoretical(x2, y2)

print("\n=== 케이스 3: 이상치 포함, 소표본 (크게 차이) ===")
x3 = np.array([5.1, 4.9, 5.3, 4.8, 5.2, 5.0, 4.7, 50.0])  # 이상치 포함
y3 = np.random.normal(5.0, 1.0, 8)
permuted_vs_theoretical(x3, y3)

9.3 Step 3: 다중 검정에서 순열 FDR 추정

import numpy as np
from scipy import stats

def permuted_fdr_estimate(X: np.ndarray, Y: np.ndarray,
                          threshold_c: float,
                          B: int = 1000, seed: int = 42) -> dict:
    """
    순열 FDR 추정 (알고리즘 13.4)
    X: (m, n_X) 행렬 — m개 가설, 각각 n_X개 관측값
    Y: (m, n_Y) 행렬 — m개 가설, 각각 n_Y개 관측값
    threshold_c: 기각 임계값
    """
    rng = np.random.default_rng(seed)
    m, nx = X.shape
    ny = Y.shape[1]
    
    # 원래 통계량
    T_obs = np.array([stats.ttest_ind(X[j], Y[j], equal_var=True)[0] for j in range(m)])
    R = np.sum(np.abs(T_obs) >= threshold_c)  # 기각된 귀무 가설 수
    
    # 순열로 V 추정
    V_estimates = []
    for _ in range(B):
        V_b = 0
        for j in range(m):
            all_data = np.concatenate([X[j], Y[j]])
            shuffled = rng.permutation(all_data)
            x_p, y_p = shuffled[:nx], shuffled[nx:]
            t_p, _ = stats.ttest_ind(x_p, y_p, equal_var=True)
            if abs(t_p) >= threshold_c:
                V_b += 1
        V_estimates.append(V_b)
    
    V_hat = np.mean(V_estimates)
    fdr_hat = V_hat / R if R > 0 else 0.0
    
    return {
        "m (가설 수)": m,
        "R (기각된 수)": int(R),
        "V_hat (추정 거짓 기각)": round(V_hat, 2),
        "FDR 추정": round(fdr_hat, 4),
        "임계값 c": threshold_c,
    }


# 시뮬레이션: m=100개 가설, 그 중 20%가 실제로 차이 있음
np.random.seed(42)
m, nx, ny = 100, 15, 15
n_true_alt = 20  # 실제 효과 있는 가설 수

X = np.random.normal(0, 1, (m, nx))
Y = np.random.normal(0, 1, (m, ny))
# 처음 n_true_alt 개 가설에 실제 차이 부여
Y[:n_true_alt] += 1.5

print("=== 다중 검정 순열 FDR 추정 ===")
print(f"m={m}개 가설, 실제 효과 있는 것: {n_true_alt}개")
print()

for c in [1.5, 2.0, 2.5]:
    result = permuted_fdr_estimate(X, Y, threshold_c=c, B=200)
    print(f"임계값 c={c}:")
    for k, v in result.items():
        if k != "임계값 c":
            print(f"  {k}: {v}")
    print()

10 핵심 정리

순열 p-값의 수학적 구조:

\[ p_{\text{perm}} = \frac{\#\{b : |T^{*b}| \geq |T|\}}{B} \approx P_{H_0}(|T| \geq |T_{\text{obs}}|) \]

유효성의 근거: \(H_0\) 하에서 교환 가능성이 성립하면, 순열 통계량 \(T^{*1}, \ldots, T^{*B}\) 는 귀무 분포의 iid 표본으로 볼 수 있다.

조건 권장 방법
대표본 + 정규성 + 등분산 이론적 t 검정 (빠르고 동등하게 정확)
소표본 또는 이상치 존재 순열 p-값 (가정 불필요)
비표준 검정 통계량 순열 p-값 (이론적 분포 없음)
다중 검정 + 분포 불확실 순열 FDR (알고리즘 13.4)

11 관련 주제

선행 지식

관련 개념

후속 주제

  • 다중 검정 보정 — Bonferroni, Benjamini-Hochberg, FDR (교재 미확인 — agent 사전학습 기반)
  • 부트스트랩 신뢰구간 — 순열과 유사한 재표본 기법

12 참고 문헌

  • James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning (2nd ed.). Springer. Chapter 13, Sections 13.5.1–13.5.3.
  • Good, P. I. (2005). Permutation, Parametric, and Bootstrap Tests of Hypotheses (3rd ed.). Springer. (교재 미확인 — agent 사전학습 기반)
  • Efron, B. & Hastie, T. (2016). Computer Age Statistical Inference. Cambridge University Press. (교재 미확인 — agent 사전학습 기반)

Subscribe

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