迴歸模型診斷
四張殘差圖各自在抓什麼、為什麼全部通過還是可能漏掉問題、VIF 的重要性被高估在哪裡、影響點怎麼量,以及逐步迴歸為什麼是醫學論文最常見的統計錯誤之一——用模擬把它做給你看。
診斷在檢查什麼
跑出一組係數不代表模型是對的。線性迴歸的每一個 p 值與信賴區間,都建立在四個假設上:
- 設定正確——放進去的變項與結果的關係形狀對了(線性、有沒有交互作用)
- 變異數固定(homoscedasticity)——殘差的散布不隨預測值改變
- 殘差近似常態——影響的主要是小樣本時的區間,大樣本靠中央極限定理救得回來
- 觀察彼此獨立——這一條看不出來,要靠設計(重複測量、群集抽樣、同一家醫院的病人都會違反)。 違反之後怎麼辦,見重複測量與群集資料
診斷就是逐條檢查前三條,外加兩個報表看不出來的問題:共線性與影響點。 本頁沿用 B2-01 的線性模型與 B2-02 的邏輯模型,你已經讀過它們的報表。
殘差圖要看什麼
figures/scripts/B2-04-model-diagnostics.R| 圖 | 在抓什麼 | 壞掉長什麼樣 | 本模型 |
|---|---|---|---|
| A. 殘差 vs 預測值 | 設定是否正確(漏掉的曲率、交互作用) | 平滑線呈 U 形或倒 U 形 | 未偵測到整體曲率(Tukey 檢定 p 0.632,未達統計顯著) |
| B. 常態 Q-Q | 殘差分布的尾巴 | 兩端離開對角線(厚尾、偏態) | 未偵測到偏離常態(Shapiro-Wilk p 0.765,未達統計顯著) |
| C. 尺度位置 | 變異數是否固定 | 平滑線往右上爬(喇叭形) | 未偵測到變異數隨預測值變化(Breusch-Pagan p 0.765,未達統計顯著) |
| D. 槓桿 vs 殘差 | 有沒有單點在主導模型 | 有點落在 Cook’s D 0.5 或 1 的等高線外 | 最大 Cook’s D 0.118 |
殘差在低預測值那一半的標準差是 628, 高預測值那一半是 643——幾乎一樣,C 圖平坦是有數字支持的。
figures/scripts/B2-04-model-diagnostics.R共線性與 VIF
共線性(collinearity)是指某個變項幾乎可以被其他變項的線性組合預測出來。 變異數膨脹因子(variance inflation factor, VIF)量的就是這件事:
其中 是「把第 個變項當結果、用其他所有變項去迴歸」得到的 R²。 代表那個係數的標準誤被放大了 2 倍。
B2-01 那個模型的 VIF 全部很低(最大 1.34)。
為了示範,故意再放進一個「同一個體重換算成公斤」的欄位——這在真實資料合併之後很常見。
注意換算時帶了一點量測/四捨五入誤差(lwt * 0.4536 + rnorm(n, 0, 0.5)),
這正是真實資料合併後的樣子;如果換算是完全等比例的,R 會判定它與 lwt 完全共線(aliased)、
係數直接給 NA、VIF 是無限大,跑不出下面這張表:
| 變項 | 原模型 VIF | 加入重複欄位後 VIF |
|---|---|---|
| age | 1.10 | 1.11 |
| lwt | 1.22 | 771.05 |
| lwt_kg | — | 771.66 |
| smoke_fYes | 1.16 | 1.16 |
| race_fBlack | 1.19 | 1.19 |
| race_fOther | 1.34 | 1.35 |
| ht | 1.08 | 1.08 |
| ui | 1.04 | 1.04 |
影響點
離群值(outlier)、槓桿(leverage)、影響力(influence)是三件不同的事:
- 離群值: 離預測值很遠 → 看標準化殘差
- 槓桿: 落在共變項空間的邊緣 → 看 hat value;平均值是 = 0.042
- 影響力:把它拿掉,係數會不會變 → Cook’s distance,它是前兩者的乘積效果
只有第三件事真的要緊。 很極端但落在迴歸線上的點,槓桿高、影響力低, 它其實在幫模型固定斜率。
本模型 Cook’s D 最大的五位:
| 列號 | 新生兒體重 (g) | 產婦年齡 | 產婦體重 (lb) | 槓桿 | 標準化殘差 | Cook’s D |
|---|---|---|---|---|---|---|
| 130 | 4990 | 45 | 123 | 0.106 | 2.82 | 0.118 |
| 133 | 1135 | 34 | 187 | 0.153 | -1.71 | 0.066 |
| 132 | 1021 | 29 | 130 | 0.057 | -2.90 | 0.063 |
| 106 | 3790 | 25 | 241 | 0.143 | 1.66 | 0.058 |
| 102 | 3756 | 19 | 184 | 0.108 | 1.72 | 0.045 |
這一頁的診斷還替 B2-01 收了一個尾。那頁的年齡平方項在統計上被支持(p 0.018), 但把 3 位超過 35 歲的產婦拿掉重跑,證據就消失了(p 0.373)。 3 個人可以決定一個 189 人模型裡的一項結論—— 這就是為什麼「顯著」與「穩健」是兩件事,也是為什麼多項式不如樣條。
二元與計數結果的診斷不一樣
線性模型的那四張圖不能直接搬到邏輯迴歸上。 原因很簡單:結果只有 0 與 1 兩個值,原始殘差只能落在兩條曲線上, 畫出來永遠是兩條帶子,看不出任何東西(這個模型的原始殘差範圍是 -0.74 到 0.91, 但形狀完全由結果值決定)。
要看的東西換成三樣:
1. 校準(calibration)——預測機率與實際發生率對不對得上。把病人依預測機率分成十組:
| 十分位 | 人數 | 平均預測機率 | 預期事件數 | 實際事件數 |
|---|---|---|---|---|
| 1 | 19 | 0.057 | 1.1 | 0 |
| 2 | 19 | 0.102 | 1.9 | 2 |
| 3 | 19 | 0.154 | 2.9 | 5 |
| 4 | 19 | 0.207 | 3.9 | 3 |
| 5 | 19 | 0.243 | 4.6 | 4 |
| 6 | 18 | 0.283 | 5.1 | 7 |
| 7 | 19 | 0.341 | 6.5 | 6 |
| 8 | 19 | 0.450 | 8.6 | 8 |
| 9 | 19 | 0.551 | 10.5 | 11 |
| 10 | 19 | 0.731 | 13.9 | 13 |
Hosmer-Lemeshow 檢定統計量 4.68,df = 8, p 0.792——未偵測到系統性的校準偏差。
2. 區辨力(discrimination)——見ROC 與 AUC(B4 家族)。校準與區辨是兩件獨立的事: 一個模型可以排序很準但機率整體偏高,也可以機率很準但排不出順序。
3. 影響力仍然要看——glm 物件一樣有 cooks.distance() 與 hatvalues(),判讀方式相同。
至於Poisson 迴歸,最關鍵的診斷不是殘差圖而是分散度: Pearson 卡方除以自由度是否接近 1。那一頁的例子是 11.05,也就是說標準誤要放大約 3.32 倍。
逐步迴歸:醫學論文最常見的統計錯誤之一
逐步迴歸(stepwise regression)是指讓演算法依 p 值或 AIC 自動增刪變項,
最後留下一組「顯著」的變項。R 的 step() / stepAIC()、SPSS 的 forward / backward 選項都是。
它在論文裡非常普遍,而且幾乎總是錯的。用模擬把它做給你看:
拿 B2-01 那個模型,額外塞進 10 個純粹的標準常態亂數
(與新生兒體重毫無關係),跑 stepAIC(),重複 300 次。
figures/scripts/B2-04-model-diagnostics.R更狠的版本:把結果變項也換成亂數,只丟 15 個亂數預測項進去, 資料裡沒有任何東西可以找。結果是 300 次執行中:
- 平均留下 2.59 個變項
- 55% 的最終模型至少有一個變項 p < 0.05
- 平均 R² 是 0.047
- 64% 的最終模型整體 F 檢定也「顯著」
最後這一項要小心分母:這 300 次裡有 24 次
stepAIC 把變項全部刪光、只剩截距,那種模型沒有整體 F 檢定。
上面的 64% 是把這 24 次
算成「不顯著」的版本(分母 300);若只算 276 次
真的有 F 檢定的執行,比例是 70%。
兩個數字都要看——只報後者等於把「逐步迴歸什麼都沒選中」這些最接近虛無的執行悄悄拿掉,
數字會往「逐步迴歸很會製造假象」的方向多推幾個百分點。順帶一提,
連純亂數資料都有 92% 的執行被 stepAIC 留下至少一個變項。
那變項該怎麼選
| 你的問題是 | 該怎麼做 |
|---|---|
| 估某個暴露的效應(大多數臨床研究) | 依因果圖(DAG)決定要校正哪些變項,事先寫進計畫書。不看 p 值決定去留。 |
| 建預測模型 | 先算所需樣本數,再用懲罰性方法(LASSO、ridge、彈性網),並做內部驗證(bootstrap / 交叉驗證)與外部驗證。報告依 TRIPOD。 |
| 探索性、沒有事先假設 | 可以做,但整篇要標明為探索性,不報 p 值當結論,且結果需要另一份資料驗證。 |
三種情況都有一個共同點:變項的去留不由同一份資料的 p 值決定。
動手跑一次
library(MASS)
data(birthwt, package = "MASS")
bw <- birthwt
bw$race_f <- factor(bw$race, levels = 1:3, labels = c("White", "Black", "Other"))
bw$smoke_f <- factor(bw$smoke, levels = 0:1, labels = c("No", "Yes"))
fit <- lm(bwt ~ age + lwt + smoke_f + race_f + ht + ui, data = bw)
par(mfrow = c(2, 2)); plot(fit) # 四張標準診斷圖
# ⚠️ 標準四張圖的橫軸是預測值;單一變項的曲率會被稀釋掉
plot(bw$age, residuals(fit)); lines(lowess(bw$age, residuals(fit)))
# VIF,從定義算(不需要 car)
X <- model.matrix(fit)[, -1]
sapply(seq_len(ncol(X)), function(j) 1 / (1 - summary(lm(X[, j] ~ X[, -j]))$r.squared))
# 共線性示範:把體重換算成公斤再放一次
# ⚠️ 換算要帶一點誤差,否則兩欄完全等比例,R 會判為 aliased、係數給 NA、算不出 VIF
bw$lwt_kg <- bw$lwt * 0.4536 + rnorm(nrow(bw), 0, 0.5)
fit_col <- lm(bwt ~ age + lwt + lwt_kg + smoke_f + race_f + ht + ui, data = bw)
summary(fit_col)$coefficients[c("lwt", "lwt_kg"), ]
# 影響點:找出來,然後實際重跑看結論會不會變
cd <- cooks.distance(fit)
worst <- which.max(cd)
coef(fit)["smoke_fYes"]
coef(lm(formula(fit), data = bw[-worst, ]))["smoke_fYes"]
# 逐步迴歸會撿到什麼:塞 10 個亂數進去試一次
d <- bw[, c("bwt", "age", "lwt", "smoke_f", "race_f", "ht", "ui")]
for (j in 1:10) d[[paste0("z", j)]] <- rnorm(nrow(d))
summary(stepAIC(lm(bwt ~ ., data = d), direction = "both", trace = 0))驗證環境:R 4.6.0 + MASS 7.3.65。car 套件未安裝,本頁的 VIF 由定義計算;car::vif() 對因子給的是 GVIF,數值不同。
import numpy as np
import statsmodels.api as sm
import statsmodels.formula.api as smf
from statsmodels.stats.outliers_influence import variance_inflation_factor
bw = sm.datasets.get_rdataset("birthwt", "MASS").data
bw["race_f"] = bw["race"].map({1: "White", 2: "Black", 3: "Other"})
bw["smoke_f"] = bw["smoke"].map({0: "No", 1: "Yes"})
fit = smf.ols("bwt ~ age + lwt + C(smoke_f) + C(race_f) + ht + ui", data=bw).fit()
infl = fit.get_influence()
cooks = infl.cooks_distance[0]
lev = infl.hat_matrix_diag
X = np.asarray(fit.model.exog)
vif = [variance_inflation_factor(X, i) for i in range(1, X.shape[1])]
# statsmodels 沒有內建 stepwise——這其實是件好事statsmodels 的 OLSInfluence 提供 cooks_distance / hat_matrix_diag / resid_studentized;VIF 在 statsmodels.stats.outliers_influence。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 跑完迴歸不看任何診斷 | p 值與信賴區間的效力建立在假設上 |
| 只看標準四張圖就宣告設定正確 | 單一變項的曲率會在預測值裡被稀釋;要逐變項畫殘差 |
| 用常態性檢定決定要不要用線性迴歸 | 假設是「殘差」近似常態不是結果變項;大樣本時這條最不重要 |
| 看到 VIF 高就刪變項 | 共線性只傷害捲進去的變項;你關心的那個不在裡面就不必動 |
| 把 VIF 5 或 10 當成硬門檻 | 都是慣例;該問的是標準誤還夠不夠回答問題 |
| 把 car::vif() 的 GVIF 與逐欄 VIF 混著比 | 兩者定義不同,數值不可互比 |
| 把高槓桿點當成離群值刪掉 | 槓桿高不等於有影響力;要看 Cook’s distance |
| 用 Cook’s D 門檻自動清資料 | 應該查是不是資料錯誤,並做敏感度分析 |
| 刪掉影響點但不申報 | 資料操弄 |
| 把線性模型的殘差圖搬到邏輯迴歸 | 0/1 結果的原始殘差只會排成兩條帶子 |
| Hosmer-Lemeshow 不顯著就宣稱校準良好 | 那是「沒有證據說它壞掉」;分組方式與樣本數都會左右結果 |
| 用逐步迴歸挑變項再照常報 p 值 | 選擇本身是多重比較;p 值、CI、R² 全部過度樂觀 |
| 用逐步迴歸的結果宣稱「獨立危險因子」 | 那組變項換個子樣本就會變 |
| 先看單變項 p 值再決定放進多變項模型 | 同一種問題的另一個版本,一樣讓推論失效 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B2-04-model-diagnostics.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
把母親體重同時以磅與公斤兩種單位放進同一個模型。lwt 那一項會怎麼變?
看答案與解析
正確答案: VIF 變成 771.1——兩個幾乎相同的變項會互相解釋
lwt 的 VIF 從 1.2 爆到 771.1。「單位不同、資訊沒變」這個直覺對資料是對的,對估計卻不是:兩個幾乎完全相同的變項會互相解釋,模型無法把效果分給誰,個別係數的標準誤因此暴增。771.7 是全模型的最大 VIF,落在公斤那個版本上,不是 lwt 這一列。值得注意的是 R 平方與殘差標準差幾乎沒變——共線性不會讓模型預測得比較差,它讓個別係數變得無法解讀。
共線性讓 lwt 的 VIF 爆炸。抽菸那一項的係數會受到什麼影響?
看答案與解析
正確答案: 變成 -358.5——只動了很小的幅度,因為它與那兩項不相關
抽菸的係數從 -360.7 移到 -358.5,動了不到一個百分點。共線性只影響彼此高度相關的那幾項,與它們無關的係數不受牽連——所以看到一個很大的 VIF 不代表整個模型要重做,要問的是「我在意的那一項有沒有被捲進去」。647.4 是殘差標準差,不是任何一個係數;它幾乎沒變,正好呼應共線性不傷預測這件事。
這個模型的整體殘差圖、常態性與變異數齊一性檢定都很漂亮。那麼把殘差對各共變項的二次項再迴歸一次,是為了看什麼?
看答案與解析
正確答案: 看年齡是不是線性,p = 0.017,有證據反對線性
整體診斷通過,只代表殘差沒有一眼可見的結構;拆到單一變項才看得出 age 的二次項顯著(0.017),也就是它可能不是線性的。0.749 是母親體重那一項,那一項確實沒問題;0.632 是 Tukey 的 non-additivity 檢定,它問的是整體有沒有非可加性,而它同樣沒抓到年齡這件事。「診斷全部過關」很多時候只代表沒有拆得夠細。
用到這個方法的章節
延伸觀看
生物統計學一 93.【迴歸分析 (1)】Simple Linear Regression Model
Lec04 統計學(二) Ch11.1-11.5 簡單廻歸分析與相關分析素材來源與授權
本頁為原創內容