23. 비모수 통계 (Nonparametric Statistics)

23. 비모수 통계 (Nonparametric Statistics)

이전: 다변량 분석 | 다음: 실험 설계

개요

비모수 통계는 모집단의 분포에 대한 가정 없이 데이터를 분석하는 방법입니다. 정규성을 만족하지 않거나 표본 크기가 작을 때 유용합니다.


1. 비모수 검정이 필요한 경우

1.1 언제 비모수 검정을 사용하는가?

import numpy as np
import pandas as pd
import scipy.stats as stats
import matplotlib.pyplot as plt

np.random.seed(42)

def when_to_use_nonparametric():
    """비모수 검정 사용 상황"""
    print("""
    ================================================
    비모수 검정을 사용해야 하는 경우
    ================================================

    1. 정규성 위반
       - 데이터가 정규분포를 따르지 않음
       - Shapiro-Wilk 검정 등에서 기각

    2. 작은 표본 크기
       - n < 30인 경우 CLT 적용 어려움
       - 정규성 검정도 검정력 부족

    3. 순위/서열 데이터
       - 리커트 척도 (1~5점)
       - 순위 데이터

    4. 이상치 존재
       - 극단값에 민감한 평균 대신 중위수 비교
       - 순위 기반 방법은 이상치에 강건

    5. 동질성 가정 위반
       - 분산 동질성 (등분산) 가정 위반

    ================================================
    비모수 검정의 장단점
    ================================================

    장점:
    - 분포 가정 불필요
    - 이상치에 강건
    - 순위 데이터에 적합

    단점:
    - 가정이 만족되면 모수적 검정보다 검정력 낮음
    - 효과 크기 해석이 어려울 수 있음
    - 일부 복잡한 설계에 적용 어려움
    """)

when_to_use_nonparametric()

# 정규성 검정 예시
def check_normality(data, alpha=0.05):
    """정규성 검정 수행"""
    # Shapiro-Wilk
    stat_sw, p_sw = stats.shapiro(data)

    # D'Agostino-Pearson
    if len(data) >= 20:
        stat_da, p_da = stats.normaltest(data)
    else:
        stat_da, p_da = np.nan, np.nan

    print("=== 정규성 검정 ===")
    print(f"표본 크기: {len(data)}")
    print(f"Shapiro-Wilk: W={stat_sw:.4f}, p={p_sw:.4f}")
    if not np.isnan(p_da):
        print(f"D'Agostino-Pearson: p={p_da:.4f}")

    is_normal = p_sw > alpha
    print(f"결론: {'정규분포로 볼 수 있음' if is_normal else '정규분포 아님'} (α={alpha})")

    return is_normal

# 정규 데이터
normal_data = np.random.normal(50, 10, 30)
check_normality(normal_data)

print()

# 비정규 데이터 (지수분포)
skewed_data = np.random.exponential(10, 30)
check_normality(skewed_data)

1.2 모수적 vs 비모수적 검정 대응

모수적 검정 비모수적 검정 상황
1-표본 t-검정 Wilcoxon 부호순위 검정 단일 표본, 중위수 검정
독립표본 t-검정 Mann-Whitney U 두 독립 표본 비교
대응표본 t-검정 Wilcoxon 부호순위 검정 대응 표본 비교
일원 ANOVA Kruskal-Wallis H 3개 이상 독립 표본
반복측정 ANOVA Friedman 3개 이상 대응 표본
Pearson 상관 Spearman/Kendall 상관관계

2. Mann-Whitney U 검정

2.1 개념

목적: 두 독립 표본의 분포 비교 (중위수 또는 분포 위치)

가설: - H₀: 두 집단의 분포가 동일 - H₁: 두 집단의 분포가 다름 (또는 한쪽이 확률적으로 더 큼)

검정 통계량: U (순위 합 기반)

def mann_whitney_example():
    """Mann-Whitney U 검정 예시"""
    np.random.seed(42)

    # 시나리오: 두 그룹의 처리 효과 비교 (정규성 위반)
    # 그룹 A: 대조군
    # 그룹 B: 처리군

    group_a = np.random.exponential(20, 25)  # 비정규
    group_b = np.random.exponential(25, 25) + 5  # 비정규, 더 큰 값

    print("=== Mann-Whitney U 검정 ===")
    print(f"\n그룹 A: n={len(group_a)}, 중위수={np.median(group_a):.2f}")
    print(f"그룹 B: n={len(group_b)}, 중위수={np.median(group_b):.2f}")

    # 정규성 검정
    print("\n정규성 검정:")
    _, p_a = stats.shapiro(group_a)
    _, p_b = stats.shapiro(group_b)
    print(f"  그룹 A: p={p_a:.4f}")
    print(f"  그룹 B: p={p_b:.4f}")

    # Mann-Whitney U 검정
    statistic, p_value = stats.mannwhitneyu(group_a, group_b, alternative='two-sided')

    print(f"\nMann-Whitney U 검정:")
    print(f"  U 통계량: {statistic:.2f}")
    print(f"  p-value: {p_value:.4f}")

    if p_value < 0.05:
        print("  결론: 두 그룹 간 유의한 차이 있음 (p < 0.05)")
    else:
        print("  결론: 두 그룹 간 유의한 차이 없음 (p >= 0.05)")

    # 효과 크기: rank-biserial correlation
    n1, n2 = len(group_a), len(group_b)
    r = 1 - (2 * statistic) / (n1 * n2)
    print(f"\n효과 크기 (rank-biserial r): {r:.3f}")
    print(f"  |r| < 0.1: 작은 효과")
    print(f"  0.1 <= |r| < 0.3: 중간 효과")
    print(f"  |r| >= 0.3: 큰 효과")

    # 시각화
    fig, axes = plt.subplots(1, 3, figsize=(14, 4))

    # 박스플롯
    ax = axes[0]
    ax.boxplot([group_a, group_b], labels=['Group A', 'Group B'])
    ax.set_ylabel('값')
    ax.set_title('박스플롯')
    ax.grid(True, alpha=0.3)

    # 히스토그램
    ax = axes[1]
    ax.hist(group_a, bins=15, alpha=0.5, label='Group A', density=True)
    ax.hist(group_b, bins=15, alpha=0.5, label='Group B', density=True)
    ax.axvline(np.median(group_a), color='blue', linestyle='--', label='A 중위수')
    ax.axvline(np.median(group_b), color='orange', linestyle='--', label='B 중위수')
    ax.set_xlabel('값')
    ax.set_ylabel('밀도')
    ax.set_title('분포 비교')
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 순위 분포
    ax = axes[2]
    all_data = np.concatenate([group_a, group_b])
    ranks = stats.rankdata(all_data)
    ranks_a = ranks[:len(group_a)]
    ranks_b = ranks[len(group_a):]
    ax.hist(ranks_a, bins=15, alpha=0.5, label='Group A ranks')
    ax.hist(ranks_b, bins=15, alpha=0.5, label='Group B ranks')
    ax.set_xlabel('순위')
    ax.set_ylabel('빈도')
    ax.set_title('순위 분포')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return group_a, group_b

group_a, group_b = mann_whitney_example()

2.2 단측 검정

# 단측 검정: 그룹 B가 그룹 A보다 큰지 검정
stat_greater, p_greater = stats.mannwhitneyu(group_a, group_b, alternative='less')
print(f"단측 검정 (B > A): p = {p_greater:.4f}")

stat_less, p_less = stats.mannwhitneyu(group_a, group_b, alternative='greater')
print(f"단측 검정 (A > B): p = {p_less:.4f}")

3. Wilcoxon 부호순위 검정

3.1 대응표본 비교

목적: 대응되는 두 측정값의 차이가 0인지 검정

사용 상황: 전/후 비교, 짝지어진 표본

def wilcoxon_signed_rank_example():
    """Wilcoxon 부호순위 검정 예시"""
    np.random.seed(42)

    # 시나리오: 다이어트 프로그램 전후 체중 변화
    n = 20
    before = np.random.normal(80, 10, n)
    # 평균 3kg 감소 효과 + 비정규 오차
    after = before - 3 + np.random.exponential(2, n) - np.random.exponential(2, n)

    diff = after - before

    print("=== Wilcoxon 부호순위 검정 ===")
    print(f"\n표본 크기: {n}")
    print(f"전: 평균={before.mean():.2f}, 중위수={np.median(before):.2f}")
    print(f"후: 평균={after.mean():.2f}, 중위수={np.median(after):.2f}")
    print(f"차이: 평균={diff.mean():.2f}, 중위수={np.median(diff):.2f}")

    # 차이의 정규성 검정
    _, p_norm = stats.shapiro(diff)
    print(f"\n차이의 정규성: p={p_norm:.4f}")

    # Wilcoxon 검정
    statistic, p_value = stats.wilcoxon(before, after, alternative='two-sided')

    print(f"\nWilcoxon 검정:")
    print(f"  W 통계량: {statistic:.2f}")
    print(f"  p-value: {p_value:.4f}")

    # 대응표본 t-검정과 비교
    t_stat, t_p = stats.ttest_rel(before, after)
    print(f"\n대응 t-검정 (비교용):")
    print(f"  t 통계량: {t_stat:.2f}")
    print(f"  p-value: {t_p:.4f}")

    # 효과 크기
    r = statistic / (n * (n + 1) / 2)
    print(f"\n효과 크기 (r): {abs(1-2*r):.3f}")

    # 시각화
    fig, axes = plt.subplots(1, 3, figsize=(14, 4))

    # 전후 비교
    ax = axes[0]
    ax.boxplot([before, after], labels=['Before', 'After'])
    ax.set_ylabel('체중 (kg)')
    ax.set_title('전후 비교')
    ax.grid(True, alpha=0.3)

    # 개인별 변화
    ax = axes[1]
    for i in range(n):
        ax.plot([0, 1], [before[i], after[i]], 'b-', alpha=0.5)
    ax.plot([0, 1], [before.mean(), after.mean()], 'r-', linewidth=2, label='평균')
    ax.set_xticks([0, 1])
    ax.set_xticklabels(['Before', 'After'])
    ax.set_ylabel('체중 (kg)')
    ax.set_title('개인별 변화')
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 차이 분포
    ax = axes[2]
    ax.hist(diff, bins=10, density=True, alpha=0.7, edgecolor='black')
    ax.axvline(0, color='r', linestyle='--', label='변화 없음')
    ax.axvline(np.median(diff), color='g', linestyle='-', label=f'중위수={np.median(diff):.2f}')
    ax.set_xlabel('차이 (After - Before)')
    ax.set_ylabel('밀도')
    ax.set_title('차이 분포')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return before, after

before, after = wilcoxon_signed_rank_example()

3.2 단일 표본 검정 (중위수 검정)

# 단일 표본: 중위수가 특정 값인지 검정
def one_sample_wilcoxon(data, hypothesized_median):
    """
    단일 표본 Wilcoxon 검정
    H0: 중위수 = hypothesized_median
    """
    diff_from_median = data - hypothesized_median

    # 0인 값 제외
    diff_from_median = diff_from_median[diff_from_median != 0]

    if len(diff_from_median) == 0:
        print("모든 값이 가설 중위수와 같습니다.")
        return

    stat, p_value = stats.wilcoxon(diff_from_median)

    print(f"=== 단일 표본 Wilcoxon 검정 ===")
    print(f"H0: 중위수 = {hypothesized_median}")
    print(f"표본 중위수: {np.median(data):.2f}")
    print(f"W 통계량: {stat:.2f}")
    print(f"p-value: {p_value:.4f}")

    if p_value < 0.05:
        print(f"결론: 중위수는 {hypothesized_median}과 유의하게 다름")
    else:
        print(f"결론: 중위수가 {hypothesized_median}이라는 증거 부족")

# 예시
sample_data = np.random.exponential(10, 30) + 5
one_sample_wilcoxon(sample_data, hypothesized_median=10)

4. Kruskal-Wallis H 검정

4.1 개념

목적: 3개 이상 독립 그룹의 분포 비교 (일원 ANOVA의 비모수적 대안)

가설: - H₀: 모든 그룹의 분포가 동일 - H₁: 최소 하나의 그룹이 다름

def kruskal_wallis_example():
    """Kruskal-Wallis H 검정 예시"""
    np.random.seed(42)

    # 시나리오: 3가지 교육 방법의 효과 비교
    method_a = np.random.exponential(10, 25) + 60  # 전통적
    method_b = np.random.exponential(10, 25) + 65  # 온라인
    method_c = np.random.exponential(10, 25) + 70  # 혼합

    print("=== Kruskal-Wallis H 검정 ===")
    print(f"\n방법 A: n={len(method_a)}, 중위수={np.median(method_a):.2f}")
    print(f"방법 B: n={len(method_b)}, 중위수={np.median(method_b):.2f}")
    print(f"방법 C: n={len(method_c)}, 중위수={np.median(method_c):.2f}")

    # 정규성 검정
    print("\n정규성 검정:")
    for name, data in [('A', method_a), ('B', method_b), ('C', method_c)]:
        _, p = stats.shapiro(data)
        print(f"  {name}: p={p:.4f}")

    # Kruskal-Wallis 검정
    H_stat, p_value = stats.kruskal(method_a, method_b, method_c)

    print(f"\nKruskal-Wallis 검정:")
    print(f"  H 통계량: {H_stat:.2f}")
    print(f"  p-value: {p_value:.4f}")

    # 효과 크기: eta-squared
    N = len(method_a) + len(method_b) + len(method_c)
    k = 3
    eta_sq = (H_stat - k + 1) / (N - k)
    print(f"\n효과 크기 (η²): {eta_sq:.3f}")
    print("  0.01: 작은 효과")
    print("  0.06: 중간 효과")
    print("  0.14: 큰 효과")

    # 일원 ANOVA와 비교
    F_stat, anova_p = stats.f_oneway(method_a, method_b, method_c)
    print(f"\n일원 ANOVA (비교용):")
    print(f"  F 통계량: {F_stat:.2f}")
    print(f"  p-value: {anova_p:.4f}")

    # 시각화
    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # 박스플롯
    ax = axes[0]
    ax.boxplot([method_a, method_b, method_c],
               labels=['Method A', 'Method B', 'Method C'])
    ax.set_ylabel('점수')
    ax.set_title('교육 방법별 점수 분포')
    ax.grid(True, alpha=0.3)

    # 바이올린 플롯
    ax = axes[1]
    parts = ax.violinplot([method_a, method_b, method_c], positions=[1, 2, 3],
                           showmeans=True, showmedians=True)
    ax.set_xticks([1, 2, 3])
    ax.set_xticklabels(['Method A', 'Method B', 'Method C'])
    ax.set_ylabel('점수')
    ax.set_title('바이올린 플롯')
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return method_a, method_b, method_c

method_a, method_b, method_c = kruskal_wallis_example()

4.2 사후검정 (Post-hoc Tests)

from itertools import combinations
from scipy.stats import mannwhitneyu

def dunn_test_alternative(groups, group_names, alpha=0.05):
    """
    Dunn 검정의 대안: Bonferroni 보정된 Mann-Whitney U 검정
    """
    n_comparisons = len(list(combinations(range(len(groups)), 2)))
    adjusted_alpha = alpha / n_comparisons

    print(f"=== 사후검정 (Bonferroni 보정) ===")
    print(f"비교 횟수: {n_comparisons}")
    print(f"보정된 α: {adjusted_alpha:.4f}")
    print()

    results = []
    for (i, j) in combinations(range(len(groups)), 2):
        stat, p = mannwhitneyu(groups[i], groups[j], alternative='two-sided')
        significant = p < adjusted_alpha
        results.append({
            'comparison': f'{group_names[i]} vs {group_names[j]}',
            'U': stat,
            'p-value': p,
            'significant': significant
        })
        print(f"{group_names[i]} vs {group_names[j]}: U={stat:.1f}, p={p:.4f} {'*' if significant else ''}")

    return pd.DataFrame(results)

groups = [method_a, method_b, method_c]
group_names = ['A', 'B', 'C']
posthoc_results = dunn_test_alternative(groups, group_names)

5. Friedman 검정

5.1 개념

목적: 반복측정 또는 블록 설계에서 3개 이상 조건 비교

사용 상황: 같은 피험자가 여러 조건에서 측정됨

def friedman_example():
    """Friedman 검정 예시"""
    np.random.seed(42)

    # 시나리오: 같은 학생들의 3개 시험 점수
    n_students = 20

    # 상관된 데이터 생성 (같은 학생의 여러 시험)
    ability = np.random.normal(70, 10, n_students)  # 기저 능력
    exam1 = ability + np.random.normal(0, 5, n_students)
    exam2 = ability + np.random.normal(3, 5, n_students)  # 약간 더 어려움
    exam3 = ability + np.random.normal(6, 5, n_students)  # 더 어려움

    print("=== Friedman 검정 ===")
    print(f"\n학생 수: {n_students}")
    print(f"Exam 1: 중위수={np.median(exam1):.2f}")
    print(f"Exam 2: 중위수={np.median(exam2):.2f}")
    print(f"Exam 3: 중위수={np.median(exam3):.2f}")

    # Friedman 검정
    stat, p_value = stats.friedmanchisquare(exam1, exam2, exam3)

    print(f"\nFriedman 검정:")
    print(f"  χ² 통계량: {stat:.2f}")
    print(f"  p-value: {p_value:.4f}")

    # 효과 크기: Kendall's W
    k = 3  # 조건 수
    W = stat / (n_students * (k - 1))
    print(f"\n효과 크기 (Kendall's W): {W:.3f}")
    print("  W < 0.3: 약한 일치")
    print("  0.3 <= W < 0.5: 중간 일치")
    print("  W >= 0.5: 강한 일치")

    # 시각화
    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # 박스플롯
    ax = axes[0]
    ax.boxplot([exam1, exam2, exam3], labels=['Exam 1', 'Exam 2', 'Exam 3'])
    ax.set_ylabel('점수')
    ax.set_title('시험별 점수 분포')
    ax.grid(True, alpha=0.3)

    # 개인별 프로파일
    ax = axes[1]
    for i in range(min(10, n_students)):  # 처음 10명만
        ax.plot([1, 2, 3], [exam1[i], exam2[i], exam3[i]], 'o-', alpha=0.5)
    ax.plot([1, 2, 3], [exam1.mean(), exam2.mean(), exam3.mean()],
            'rs-', linewidth=2, markersize=8, label='평균')
    ax.set_xticks([1, 2, 3])
    ax.set_xticklabels(['Exam 1', 'Exam 2', 'Exam 3'])
    ax.set_ylabel('점수')
    ax.set_title('개인별 프로파일')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return exam1, exam2, exam3

exam1, exam2, exam3 = friedman_example()

5.2 Nemenyi 사후검정

def nemenyi_posthoc(groups, group_names, alpha=0.05):
    """
    Nemenyi 사후검정 (Friedman 이후)
    Wilcoxon 부호순위 검정의 Bonferroni 보정
    """
    n_comparisons = len(list(combinations(range(len(groups)), 2)))
    adjusted_alpha = alpha / n_comparisons

    print(f"=== Nemenyi 사후검정 ===")
    print(f"비교 횟수: {n_comparisons}")
    print(f"보정된 α: {adjusted_alpha:.4f}")
    print()

    results = []
    for (i, j) in combinations(range(len(groups)), 2):
        stat, p = stats.wilcoxon(groups[i], groups[j])
        significant = p < adjusted_alpha
        results.append({
            'comparison': f'{group_names[i]} vs {group_names[j]}',
            'W': stat,
            'p-value': p,
            'significant': significant
        })
        print(f"{group_names[i]} vs {group_names[j]}: W={stat:.1f}, p={p:.4f} {'*' if significant else ''}")

    return pd.DataFrame(results)

groups = [exam1, exam2, exam3]
group_names = ['Exam1', 'Exam2', 'Exam3']
nemenyi_results = nemenyi_posthoc(groups, group_names)

6. 비모수적 상관 (Spearman, Kendall)

6.1 Spearman 순위 상관

특징: 순위 기반, 단조 관계 측정 (선형일 필요 없음)

def spearman_correlation_example():
    """Spearman 상관 예시"""
    np.random.seed(42)

    # 비선형 관계 데이터
    n = 50
    x = np.random.uniform(0, 10, n)
    y = np.log(x + 1) + np.random.normal(0, 0.3, n)  # 로그 관계 + 잡음

    # Pearson 상관
    pearson_r, pearson_p = stats.pearsonr(x, y)

    # Spearman 상관
    spearman_r, spearman_p = stats.spearmanr(x, y)

    print("=== 상관 분석 ===")
    print(f"\nPearson 상관: r={pearson_r:.4f}, p={pearson_p:.4f}")
    print(f"Spearman 상관: ρ={spearman_r:.4f}, p={spearman_p:.4f}")
    print("\n비선형 관계에서 Spearman이 더 적합")

    # 시각화
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))

    # 원본 데이터
    ax = axes[0]
    ax.scatter(x, y, alpha=0.7)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_title(f'원본 데이터\nPearson r={pearson_r:.3f}, Spearman ρ={spearman_r:.3f}')
    ax.grid(True, alpha=0.3)

    # 순위 변환
    ax = axes[1]
    x_ranks = stats.rankdata(x)
    y_ranks = stats.rankdata(y)
    ax.scatter(x_ranks, y_ranks, alpha=0.7)
    ax.set_xlabel('X 순위')
    ax.set_ylabel('Y 순위')
    ax.set_title('순위 변환 후')
    ax.grid(True, alpha=0.3)

    # 이상치가 있는 경우
    ax = axes[2]
    x_outlier = np.append(x, [50])  # 극단적 이상치
    y_outlier = np.append(y, [y.mean()])

    pearson_out, _ = stats.pearsonr(x_outlier, y_outlier)
    spearman_out, _ = stats.spearmanr(x_outlier, y_outlier)

    ax.scatter(x_outlier[:-1], y_outlier[:-1], alpha=0.7, label='일반 데이터')
    ax.scatter(x_outlier[-1], y_outlier[-1], color='r', s=100, label='이상치', marker='x')
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_title(f'이상치 포함\nPearson={pearson_out:.3f}, Spearman={spearman_out:.3f}')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return x, y

x, y = spearman_correlation_example()

6.2 Kendall 순위 상관

특징: 쌍별 비교 기반, Spearman보다 강건

def kendall_correlation_example():
    """Kendall tau 상관 예시"""
    np.random.seed(42)

    # 순위 데이터 (리커트 척도 등)
    n = 30
    rater1 = np.random.randint(1, 6, n)  # 1~5점
    # 어느 정도 일치하지만 완전히 같지는 않음
    rater2 = np.clip(rater1 + np.random.randint(-1, 2, n), 1, 5)

    # Pearson (부적절)
    pearson_r, _ = stats.pearsonr(rater1, rater2)

    # Spearman
    spearman_r, _ = stats.spearmanr(rater1, rater2)

    # Kendall
    kendall_tau, kendall_p = stats.kendalltau(rater1, rater2)

    print("=== 순위 데이터 상관 분석 ===")
    print(f"\n평가자 1 분포: {np.bincount(rater1)[1:]}")
    print(f"평가자 2 분포: {np.bincount(rater2)[1:]}")
    print(f"\nPearson r: {pearson_r:.4f} (순위 데이터에 부적절)")
    print(f"Spearman ρ: {spearman_r:.4f}")
    print(f"Kendall τ: {kendall_tau:.4f}, p={kendall_p:.4f}")

    print("\n해석:")
    print("  τ = 0: 일치/불일치 쌍이 균등")
    print("  τ = 1: 완전한 순위 일치")
    print("  τ = -1: 완전한 역순위")

    # 시각화
    fig, ax = plt.subplots(figsize=(8, 6))

    # 지터 추가 (같은 점수가 겹치지 않게)
    jitter1 = rater1 + np.random.uniform(-0.1, 0.1, n)
    jitter2 = rater2 + np.random.uniform(-0.1, 0.1, n)

    ax.scatter(jitter1, jitter2, alpha=0.7, s=50)
    ax.plot([0, 6], [0, 6], 'r--', label='완전 일치선')
    ax.set_xlabel('평가자 1')
    ax.set_ylabel('평가자 2')
    ax.set_title(f'평가자 간 일치도\nKendall τ = {kendall_tau:.3f}')
    ax.set_xlim(0.5, 5.5)
    ax.set_ylim(0.5, 5.5)
    ax.set_xticks(range(1, 6))
    ax.set_yticks(range(1, 6))
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.show()

    return rater1, rater2

rater1, rater2 = kendall_correlation_example()

6.3 상관계수 비교

def compare_correlations():
    """세 가지 상관계수 비교"""
    print("""
    =================================================
    상관계수 비교
    =================================================

    | 특성 | Pearson | Spearman | Kendall |
    |------|---------|----------|---------|
    | 관계 | 선형 | 단조 | 단조 |
    | 데이터 | 연속형 | 순서형/연속형 | 순서형 |
    | 이상치 | 민감 | 강건 | 매우 강건 |
    | 동률 처리 | 해당없음 | 평균 순위 | 보정 가능 |
    | 계산 | O(n) | O(n log n) | O(n²) |
    | 범위 | [-1, 1] | [-1, 1] | [-1, 1] |

    선택 기준:
    - 연속형 & 선형 관계 → Pearson
    - 비정규/이상치/비선형 → Spearman
    - 순위/서열 데이터 → Kendall
    - 작은 표본 크기 → Kendall
    """)

compare_correlations()

7. 실습 예제

7.1 종합 비모수 분석

def comprehensive_nonparametric_analysis(data_dict, alpha=0.05):
    """
    종합 비모수 분석 수행

    Parameters:
    -----------
    data_dict : dict
        {그룹명: 데이터} 형태
    """
    print("="*60)
    print("종합 비모수 분석")
    print("="*60)

    groups = list(data_dict.values())
    group_names = list(data_dict.keys())
    n_groups = len(groups)

    # 1. 기술 통계
    print("\n[1] 기술 통계")
    for name, data in data_dict.items():
        print(f"  {name}: n={len(data)}, 중위수={np.median(data):.2f}, "
              f"IQR={np.percentile(data, 75)-np.percentile(data, 25):.2f}")

    # 2. 정규성 검정
    print("\n[2] 정규성 검정 (Shapiro-Wilk)")
    all_normal = True
    for name, data in data_dict.items():
        _, p = stats.shapiro(data)
        is_normal = p > alpha
        all_normal = all_normal and is_normal
        print(f"  {name}: p={p:.4f} {'(정규)' if is_normal else '(비정규)'}")

    # 3. 적절한 검정 선택 및 수행
    print(f"\n[3] 그룹 비교 검정 (그룹 수: {n_groups})")

    if n_groups == 2:
        # 두 그룹: Mann-Whitney U
        stat, p = stats.mannwhitneyu(groups[0], groups[1], alternative='two-sided')
        print(f"  Mann-Whitney U: U={stat:.2f}, p={p:.4f}")
        test_name = "Mann-Whitney U"
    else:
        # 세 그룹 이상: Kruskal-Wallis
        stat, p = stats.kruskal(*groups)
        print(f"  Kruskal-Wallis H: H={stat:.2f}, p={p:.4f}")
        test_name = "Kruskal-Wallis"

        # 유의하면 사후검정
        if p < alpha:
            print("\n  사후검정 (Bonferroni 보정):")
            n_comp = n_groups * (n_groups - 1) // 2
            adj_alpha = alpha / n_comp
            for (i, j) in combinations(range(n_groups), 2):
                _, ph_p = stats.mannwhitneyu(groups[i], groups[j])
                sig = '*' if ph_p < adj_alpha else ''
                print(f"    {group_names[i]} vs {group_names[j]}: p={ph_p:.4f} {sig}")

    # 4. 시각화
    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # 박스플롯
    ax = axes[0]
    ax.boxplot(groups, labels=group_names)
    ax.set_ylabel('값')
    ax.set_title(f'{test_name} 검정\np = {p:.4f}')
    ax.grid(True, alpha=0.3)

    # 히스토그램
    ax = axes[1]
    for name, data in data_dict.items():
        ax.hist(data, bins=15, alpha=0.5, label=name, density=True)
    ax.set_xlabel('값')
    ax.set_ylabel('밀도')
    ax.set_title('분포 비교')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# 사용 예시
np.random.seed(42)
data = {
    'Control': np.random.exponential(10, 30) + 50,
    'Treatment A': np.random.exponential(10, 30) + 55,
    'Treatment B': np.random.exponential(10, 30) + 60
}
comprehensive_nonparametric_analysis(data)

8. 연습 문제

문제 1: 검정 선택

다음 상황에 적합한 비모수 검정을 선택하세요: 1. 남녀 학생의 시험 점수 비교 (점수 분포가 편포) 2. 3개 병원의 대기 시간 비교 3. 동일 환자의 치료 전후 통증 점수 비교

문제 2: Mann-Whitney U

두 그룹의 데이터가 주어졌을 때: - Group A: [23, 28, 31, 35, 39, 42] - Group B: [18, 22, 25, 29, 33]

수동으로 U 통계량을 계산하고 scipy 결과와 비교하세요.

문제 3: 상관 분석

10개의 순위 쌍 데이터에 대해: 1. Spearman 상관계수 계산 2. Kendall tau 계산 3. 두 계수의 차이 해석

문제 4: Kruskal-Wallis 후 사후검정

4개 그룹 비교에서 Kruskal-Wallis가 유의할 때: 1. 필요한 사후검정 비교 횟수 2. Bonferroni 보정된 유의수준 3. 어떤 쌍이 유의하게 다른지 판단


9. 핵심 요약

검정 선택 플로우차트

데이터 유형 확인
    │
    ├── 정규성 만족? ───┬── Yes → 모수적 검정
    │                   └── No → 비모수 검정
    │
비모수 검정 선택:
    │
    ├── 2 독립 그룹 → Mann-Whitney U
    │
    ├── 2 대응 그룹 → Wilcoxon 부호순위
    │
    ├── 3+ 독립 그룹 → Kruskal-Wallis H → 사후: Dunn
    │
    ├── 3+ 대응 그룹 → Friedman → 사후: Nemenyi
    │
    └── 상관 → Spearman (연속) / Kendall (순위)

scipy.stats 함수

검정 함수
Mann-Whitney U mannwhitneyu(x, y)
Wilcoxon wilcoxon(x, y)
Kruskal-Wallis kruskal(g1, g2, g3, ...)
Friedman friedmanchisquare(g1, g2, g3, ...)
Spearman spearmanr(x, y)
Kendall kendalltau(x, y)

효과 크기 해석

검정 효과 크기 작음 중간
Mann-Whitney rank-biserial r 0.1 0.3 0.5
Kruskal-Wallis η² 0.01 0.06 0.14
Friedman Kendall's W 0.1 0.3 0.5

다음 장 미리보기

14장 실험 설계에서는: - 실험 설계의 기본 원리 - A/B 테스트 - 표본 크기 결정 (검정력 분석) - 순차적 검정

to navigate between lessons