變數選擇
預測模型挑變數的目標與病因學研究完全不同——不看因果、只看有沒有幫助預測。這一頁把三種常見策略跑在同一份乳癌世代上,這份資料分不出它們的外部表現差別(三個點估計只差在小數點第三位,本頁未估計不確定性),真正的差別在別的地方:同一個演算法在同一群人的重抽樣上,會給你幾十個不同的「最終模型」。最後一節談怎麼讀機器學習預測模型的論文——variable importance 與 SHAP 各自說了什麼、沒說什麼,以及為什麼驗收標準一格都沒有改變。
換了問題,挑變數的規則就整套換掉
前面 B2、B6 兩個家族的迴歸都在回答病因學的問題:這個暴露對這個結果有沒有效應。 那種模型裡「要不要放某個變項」是一個因果結構的問題——它是干擾因子(confounder)就必須放, 它是中介(mediator)就不能放,它是對撞因子(collider)放了會製造偏誤。判準在 世代研究與 DAG 那一頁。
預測模型問的是這個人的風險是多少。於是同一個決定換了判準:
| 病因學模型 | 預測模型 | |
|---|---|---|
| 某變項要不要進去 | 看它在因果圖上的位置 | 看它有沒有讓預測變準 |
| 中介變項 | 放進去會擋掉要估的效應 | 可以放,只要它在部署時拿得到 |
| 共線性 | 目標係數會不穩,是問題 | 只要預測值穩定就不是問題 |
| 係數怎麼解讀 | 效應量,要能講因果 | 不要解讀,它只是權重 |
| 驗收標準 | 估計無偏、假設成立 | 在沒看過的人身上表現如何 |
三種策略,同一份資料
用 survival::rotterdam(開發,2982 位病人、1713 個事件)
配 survival::gbsg(外部驗證,686 位)。候選變項有 8 個:
年齡、停經狀態、腫瘤大小、分級、淋巴結數、PgR、ER、荷爾蒙治療。
三種在論文裡最常見的做法:
- 全模型法(full model approach)——事前把 8 個全部指定進去,不論 p 值。
- 逐步迴歸——用 AIC 從全模型往回刪。
- 單變量篩選——先一個一個變項各跑一次,p < 0.05 的才進多變量模型。
| 策略 | 留下幾個變項 | 開發世代 C-index | 外部 C-index | 外部校準斜率 |
|---|---|---|---|---|
| 全模型法(事前指定) | 8 | 0.670 | 0.662 | 0.723 |
| 向後逐步迴歸(AIC) | 6 | 0.669 | 0.661 | 0.721 |
| 單變量篩選(p < 0.05) | 7 | 0.669 | 0.662 | 0.722 |
逐步迴歸的問題不是它挑得不準,是它挑得不穩
模型診斷那一頁已經用 300 次模擬示範過一件事:
把 10 個純亂數變項丟進 stepAIC(),它平均會留下
1.81 個,其中 48.0% 的執行至少出現一個
「顯著」的亂數。那個示範不在這裡重做。
這一頁問一個不同的問題:同一個演算法、同一群人,只是重抽一次樣本,它會給你同一個模型嗎?
把開發世代重抽 200 次,每次都跑一遍相同的向後 AIC,記錄每個變項被留下的比例:
figures/scripts/B5-01-variable-selection.R| 變項 | 單變量 p 值 | 在完整資料的最終模型裡? | 重抽樣選中率(全部病人) | 重抽樣選中率(只抽 200 人) |
|---|---|---|---|---|
| age | < 0.001 | 是 | 62% | 32% |
| menopausal | < 0.001 | 否 | 40% | 22% |
| tumour size | < 0.001 | 是 | 100% | 79% |
| grade 3 | < 0.001 | 是 | 100% | 56% |
| nodes | < 0.001 | 是 | 100% | 99% |
| log PgR | < 0.001 | 是 | 76% | 31% |
| log ER | 0.773 | 否 | 27% | 32% |
| hormonal tx | < 0.001 | 是 | 63% | 26% |
在 200 次重抽樣裡,向後 AIC 一共產生了 25 個不同的模型, 出現最多次的那一個也只佔 22%。 把每次抽樣縮到兩百人(更接近多數單中心研究的規模),變成 69 個不同的模型,最常見的那個只佔 12%。
單變量篩選有一個逐步迴歸沒有的毛病
單變量篩選(先各跑一次、p < 0.05 才進多變量)比逐步迴歸更常見,因為它看起來更保守。它不是。
看上表的停經狀態:單變量 p 值遠小於 0.001,所以它通過篩選、進了多變量模型; 但向後 AIC 把它刪掉了。原因是它和年齡幾乎重疊——校正年齡之後它沒有剩下什麼。
反過來的情況更危險:一個變項可以在單變量下毫無訊號,卻在校正之後變得重要。 單變量篩選會在它有機會被校正之前就把它丟掉,而且丟掉的痕跡不會出現在最終模型的任何一個數字裡。
那到底該怎麼挑
現在的建議(TRIPOD+AI、Riley 等人的方法學文獻)基本上是同一句話: 在看到結果之前就把候選變項與模型形式定下來,然後全部放進去。
| 做法 | 什麼時候合理 | 要注意 |
|---|---|---|
| 依領域知識事前指定,全部保留(全模型法) | 預設做法。候選變項來自既有模型、guideline、臨床上實際拿得到的東西 | 候選數量必須先過 樣本數那一關 |
| 用懲罰迴歸讓資料決定權重 | 候選變項多、樣本量不寬裕時 | lasso 的「挑掉」同樣不穩定,見 收縮那一頁 |
| 依「拿不拿得到」刪減 | 永遠可以做,而且應該先做 | 這不是統計決定,不會產生樂觀偏誤 |
| 逐步迴歸 | 幾乎沒有 | 若真的用了,選擇過程必須整個進到重抽樣迴圈裡,見 內部驗證那一頁 |
動手跑一次
library(survival); library(MASS)
data(cancer, package = "survival")
rot <- rotterdam
rot$rfs_time <- pmin(rot$rtime, rot$dtime)
rot$rfs_event <- as.integer(rot$recur == 1 | rot$death == 1)
rot$size_mm <- c("<=20" = 15, "20-50" = 35, ">50" = 60)[as.character(rot$size)]
rot$grade3 <- as.integer(rot$grade >= 3)
rot$log_pgr <- log1p(rot$pgr); rot$log_er <- log1p(rot$er)
cand <- c("age", "meno", "size_mm", "grade3", "nodes", "log_pgr", "log_er", "hormon")
f <- as.formula(paste("Surv(rfs_time, rfs_event) ~", paste(cand, collapse = " + ")))
full <- coxph(f, data = rot) # 全模型法:就這一行
step <- stepAIC(full, direction = "backward", trace = 0)
# 單變量篩選:注意它問的是「不校正時有沒有關聯」
sapply(cand, function(v)
summary(coxph(as.formula(paste("Surv(rfs_time, rfs_event) ~", v)),
data = rot))$coefficients[1, "Pr(>|z|)"])
# 穩定度:同一份資料重抽,看它每次挑到什麼
tally <- setNames(numeric(length(cand)), cand)
for (b in 1:200) {
d <- rot[sample(nrow(rot), replace = TRUE), ]
ff <- f; environment(ff) <- environment() # 少這行會整批靜默失敗
st <- stepAIC(coxph(ff, data = d), direction = "backward", trace = 0)
tally[names(coef(st))] <- tally[names(coef(st))] + 1
}
round(100 * tally / 200)驗證環境:R 4.6.0 + survival 3.8.6 + MASS 7.3.65。⚠️ stepAIC() 會在「呼叫它的那一層」重新求值模型呼叫式,所以迴圈裡的資料物件必須讓公式看得見;產圖腳本裡有一行 environment(ff) <- environment() 就是在處理這件事,少了它每一次重抽樣都會靜默失敗。
import numpy as np
import pandas as pd
from lifelines import CoxPHFitter
RD = "https://vincentarelbundock.github.io/Rdatasets/csv/"
d = pd.read_csv(RD + "survival/rotterdam.csv")
# 與上面 R 端相同的衍生欄位。rotterdam 原始欄位沒有這些,少了這一段
# 下面每一行都會 KeyError。
d["rfs_time"] = d[["rtime", "dtime"]].min(axis=1)
d["rfs_event"] = ((d["recur"] == 1) | (d["death"] == 1)).astype(int)
d["size_mm"] = d["size"].map({"<=20": 15, "20-50": 35, ">50": 60})
d["grade3"] = (d["grade"] >= 3).astype(int)
d["log_pgr"] = np.log1p(d["pgr"]); d["log_er"] = np.log1p(d["er"])
cand = ["age", "meno", "size_mm", "grade3", "nodes", "log_pgr", "log_er", "hormon"]
cols = cand + ["rfs_time", "rfs_event"]
# 全模型法
full = CoxPHFitter().fit(d[cols], "rfs_time", "rfs_event")
print(full.concordance_index_)
# 懲罰版本(B5-02 的主題),不是變數選擇的替代品而是它的替代方案
pen = CoxPHFitter(penalizer=0.1, l1_ratio=1.0).fit(d[cols], "rfs_time", "rfs_event")
print(pen.params_[pen.params_ != 0].index.tolist())Python 這邊刻意不提供逐步迴歸的實作:statsmodels 沒有內建 stepwise,而這是件好事。lifelines 的 CoxPHFitter 可以配 Cox 模型,懲罰版本用 penalizer 參數。
當論文用的是機器學習模型時
「XGBoost 預測 AKI」「隨機森林預測敗血症」這類論文在臨床資訊與重症領域已經很多, 而醫學生第一次撞到它們通常是在書報討論上。這一節只教怎麼讀,不教怎麼跑—— 把模型換成森林或梯度提升是另一條技能樹,但驗收標準一格都沒有變。
以下依序談 variable importance、impurity 偏誤、SHAP,以及那件沒有改變的事。
一、variable importance 不是迴歸係數。 它沒有方向、沒有信賴區間、也不是效應量。 一個變項排在第一名,只代表在那個套件所用的重要度定義下它排第一—— permutation importance 的意思是「把它打亂之後模型的預測變差最多」, 而樹系套件的預設往往是下一點要講的 impurity-based 版本,兩者可以排出不同的名次。 不論是哪一種,它都不告訴你風險是升是降、升多少, 更不能被引用成「某因子使風險上升多少」。論文的 Discussion 把重要度排序寫成危險因子清單時, 那是把兩種完全不同的量當成同一個。
二、impurity-based importance 對高基數變項有系統性偏誤。 樹在每個節點挑切點, 取值愈多的變項可切的位置愈多,愈容易在隨機的雜訊上找到一個看起來有用的切法。 所以連續變項與類別很多的變項(例如就診科別、藥品代碼)會系統性地排在前面, 而二元變項會系統性地被低估——這與它們實際上有沒有用無關。 用 permutation importance 或在獨立資料上重算會緩解,但不會消失。
三、SHAP 是局部歸因,不是因果效應。 它回答的是「對這一位病人, 模型的預測值相對於基準被這個變項推了多少」,而那是在模型已經長成這樣的前提下的分解。 模型學到的關聯若來自干擾、選擇或反向因果,SHAP 會忠實地把那份關聯歸因出來, 而且畫得非常有說服力。一張漂亮的 SHAP 圖不是因果證據,它是模型的自述。
四、驗收標準完全沒變。 鑑別力、校準、 決策曲線、 外部驗證——這四項一件都不能少。 而 ML 論文最常跳過的正是校準:樹系模型的預測值天然往中間集中, 校準斜率大於 1 是常見結果,而只報 AUC 的論文不會讓你看到這件事。
讀論文時要問的四件事
- 候選變項是事前定的,還是資料挑的? Methods 沒寫「事先指定」就要當作是資料挑的。
- 挑完之後有沒有重新驗證? 選變數本身用掉了自由度;事後的 p 值、CI、C-index 全部偏樂觀。
- 模型裡的變項在部署時真的拿得到嗎? 這是 data leakage 唯一能被讀者抓到的地方。
- 有沒有報告被刪掉的那些? 只列最終模型的論文,讀者無從判斷刪的過程有多勉強。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 用逐步迴歸挑變項,再照常報 p 值與 CI | 選擇本身是多重比較;重抽一次就會換一組變項 |
| 說「最終模型納入的都是獨立危險因子」 | 那是預測用的權重,不是因果效應;而且換個子樣本就會變 |
| 單變量 p 值不顯著就把變項丟掉 | 有些變項只在校正之後才有貢獻,丟掉的痕跡不會留在任何數字裡 |
| 把預測模型的變數選擇當成病因學的變數選擇 | 判準完全不同:一個看因果結構,一個看預測貢獻 |
| 因為擔心共線性而刪掉變項 | 預測模型只要預測值穩定就好,共線性不是問題 |
| 把部署時拿不到的變項放進模型 | data leakage;所有統計檢查都會通過,只有實際上線才會發現 |
| 候選變項數量不看樣本量就開始篩 | 順序反了,先過樣本數那一關 |
| 用「模型比較簡潔」當作刪變項的理由 | 簡潔本身不是驗收標準,預測表現才是 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B5-01-variable-selection.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
三種變數選擇策略在同一個外部世代上算出來的校準斜率分別是 0.723、0.721、0.722。這組數字最該被讀成什麼?
看答案與解析
正確答案: 三個值都明顯小於 1、彼此又只差在小數點第三位,0.723 與另外兩個說的是同一件事:模型把外部世代的風險差異拉得太極端
校準斜率小於 1 的意思是預測出來的風險差異太極端——高風險的人被推得太高、低風險的人被壓得太低。這跟「整體高估」不是同一件事:整體水準偏移屬於截距那一層,把預測值一律往下調只會動到平均,動不到斜率。至於 0.721、0.722、0.723 誰高誰低,那是在同一個外部世代上各算一次的點估計,本頁沒有估計它們的不確定性,第三位小數的排名不能拿來選策略。三個數字一致說的是同一件事,而且是三種策略共同的校準問題,不是它們彼此優劣的證據。
年齡的單變量 p 值遠小於千分之一,但在重抽樣裡它並不是每次都被向後 AIC 留下。這兩件事放在一起說明什麼?
看答案與解析
正確答案: 年齡被留下的比例是百分之 62.0;單變量 p 值問的是不校正時有沒有關聯,AIC 問的是校正之後還剩多少貢獻,兩個問題可以給出不同答案
62.0 與 100.0 的對比就是重點:腫瘤大小在每一次重抽樣都被留下,年齡只有六成出頭。年齡的單變量關聯很強,但它與停經狀態等變項重疊,校正之後剩下的獨立貢獻不足以每次都讓 AIC 願意付那個參數的代價。把落差讀成「重抽樣次數不夠」也站不住——同一批重抽樣裡腫瘤大小拿到 100.0,雜訊不會只挑年齡欺負。停經狀態的 40.0 確實比年齡更不穩定,但那不能反過來證明年齡是穩的;論文上讀者看到的只有「最終模型納入了哪些變項」,看不到它們各自有多勉強。
把開發世代重抽 200 次,每次跑一遍完全相同的向後 AIC,記錄每次得到的最終模型。這個實驗最重要的產出是什麼?
看答案與解析
正確答案: 同一群人、同一支程式,只換抽樣就產生了 25 個不同的最終模型;論文那句「最終模型納入下列變項」描述的是一次抽樣
用全部病人的版本產生了 25 個不同的模型,不是一個——「收斂到唯一一個」的說法在同一份輸出上就被推翻了;每次只抽兩百人的版本更誇張,69 個。重抽樣也不是把完整資料上那個模型再確認一次,它問的是「同一個演算法遇到另一批同樣來源的人會挑出什麼」,而答案常常是別的東西。真正被繼承下去的是後面每一句以「被納入最終模型」為前提的敘述——某某是獨立危險因子、某某在校正後仍有預測價值——它們全部帶著這個不穩定性。
把每次重抽的樣本從全部病人縮到兩百人之後,多數變項的選中率都下降,只有淋巴結數幾乎沒動。這代表什麼?
看答案與解析
正確答案: 淋巴結數在小樣本下仍有百分之 99.0,訊號強到樣本縮小也撐得住;其他變項一起下滑則說明樣本量也在決定誰進得了最終模型
99.0 與 100.0 幾乎一樣,而分級從 100.0 掉到 55.5——同一個演算法、同一群人的子集,差別只在每次看到幾個人。選中率量的是「這個變項的訊號強到能不能穩定通過 AIC 的門檻」,那由訊號強度與樣本量一起決定,不是這個變項在臨床上重不重要。所以把 55.5 讀成「分級在小族群裡比較不重要」,是把統計檢定力的問題講成了生物學;而只看淋巴結數兩欄都接近滿分就說樣本量沒有影響,是挑了唯一一個不受影響的變項來代表整張表。
停經狀態的單變量 p 值遠小於千分之一,所以它通過了單變量篩選;但向後 AIC 把它從模型裡刪掉了。最合理的解讀是什麼?
看答案與解析
正確答案: 停經狀態只在百分之 40.0 的重抽樣裡被留下;它與年齡幾乎重疊,校正年齡之後沒剩下多少獨立貢獻
40.0 這個選中率跟它的單變量 p 值並不矛盾:單變量看的是停經狀態自己跟結果的關聯,而年齡與停經狀態量的幾乎是同一件事,兩個一起放進模型時,停經狀態能多解釋的部分就很小了。年齡的 62.0 高一些,但那不是「誰是真正的危險因子」的判決——選中率是統計上的穩定度,不是因果地位,這一頁的模型本來就不是為了估計效應而配的。log ER 的 26.5 更低沒錯,但它被刪的理由完全不同:它連單變量關聯都沒有。AIC 也不是按選中率排序刪人,它每一步都在問「這個參數付出的代價換不換得到概似的改善」。
三種策略在完整開發資料上留下的變項個數不同,而它們的外部 C-index 差在小數點第三位。這組對照可以支持什麼結論?
看答案與解析
正確答案: 向後 AIC 留下 6 個變項而外部表現沒有掉下來,說明事件數相對參數非常充足時,被刪掉的本來就貢獻很小
這份開發世代每一個參數分到兩百多個事件,在這種條件下逐步迴歸刪掉的通常是本來就沒什麼貢獻的變項,外部表現當然看不出差別——換到事件數吃緊的資料上,同一個刪法會刪掉真正有用的東西。第二個選項把「這一次分不出差別」讀成「事前指定沒有必要」,可是事前指定要防的不是這一次的 C-index,而是上一節那個問題:同一個演算法重抽一次就換一組變項。第三個選項更直接:變項個數介於中間不是任何一種驗收標準,預測表現才是,而預測表現在這裡分不出高下。
用到這個方法的章節
延伸觀看
What Are Clinical Prediction Rules?
AI 臨床研究實戰 EP7|預測 vs 分類、Data Leakage、Propensity Score
Key Steps and Common Pitfalls in Clinical Prediction Model Research素材來源與授權
本頁為原創內容