9  Chapter 9: 非參數方法 (nonparametric methods)

當資料不想乖乖常態:用排序與重抽樣聽懂它

9.1 本章學習目標

前幾章我們學到 t 檢定、信賴區間與常態近似。這些方法很好用,但醫療資料常常不太乖:住院天數右偏、疼痛分數是等級、樣本數很小、離群值很有存在感。這時候,非參數方法 (nonparametric methods) 提供另一條路。

非參數方法不是「不用假設」的方法,而是較少依賴特定母群體分布形狀的方法。許多非參數檢定使用排序 (rank) 而非原始數值,因此對極端值較穩健。它們像臨床老師看到病人資料很亂時,溫柔但堅定地說:「我們先排隊。」

讀完本章後,你應該能夠:

  1. 說明非參數方法適用的常見情境。
  2. 使用符號檢定 (sign test) 與 Wilcoxon 符號等級檢定 (Wilcoxon signed-rank test) 分析單一樣本或配對資料。
  3. 使用 Mann-Whitney U 檢定 (Mann-Whitney U test) 比較兩個獨立樣本。
  4. 使用 Kruskal-Wallis 檢定 (Kruskal-Wallis test) 比較三組以上獨立樣本。
  5. 使用 Spearman 等級相關 (Spearman rank correlation) 描述單調關係。
  6. 理解置換檢定 (permutation test) 的基本直覺。

9.2 什麼時候考慮非參數方法?

非參數方法常用於下列情境:

  • 樣本數小,難以評估常態假設。
  • 連續資料明顯偏態,例如住院天數、醫療費用、CRP。
  • 有離群值,且離群值不一定是錯誤。
  • 結果變項是次序資料 (ordinal data),例如疼痛分數、疾病嚴重度等級。
  • 研究問題更關心中位數或分布位置,而不是平均值。

不過,非參數方法也不是萬能。它們通常檢定分布位置或排序差異,不一定直接檢定平均值差異。報告結果時,最好搭配中位數、四分位距與合適的效果量,避免只丟出一個 p 值讓讀者自己找路。

9.3 排序的基本概念

許多非參數方法會把原始資料轉成排序。假設資料為 3、8、10,排序分別是 1、2、3。若有相同值,通常使用平均排序。例如 3、8、8、10 中,兩個 8 佔第 2 與第 3 名,因此各給 2.5。

排序的優點是降低極端值影響。若某病人住院 60 天,平均值會被明顯拉高;但在排序資料中,它只是最大值。這不代表離群值不重要,而是非參數方法不讓它一個人把整張桌子掀翻。

9.4 Mann-Whitney U 檢定:兩獨立樣本

Mann-Whitney U 檢定用於比較兩個獨立樣本的分布位置,常被視為兩獨立樣本 t 檢定的非參數替代方法。它也稱 Wilcoxon rank-sum test。

重要提醒:Mann-Whitney U 檢定不只是「中位數檢定」。若兩組分布形狀相似,它可被解讀為位置差異;若分布形狀不同,解釋要更小心。

9.5 範例 1:加強出院準備是否縮短住院天數?

假設研究比較標準照護與加強出院準備兩組病人的住院天數。住院天數右偏且樣本不大,因此考慮 Mann-Whitney U 檢定。

standard_los = np.array([3, 4, 4, 5, 5, 6, 7, 8, 10, 13, 18, 25])
enhanced_los = np.array([2, 3, 3, 4, 4, 5, 5, 6, 7, 9, 12, 16])
los_df = pd.DataFrame(
    {
        "los": np.r_[standard_los, enhanced_los],
        "group": ["標準照護"] * len(standard_los) + ["加強出院準備"] * len(enhanced_los),
    }
)

los_df.groupby("group")["los"].agg(["count", "median", "mean", "std"]).round(2)
count median mean std
group
加強出院準備 12 5.0 6.33 4.14
標準照護 12 6.5 9.00 6.65
plt.figure(figsize=(7, 4.5))
sns.boxplot(data=los_df, x="group", y="los", hue="group", palette="Set2", legend=False)
sns.stripplot(data=los_df, x="group", y="los", color="#333333", alpha=0.7, jitter=0.12)
plt.xlabel("照護組別")
plt.ylabel("住院天數")
plt.title("右偏住院天數:兩組比較")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch09_mannwhitney_los_boxplot.png", dpi=300)
plt.show()
Figure 9.1: 右偏住院天數的兩組比較。每個點代表一位病人。
mw_result = mannwhitneyu(enhanced_los, standard_los, alternative="less")

pd.DataFrame(
    {
        "quantity": ["U 統計量", "單尾 p 值"],
        "value": [mw_result.statistic, mw_result.pvalue],
    }
).round(4)
quantity value
0 U 統計量 52.0000
1 單尾 p 值 0.1288

這裡使用單尾對立假設,因為研究問題是「加強出院準備是否縮短住院天數」。若研究前沒有明確方向,應使用雙尾檢定。

9.6 符號檢定與 Wilcoxon 符號等級檢定

配對資料若差值明顯不符合常態,或結果是次序尺度,可以考慮符號檢定或 Wilcoxon 符號等級檢定。

符號檢定只看差值方向:改善、惡化或無變化。它非常穩健,但也丟掉很多資訊。Wilcoxon 符號等級檢定同時考慮差值方向與大小排序,因此通常比符號檢定更有檢定力,但它假設差值分布大致對稱。

9.7 範例 2:治療前後疼痛分數

某疼痛門診記錄 12 位病人治療前後的疼痛分數,分數為 0 到 10 的次序量尺。研究問題是治療後疼痛是否下降。

pain_before = np.array([8, 7, 6, 8, 9, 7, 6, 8, 7, 9, 6, 8])
pain_after = np.array([5, 6, 4, 6, 6, 5, 5, 7, 4, 7, 4, 6])
paired_df = pd.DataFrame({"id": np.arange(1, 13), "before": pain_before, "after": pain_after})
paired_df["difference"] = paired_df["after"] - paired_df["before"]

paired_df
id before after difference
0 1 8 5 -3
1 2 7 6 -1
2 3 6 4 -2
3 4 8 6 -2
4 5 9 6 -3
5 6 7 5 -2
6 7 6 5 -1
7 8 8 7 -1
8 9 7 4 -3
9 10 9 7 -2
10 11 6 4 -2
11 12 8 6 -2
plt.figure(figsize=(7, 4.5))
sns.histplot(data=paired_df, x="difference", bins=np.arange(-5.5, 1.5, 1), color="#8ab17d")
plt.axvline(0, color="#d62828", linestyle="--")
plt.xlabel("疼痛分數差值:治療後 - 治療前")
plt.ylabel("人數")
plt.title("配對差值分布")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch09_wilcoxon_paired_differences.png", dpi=300)
plt.show()
Figure 9.2: 治療後與治療前疼痛分數差值分布。負值代表疼痛下降。
wilcoxon_result = wilcoxon(pain_after, pain_before, alternative="less")

pd.DataFrame(
    {
        "quantity": ["Wilcoxon 統計量", "單尾 p 值", "差值中位數"],
        "value": [wilcoxon_result.statistic, wilcoxon_result.pvalue, np.median(paired_df["difference"])],
    }
).round(4)
quantity value
0 Wilcoxon 統計量 0.0000
1 單尾 p 值 0.0002
2 差值中位數 -2.0000

如果 p 值小,代表資料提供證據支持治療後疼痛分數下降。臨床上仍應報告下降幅度,例如差值中位數與四分位距。病人關心的不是 p 值多小,而是疼痛少了多少。

9.8 Kruskal-Wallis 檢定:三組以上比較

Kruskal-Wallis 檢定是 Mann-Whitney U 檢定的多組延伸,用於比較三組以上獨立樣本的分布位置。它常被視為單因子 ANOVA 的非參數替代方法。

虛無假設是各組來自相同分布。若檢定顯著,代表至少一組不同,但不會告訴你哪兩組不同。後續需要事後比較,並注意多重比較問題。

9.9 範例 3:三種劑量下的 CRP

dose_a = np.array([9, 12, 14, 15, 18, 21, 25, 31])
dose_b = np.array([7, 9, 10, 12, 14, 18, 20, 23])
dose_c = np.array([5, 6, 8, 9, 10, 12, 14, 16])
crp_df = pd.DataFrame(
    {
        "crp": np.r_[dose_a, dose_b, dose_c],
        "dose": ["低劑量"] * len(dose_a) + ["中劑量"] * len(dose_b) + ["高劑量"] * len(dose_c),
    }
)

crp_df.groupby("dose")["crp"].agg(["count", "median", "mean"]).round(2)
count median mean
dose
中劑量 8 13.0 14.12
低劑量 8 16.5 18.12
高劑量 8 9.5 10.00
plt.figure(figsize=(7, 4.5))
sns.boxplot(data=crp_df, x="dose", y="crp", hue="dose", palette="Set3", legend=False)
sns.stripplot(data=crp_df, x="dose", y="crp", color="#333333", alpha=0.7, jitter=0.12)
plt.xlabel("治療劑量")
plt.ylabel("CRP (mg/L)")
plt.title("三組 CRP 分布")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch09_kruskal_crp_groups.png", dpi=300)
plt.show()
Figure 9.3: 三種治療劑量下的 CRP 分布。
kruskal_result = kruskal(dose_a, dose_b, dose_c)

pd.DataFrame(
    {
        "quantity": ["H 統計量", "p 值"],
        "value": [kruskal_result.statistic, kruskal_result.pvalue],
    }
).round(4)
quantity value
0 H 統計量 6.1965
1 p 值 0.0451

若結果顯著,下一步應該做組間事後比較,並控制多重比較造成的第一型錯誤增加。本章先保留這個伏筆,因為多組推論會在後面章節更完整處理。

9.10 Spearman 等級相關

Pearson 相關衡量線性關係,而 Spearman 等級相關衡量單調關係 (monotonic relationship)。如果一個變項增加時另一個變項大致增加或大致減少,即使不是直線,Spearman 相關仍可能捕捉到。

Spearman 相關會先把資料轉成排序,再計算排序間的相關。因此它對離群值較不敏感,也適合次序資料。

9.11 範例 4:身體活動分數與 HbA1c

假設一組糖尿病病人有身體活動分數與 HbA1c。活動分數越高,HbA1c 看起來越低,但關係不一定線性。

activity = np.array([1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12])
hba1c = np.array([9.8, 9.4, 9.1, 8.9, 8.7, 8.4, 8.1, 7.9, 7.6, 7.4, 7.2, 7.1, 6.9, 6.8, 6.7])
corr_df = pd.DataFrame({"activity_score": activity, "hba1c": hba1c})

corr_df.head()
activity_score hba1c
0 1 9.8
1 2 9.4
2 2 9.1
3 3 8.9
4 4 8.7
plt.figure(figsize=(7, 4.5))
sns.regplot(data=corr_df, x="activity_score", y="hba1c", lowess=True, scatter_kws={"s": 80}, line_kws={"color": "#d62828"})
plt.xlabel("身體活動分數")
plt.ylabel("HbA1c (%)")
plt.title("Spearman 相關:單調關係")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch09_spearman_activity_hba1c.png", dpi=300)
plt.show()
Figure 9.4: 身體活動分數與 HbA1c 的單調關係。紅線為 LOWESS 平滑線。
spearman_result = spearmanr(activity, hba1c)

pd.DataFrame(
    {
        "quantity": ["Spearman rho", "p 值"],
        "value": [spearman_result.statistic, spearman_result.pvalue],
    }
).round(4)
quantity value
0 Spearman rho -0.9973
1 p 值 0.0000

相關不等於因果。活動量高與 HbA1c 低可能有關,但也可能受到飲食、藥物順從性、病程長短與社經因素影響。Spearman 相關能描述關係,但不能替代研究設計。

9.12 置換檢定的直覺

置換檢定是一種重抽樣方法。其核心想法是:若虛無假設下兩組沒有差異,那麼組別標籤應該可以任意交換。每次隨機重新分配標籤並計算統計量,就能建立虛無假設下的參考分布。

置換檢定的優點是直覺清楚,且不需要強烈分布假設。缺點是計算量較大,不過現代電腦通常不是問題。請注意,置換檢定仍依賴交換性 (exchangeability):在虛無假設下,資料標籤可以交換才合理。

def median_difference(x, y, axis=0):
    return np.median(x, axis=axis) - np.median(y, axis=axis)

perm_result = permutation_test(
    (enhanced_los, standard_los),
    median_difference,
    alternative="less",
    n_resamples=5000,
    random_state=20260627,
)

pd.DataFrame(
    {
        "quantity": ["觀察到的中位數差", "置換檢定 p 值"],
        "value": [median_difference(enhanced_los, standard_los), perm_result.pvalue],
    }
).round(4)
quantity value
0 觀察到的中位數差 -1.5000
1 置換檢定 p 值 0.2827

置換檢定很適合教學,因為它把「如果沒有組間差異,結果會長什麼樣子」這件事具體化。它也提醒我們,統計檢定本質上是在建立一個虛無世界,然後看現實資料在那個世界裡有多不尋常。

9.13 常見陷阱

  1. 以為非參數等於沒有假設:非參數方法仍有資料結構與抽樣假設。
  2. 把 Mann-Whitney U 一律解釋為中位數差異:只有在分布形狀相似時較合理。
  3. 忽略配對結構:配對資料應分析差值或使用配對方法。
  4. 只報 p 值:仍應報告中位數、四分位距或效果量。
  5. 多組檢定顯著後不做適當事後比較:Kruskal-Wallis 只告訴你至少一組不同。
  6. 把相關當因果:Spearman 相關只描述單調關係。

9.14 本章重點整理

  • 非參數方法較少依賴特定母群體分布,常用於偏態、小樣本或次序資料。
  • Mann-Whitney U 檢定用於兩獨立樣本的排序比較。
  • Wilcoxon 符號等級檢定用於配對資料或單一樣本中位數相關問題。
  • Kruskal-Wallis 檢定用於三組以上獨立樣本比較。
  • Spearman 等級相關描述兩變項的單調關係。
  • 置換檢定透過重新排列標籤建立虛無分布。
  • 非參數方法也需要搭配圖形、摘要統計與臨床解釋。

9.15 小練習

  1. 某研究比較兩組住院費用,資料明顯右偏,你會考慮哪一種檢定?為什麼?
  2. 若同一批病人治療前後疼痛分數為次序尺度,應使用獨立樣本方法還是配對方法?
  3. Kruskal-Wallis 檢定顯著後,為何不能直接知道哪兩組不同?
  4. Spearman 相關與 Pearson 相關的主要差異是什麼?
  5. 用自己的話說明置換檢定的虛無分布如何產生。
practice_group_a = np.array([12, 14, 15, 18, 22, 40])
practice_group_b = np.array([8, 9, 11, 12, 13, 18])
practice_mw = mannwhitneyu(practice_group_b, practice_group_a, alternative="less")

practice_before = np.array([6, 7, 8, 7, 9, 6])
practice_after = np.array([5, 5, 6, 6, 7, 5])
practice_wilcoxon = wilcoxon(practice_after, practice_before, alternative="less")

pd.DataFrame(
    {
        "檢定": ["Mann-Whitney U", "Wilcoxon signed-rank"],
        "統計量": [practice_mw.statistic, practice_wilcoxon.statistic],
        "p 值": [practice_mw.pvalue, practice_wilcoxon.pvalue],
    }
).round(4)
檢定 統計量 p 值
0 Mann-Whitney U 5.0 0.0223
1 Wilcoxon signed-rank 0.0 0.0156

9.16 Glossary

中文術語 English term 說明
非參數方法 nonparametric methods 較少依賴特定母群體分布形式的統計方法。
排序 rank 將觀察值依大小轉換為名次。
次序資料 ordinal data 有自然順序但間距未必相等的資料。
Mann-Whitney U 檢定 Mann-Whitney U test 比較兩獨立樣本排序分布的非參數檢定。
Wilcoxon rank-sum test Wilcoxon rank-sum test Mann-Whitney U 檢定的另一名稱。
符號檢定 sign test 只使用差值方向進行推論的非參數檢定。
Wilcoxon 符號等級檢定 Wilcoxon signed-rank test 使用配對差值方向與大小排序的非參數檢定。
Kruskal-Wallis 檢定 Kruskal-Wallis test 比較三組以上獨立樣本排序分布的非參數檢定。
Spearman 等級相關 Spearman rank correlation 基於排序衡量兩變項單調關係的相關係數。
單調關係 monotonic relationship 一變項增加時另一變項大致持續增加或減少的關係。
置換檢定 permutation test 透過重新排列標籤建立虛無分布的重抽樣檢定。
交換性 exchangeability 在虛無假設下觀察值標籤可交換的性質。
事後比較 post-hoc comparison 多組整體檢定後進一步比較特定組別的方法。
多重比較 multiple comparisons 同時進行多個比較而增加第一型錯誤風險的情況。