順序型邏輯迴歸與 shift plot
結果是分級而不是有無時(mRS、NYHA、CTCAE、疼痛分數),二分法會丟掉一半的訊息。common odds ratio 是什麼、proportional odds 是一個假設而不是結果、怎麼檢查它、以及格子人數太少時估計會怎麼壞掉。
分級的結果,被二分之後剩下什麼
臨床研究有一整類結果既不是連續的、也不是有無,而是有序的分級:
- mRS(modified Rankin Scale,改良 Rankin 量表)0–6,中風試驗的標準結果
- NYHA 心衰竭功能分級 I–IV
- CTCAE 不良事件嚴重度 1–5 級
- 疼痛評分 0–10、咳嗽嚴重度 0–2、內視鏡分級、病理分期
這些分級的共同性質是有順序但沒有間距:mRS 從 2 進到 3 與從 4 進到 5,都叫「差一級」, 但那兩件事對病人的意義完全不同。所以既不能當成連續變項去算平均(間距沒有意義), 也不該直接二分成「好 vs 壞」(會把大量訊息丟掉)。
最常見的處理是二分:「mRS 0–2 算 good outcome」。這麼做的代價是 從 mRS 5 進步到 mRS 3 的病人,在分析裡與完全沒有進步的病人被算成同一件事。 中風試驗因此改用 shift analysis(位移分析),問的不是「多少人達標」, 而是「整個分布有沒有往好的方向移動」。它的統計工具就是這一頁的順序型邏輯迴歸 (ordinal logistic regression),而它報出來的那個數字,就是讀者最常撞到卻最少被解釋的 common odds ratio(共同勝算比)。
這一頁的例子
沿用群集與重複測量資料那一頁的甘草含漱液試驗
(medicaldata::licorice_gargle),但換一個結果:拔管當下的咳嗽嚴重度
extubation_cough,分成 0 = 沒有咳嗽、1 = 輕度、2 = 中重度 三級。
233 位病人有紀錄(2 位缺失)。
| 組別 | 0 = 沒有咳嗽 | 1 = 輕度 | 2 = 中重度 | 合計 |
|---|---|---|---|---|
| 對照組 | 71(61%) | 30(26%) | 15(13%) | 116 |
| 甘草含漱組 | 88(75%) | 25(21%) | 4(3%) | 117 |
figures/scripts/B2-13-ordinal.R這張圖就是 shift analysis 的視覺形式。 它把「分布整體往哪邊移動」直接畫出來, 而不是先選一個切點再報一個比例。注意兩件事:無咳嗽的比例上升了 14 個百分點, 而中重度的比例從 13% 掉到 3%—— 後面這個變化的相對幅度大得多,但如果你只二分成「有沒有咳嗽」,它會完全消失。
模型長什麼樣
順序型邏輯迴歸最常用的形式是比例勝算模型(proportional odds model)。
它不是對每一個等級各配一個模型,而是對每一個切點同時建模。三個等級有兩個切點:
「≥ 1 vs 0」與「≥ 2 vs ≤ 1」。MASS::polr 是從每個切點的下側寫這個模型的,
下面印出來的結果就是這個形式:
關鍵在於:每個切點有自己的截距 ,但共用同一個 。 截距吸收「這個等級本來有多常見」,斜率吸收「治療把整個分布推了多遠」。
本頁模型的兩個截距是 0.417(切在 0|1) 與 2.104(切在 1|2), 治療的係數是 -0.723(標準誤 0.283,t = -2.55)。
取指數並翻成「對照組相對於甘草組」的方向,得到 common OR 2.060(95% CI 1.18–3.59)。
動手跑一次
library(medicaldata)
library(MASS)
lg <- licorice_gargle
d <- subset(data.frame(y = lg$extubation_cough, treat = lg$treat), !is.na(y))
table(d$treat, d$y) # 先看格子有多空
fit <- polr(factor(y, ordered = TRUE) ~ factor(treat), data = d, Hess = TRUE)
summary(fit)
exp(-coef(fit)) # common OR:對照組 vs 甘草組
exp(-rev(confint.default(fit))) # 95% CI
# proportional odds:每個切點各配一個二元 logistic
summary(glm(I(y >= 1) ~ factor(treat), data = d, family = binomial))
summary(glm(I(y >= 2) ~ factor(treat), data = d, family = binomial))
# 因為只有一個二元共變項,2x3 表可以直接做飽和模型的概似比檢定
tb <- table(d$treat, d$y)
ll_sat <- sum(ifelse(tb > 0, tb * log(tb / rowSums(tb)), 0))
lrt <- 2 * (ll_sat - as.numeric(logLik(fit)))
c(statistic = lrt, p = pchisq(lrt, df = 1, lower.tail = FALSE))
# 格子太空會怎樣:換成 pacu30min_cough
ds <- subset(data.frame(y = lg$pacu30min_cough, treat = lg$treat), !is.na(y))
table(ds$treat, ds$y)
summary(polr(factor(y, ordered = TRUE) ~ factor(treat), data = ds, Hess = TRUE))
summary(glm(I(y >= 2) ~ factor(treat), data = ds, family = binomial))驗證環境:R 4.6.0 + MASS 7.3.65 + medicaldata 0.2.0。polr() 不給 p 值,只給 t value;要 p 值請自己從常態近似算,或用 confint() 的 profile 區間。注意 polr 的模型寫法是 zeta - beta*x,所以係數的符號方向與一般 glm 相反。
import numpy as np
import statsmodels.api as sm
import statsmodels.formula.api as smf
from statsmodels.miscmodels.ordinal_model import OrderedModel
lg = sm.datasets.get_rdataset("licorice_gargle", "medicaldata").data
d = lg[["extubation_cough", "treat"]].dropna().rename(
columns={"extubation_cough": "y"})
print(np.asarray(sm.stats.Table.from_data(d[["treat", "y"]]).table_orig))
mod = OrderedModel(d["y"].astype(int), d[["treat"]], distr="logit").fit(method="bfgs")
print(mod.summary())
print(np.exp(-mod.params["treat"])) # common OR:對照組 vs 甘草組
# 每個切點各一個二元 logistic
for k in (1, 2):
d[f"ge{k}"] = (d["y"] >= k).astype(int)
print(smf.logit(f"ge{k} ~ treat", data=d).fit(disp=0).summary())statsmodels 的 OrderedModel(distr='logit') 對應 polr;它的截距參數化與 R 不同,比較係數前先確認方向。
假設怎麼檢查
最直覺、也最容易向臨床讀者解釋的檢查方式是:把每一個切點各配一個二元 logistic 迴歸, 把得到的 log OR 並排看。如果比例勝算假設在資料裡站得住,這些數字應該接近。
| 切點 | 對照組事件數 | 甘草組事件數 | OR(對照 vs 甘草) | 95% CI | log OR |
|---|---|---|---|---|---|
| 切在 ≥ 1(任何咳嗽 vs 無) | 45 / 116 | 29 / 117 | 1.92 | 1.10–3.37 | 0.654 |
| 切在 ≥ 2(中重度 vs 其餘) | 15 / 116 | 4 / 117 | 4.20 | 1.35–13.05 | 1.434 |
| 順序型模型:兩個切點一起用 | — | — | 2.06 | 1.18–3.59 | 0.723 |
figures/scripts/B2-13-ordinal.R兩個切點的 log OR 相差 0.780, 換算成 OR 是 2.18 倍。看起來差滿多的——但這個「看起來」值多少錢, 要看不確定性有多大。
因為本頁只有一個二元共變項,這張 2×3 表可以直接做精確的概似比檢定: 飽和模型有四個自由參數、比例勝算模型有三個,所以檢定有 1 個自由度。 結果是 χ² = 2.411,p = 0.121。
格子太空的時候
同一份資料換一個時點:PACU 30 分鐘的咳嗽 pacu30min_cough。這個變項的分布是
| 組別 | 0 = 沒有咳嗽 | 1 = 輕度 | 2 = 中重度 |
|---|---|---|---|
| 對照組 | 88 | 24 | 4 |
| 甘草含漱組 | 99 | 18 | 0 |
全體只有 4 位病人達到中重度, 而且這 4 位全部落在對照組,甘草組是 0 位。這一格是空的。
等級多不等於資訊多
同一份資料的喉嚨痛評分 pacu30min_throatPain 有 7 個等級(0 到
6),比咳嗽的三級多得多。但它的次數分布是:
| 等級 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 人數 | 169 | 20 | 18 | 14 | 9 | 1 | 2 |
73% 的人是 0 分,尾端有 2 個等級 的人數少於五位,最少的只有 1 位。等級數是量表設計的性質, 有效資訊量是資料的性質,兩者無關。 這種分布配順序型模型時,尾端的切點基本上是靠假設在支撐, 所以本頁不對它配模型——實務上會先合併尾端等級,或改用不需要比例勝算假設的做法。
違反比例勝算假設時怎麼辦
檢查發現各切點的 OR 明顯不同時,選項由簡到繁:
| 做法 | 它在做什麼 | 代價 |
|---|---|---|
| 照實報告各切點的 OR | 放棄用一個數字概括,把兩三個切點的估計並列 | 沒有單一的效果量;多重比較的問題浮現 |
| 部分比例勝算模型(partial proportional odds) | 只讓違反假設的那個變項有各切點自己的係數,其餘共用 | 要用 VGAM 或 ordinal 套件;解讀變複雜 |
| 多項式邏輯迴歸(multinomial logistic) | 完全放棄順序資訊,每個等級對參考等級各一組係數 | 參數變多、檢定力下降,而且丟掉了「有順序」這個真實訊息 |
| 連續比例模型(continuation ratio) | 改問「已經到了 k 級的人,會不會再進到 k+1 級」 | 回答的是不同的臨床問題,要確認那個問題才是你要的 |
| 無母數的位移檢定(Wilcoxon / van Elteren) | 只檢定分布有沒有位移,不估效果量 | 沒有可以校正共變項的效果量可報 |
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 把分級當連續變項算平均與 t 檢定 | 等級之間的間距沒有意義,平均數不可解讀 |
| 沒查比例勝算假設就報 common OR | 那個「common」是假設,不是結果 |
| 假設檢定不顯著就寫「假設成立」 | 只能寫「未偵測到偏離的證據」;小樣本下檢定力很低 |
| 只看 p 值不看各切點估計的差距與區間 | p 值不顯著可能只是資訊不足,區間才看得出能排除多少 |
| 先試幾個切點再挑最顯著的二分 | 選擇後的 p 值失效,是隱藏的多重比較 |
| 報 common OR 卻不寫方向 | 要寫清楚是哪一組相對於哪一組、往哪個方向 |
| 稀疏格出現巨大 OR 仍照樣報 | 完全分離時該估計值不存在,見邏輯迴歸 |
| 以為順序型模型能免疫於稀疏格 | 它靠比例勝算假設借資訊,而該假設在空格處無法驗證 |
| 等級數多就以為資訊量大 | 尾端每級一兩位病人時,那些切點是假設在撐 |
| shift plot 只畫二分後的比例 | 那就失去 shift analysis 的全部意義;要畫完整分布 |
| 拿 common OR 直接與二分後的 OR 比大小 | 兩者定義的對比不同,數值不可互換 |
| 重複測量的分級結果直接配 polr | 同一個人多次測量不獨立,見群集與重複測量資料 |
相關頁面
- 群集與重複測量資料——同一份
licorice_gargle資料,改看四個時點的喉嚨痛評分 - 邏輯迴歸——二元結果的基礎,以及完全分離長什麼樣子
- 多重比較——為什麼切點要事先指定
- 卡方與 Fisher 精確檢定——列聯表的無母數檢定,不使用順序資訊
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B2-13-ordinal.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
檢查比例勝算假設的做法,是在每個切點各配一個 logistic。cough 大於等於一這個切點得到的是哪一個數字,它要拿來跟什麼比?
看答案與解析
正確答案: 1.92——該切點的 odds ratio,拿來跟其他切點的 odds ratio 比
這個切點的 odds ratio 是 1.92,而檢查比例勝算假設要做的,是把各個切點的 OR 排在一起看它們差多少——假設說的正是「每一個切點的 OR 都相同」。1.10 與 3.37 是同一個 OR 的信賴區間兩端,不是另外的估計。跟模型的共同 OR 比也不對:共同 OR 是在假設成立的前提下算出來的,用它當基準等於預設了要檢查的東西。差太多的時候,那個共同 OR 不代表任何一個切點,而模型仍然只會印出一個數字。
在那個稀疏的結果變項上,cough 大於等於二這個切點的 licorice 組事件數是零。模型會怎麼反應?
看答案與解析
正確答案: 有 0 個事件就是完全分離,估計被推向無限大,而模型照樣印出一個數字
有一格是零就是完全分離:那個切點的 log odds ratio 被推到十幾、信賴區間上界跑到無限大,而模型不會停下來,它照樣印出一個看起來正常的數字。4 是對照組在同一個切點的事件數——另一組有資料並不能救回來,分離的條件是任何一格為零。18 是另一個切點上 licorice 組的事件數,模型不會因為別的切點健康就改用它。看到一個大得離譜的 OR 配上一個大得離譜的信賴區間,先去數那張表的格子。
throat pain 有七個等級,其中兩個等級的人數少於五人。這對比例勝算模型有什麼影響?
看答案與解析
正確答案: 有 2 個等級人數過少,那些切點幾乎沒有資料可用
兩個等級的人數少於五人,比例勝算模型在那些切點上幾乎沒有資料可用,而它仍然會給出一個看起來很精確的共同 odds ratio——等級多不等於資訊多。7 是等級的個數,把它當成資訊量正好倒過來;1 是最小那一格的人數,合併等級確實是一種處理方式,但合併會改變模型在問的問題,不能說「就沒有問題了」。決定怎麼處理稀疏等級之前,要先知道有幾個切點是靠幾個人撐起來的。
用到這個方法的章節
素材來源與授權
本頁為原創內容