交互作用與次族群分析
含交互作用的模型裡四個係數各代表什麼、為什麼 exp(交互作用項) 是「比值的比值」、怎麼從一個模型算回各層別的估計,以及次族群 forest plot 上最容易犯的那個錯:把顯著性的差異讀成差異的顯著性。
交互作用在回答什麼問題
一篇試驗的主要結果給的是平均效果。看完那個數字,臨床上的下一個問題幾乎一定是: 「這個藥對誰比較有效?年紀大的人呢?本來就高風險的人呢?」
這個問題有一個正式的名字:交互作用(interaction), 在流行病學的脈絡下也叫效果修飾(effect modification)。 它問的是——治療效果會不會隨著某個特徵而改變。
回答它的方式不是把資料切成幾塊、各自跑一次、然後比較誰的 p 值比較小。 那是本頁要拆掉的錯誤,而且它是臨床論文裡最常見的統計誤讀之一:
「女性顯著、男性不顯著」=「女性和男性的治療效果不同」
這個等號不成立。左邊是兩個各自對照 0 的檢定,右邊是一個兩者互相對照的檢定。 兩個層別的樣本數不同、事件數不同,光是這一點就足以讓其中一邊跨過 0.05 而另一邊沒有, 即使底下的效果完全沒有隨層別改變。這一頁會用同一份資料把兩種讀法擺在一起, 讓落差看起來像它實際的樣子。
這一頁的例子
沿用隨機對照試驗那一章的 medicaldata::indo_rct:
直腸給予 indomethacin 預防 ERCP 術後胰臟炎(post-ERCP pancreatitis, PEP)的隨機試驗。
602 位受試者,共 79 個 PEP 事件。
整體效果(未校正的邏輯迴歸):indomethacin 組 27/295(9.2%), 安慰劑組 52/307(16.9%), OR 0.49(0.30–0.81), p 0.005。
這份資料被選來教這一頁,是因為它自帶一整排預先指定的次族群變項, 而其中好幾個的層別估計看起來差很多、交互作用檢定卻完全沒有支持。 那正是最難教、也最需要被看見的組合。
library(medicaldata)
data(indo_rct, package = "medicaldata")
d <- indo_rct
d$y <- as.integer(d$outcome == "1_yes") # 1 = 發生 PEP
d$tx <- as.integer(d$rx == "1_indomethacin") # 1 = indomethacin
d$male <- as.integer(d$gender == "2_male") # 0 = 女性(參考層)
# 一個含交互作用的模型,四個係數
fit <- glm(y ~ tx * male, data = d, family = binomial)
summary(fit)
# 層別 OR:不要把資料切開跑兩次,從同一個模型算回來
exp(coef(fit)["tx"]) # 女性層
exp(coef(fit)["tx"] + coef(fit)["tx:male"]) # 男性層
# 男性層的 CI 要用兩個係數的變異數「和」共變異數,不能只加兩個 SE
V <- vcov(fit)
b <- unname(coef(fit)["tx"] + coef(fit)["tx:male"])
se <- sqrt(V["tx", "tx"] + V["tx:male", "tx:male"] + 2 * V["tx", "tx:male"])
exp(c(OR = b, lcl = b - 1.96 * se, ucl = b + 1.96 * se))
# P for interaction:概似比檢定,不是 Wald
fit0 <- glm(y ~ tx + male, data = d, family = binomial)
anova(fit0, fit, test = "LRT")
# 對每個次族群變項重複同一件事
for (v in c("gender", "sod", "pep", "psphinc", "precut", "train", "bsphinc")) {
f <- droplevels(d[[v]])
m0 <- glm(y ~ tx + f, data = d, family = binomial)
m1 <- glm(y ~ tx * f, data = d, family = binomial)
cat(v, anova(m0, m1, test = "LRT")[["Pr(>Chi)"]][2], "\n")
}驗證環境:R 4.6.0 + medicaldata 0.2.0。交互作用檢定一律用 anova(..., test = "LRT"),理由見本頁最後一節。
import numpy as np
import statsmodels.api as sm
import statsmodels.formula.api as smf
from scipy import stats
d = sm.datasets.get_rdataset("indo_rct", "medicaldata").data
d["y"] = (d["outcome"] == "1_yes").astype(int)
d["tx"] = (d["rx"] == "1_indomethacin").astype(int)
d["male"] = (d["gender"] == "2_male").astype(int)
fit = smf.logit("y ~ tx * male", data=d).fit()
print(fit.summary())
# 層別 OR
b_tx, b_int = fit.params["tx"], fit.params["tx:male"]
print(np.exp(b_tx), np.exp(b_tx + b_int))
# 男性層的 CI:用 contrast 向量取變異數,statsmodels 會自動帶入共變異數
c = np.zeros(len(fit.params))
c[list(fit.params.index).index("tx")] = 1
c[list(fit.params.index).index("tx:male")] = 1
se = np.sqrt(c @ fit.cov_params().values @ c)
print(np.exp([b_tx + b_int - 1.96 * se, b_tx + b_int + 1.96 * se]))
# P for interaction:LRT 要自己做
fit0 = smf.logit("y ~ tx + male", data=d).fit()
lr = 2 * (fit.llf - fit0.llf)
print(lr, stats.chi2.sf(lr, df=1))statsmodels 沒有現成的 LRT 介面,要自己從兩個模型的 llf 相減再查卡方分布——下面的程式碼把這步寫出來了。
四個係數各代表什麼
glm(y ~ tx * male) 展開之後是這樣一個模型:
跑出來的四個係數:
| 項 | 它是什麼 | 係數(log 尺度) | 標準誤 | 取指數 | p |
|---|---|---|---|---|---|
(Intercept) | 參考格(安慰劑 × 女性)的 PEP log-odds | -1.5569 | 0.1678 | 0.2108 | < 0.001 |
tx | 女性層裡 indomethacin 相對安慰劑的 log OR | -0.7897 | 0.2880 | 0.4540 | 0.006 |
male | 安慰劑組裡男性相對女性的 log OR | -0.1777 | 0.3986 | 0.8372 | 0.656 |
tx:male | 男性層的治療 log OR 減去女性層的治療 log OR | 0.3927 | 0.6111 | 1.4809 | 0.521 |
一列一列看:
-
截距。把
tx與male都設成 0,剩下的就是截距。 取指數之後 0.2108 是安慰劑組女性的 PEP 勝算—— 可以直接用手驗:那一格有 43 個事件、 204 個非事件,相除就是這個數字。 -
tx的係數,是女性層的治療效果,不是「整體的治療效果」。 取指數 0.4540 就是女性層的 OR。 這是全篇最常被誤讀的一格。 -
male的係數,是安慰劑組裡男性相對女性的效果, 不是「男性整體的風險」。取指數 0.8372。 -
tx:male才是交互作用:男性層的治療 log OR 減去女性層的治療 log OR, 也就是兩個效果之間的差,而不是任何一個效果本身。
exp(交互作用項) 是「比值的比值」
把上面那個模型的定義展開,兩個層別的治療 OR 分別是:
兩式相除, 消掉:
所以交互作用項取指數之後,是一個勝算比的勝算比(ratio of odds ratios)。 產圖腳本把等號兩邊都算出來對過:
| 算法 | 數值 |
|---|---|
| 男性層 OR ÷ 女性層 OR = 0.6723 ÷ 0.4540 | 1.480909 |
| exp(交互作用項) = exp(0.3927) | 1.480909 |
| 兩者相差 | 0 |
這個恆等式是代數上的必然,不是這份資料的巧合——腳本裡用 stopifnot() 卡住,不成立就不會產出。
知道它是「比值的比值」之後,判讀方式就跟著決定了。 它的虛無值是 1(不是 0),要在 log 尺度上做檢定與算信賴區間, 而且它跟一般的 OR 一樣需要看區間: 這裡是 1.48 (0.45–4.91), 這個區間要翻回 OR 才讀得懂它有多寬,因為它是比值不是效果:把兩端各乘上女性層的 OR 0.45,就得到與資料相容的男性層 OR 範圍。
下界 0.447 對應男性層 OR 0.20—— 比女性層的 0.45 更低,也就是男性得到的保護反而更強。 上界 4.906 對應男性層 OR 2.23—— 已經越過 1,也就是在男性層可能反而偏向有害。
同一個區間同時容得下「男性得益更大」與「男性受害」這兩個相反的方向, 本研究並未偵測到治療效果隨性別而異。
從模型算回層別估計
有了四個係數,各層的 OR 直接由定義得到:參考層是 exp(第二個係數), 另一層是 exp(第二個係數 + 交互作用項)。
CI 就沒那麼直接了。男性層的 log OR 是兩個係數相加,它的變異數是
漏掉那個共變異數項,區間就會算錯,而錯的方向由它的正負決定。
在這個模型裡它是負的,所以漏掉它會把區間算得太寬。
上面 R 程式碼裡的 vcov(fit) 就是在取它。
次族群 forest plot 怎麼讀
這份試驗有 7 個二元的預先指定次族群變項。 把每一層的治療 OR 與 P for interaction 一起畫出來,就是論文裡那張圖:
figures/scripts/B2-06-interaction.R同一批數字列成表:
| 次族群 | 層 | n | 事件 | OR | 95% CI | 層別 p | P for interaction |
|---|---|---|---|---|---|---|---|
| 性別 | 女性 | 476 | 63 | 0.45 | 0.26–0.80 | 0.006 | 0.52 |
| 男性 | 126 | 16 | 0.67 | 0.23–1.93 | 0.461 | ||
| 疑似 Sphincter of Oddi 功能異常 | 否 | 107 | 16 | 0.37 | 0.11–1.24 | 0.108 | 0.60 |
| 是 | 495 | 63 | 0.53 | 0.31–0.91 | 0.022 | ||
| 曾發生過 post-ERCP 胰臟炎 | 否 | 506 | 56 | 0.54 | 0.30–0.96 | 0.037 | 0.49 |
| 是 | 96 | 23 | 0.36 | 0.13–0.98 | 0.046 | ||
| 胰管括約肌切開術 | 否 | 259 | 32 | 0.33 | 0.14–0.77 | 0.010 | 0.23 |
| 是 | 343 | 47 | 0.63 | 0.33–1.17 | 0.142 | ||
| Precut 括約肌切開術 | 否 | 570 | 74 | 0.52 | 0.31–0.86 | 0.011 | 0.49 |
| 是 | 32 | 5 | 0.23 | 0.02–2.36 | 0.217 | ||
| 有受訓醫師參與操作 | 否 | 319 | 31 | 0.49 | 0.22–1.08 | 0.076 | 0.95 |
| 是 | 283 | 48 | 0.47 | 0.25–0.90 | 0.023 | ||
| 膽管括約肌切開術 | 否 | 258 | 33 | 0.31 | 0.13–0.72 | 0.006 | 0.16 |
| 是 | 344 | 46 | 0.66 | 0.35–1.23 | 0.192 |
讀這張圖的順序:
- 先看整體效果那一列,它是所有層別的參考點。這裡是 OR 0.49。
- 看每個變項的 P for interaction,判斷有沒有證據支持效果隨這個特徵改變。 本頁 8 個次族群變項的 P for interaction 落在 0.16 到 0.95 之間; 低於 0.05 的有 0 個。
- 最後才看各層的點估計與區間,而且是看它們有沒有一致地落在整體效果附近, 不是看誰的區間有沒有壓到 1。
- 順便看每一層的 n 與事件數。Precut 括約肌切開術「是」那一層只有 32 人、5 個事件, 區間 0.02–2.36 涵蓋了從「幾乎完全防住」到「風險增為兩倍以上」的範圍。這種層別的點估計不含什麼資訊, 而它在圖上跟其他列長得一樣顯眼。
顯著性的差異,不是差異的顯著性
現在把倒數第二欄拿出來看。在 7 個二元次族群變項裡, 有 6 個 出現「一層的 p 低於 0.05、另一層不低於」的組合。 如果拿層別 p 值當判準,這份試驗可以生出 6 種不同的 「這個藥只對某某人有效」的故事。
而同一批資料的 P for interaction 全部不顯著。下面這張圖把兩種讀法擺在一起:
figures/scripts/B2-06-interaction.R以性別為例,把左邊面板的兩列拆開來看:
| 層 | n | OR | 95% CI | 層別 p |
|---|---|---|---|---|
| 女性 | 476 | 0.45 | 0.26–0.80 | 0.006 |
| 男性 | 126 | 0.67 | 0.23–1.93 | 0.461 |
點估計 0.45 與 0.67 看起來確實有距離。 但是:
- 女性層的整個信賴區間,被男性層的信賴區間完全包住。 重疊區間是 0.26–0.80, 佔較窄那條區間的 100%。 也就是說,凡是與女性層資料相容的 OR,沒有一個與男性層資料不相容。
- 兩層的樣本數相差 3.78 倍 (476 對 126),事件數相差 3.94 倍 (63 對 16)。 男性層的區間比女性層寬得多,光靠這一點就足以讓它跨過 1。
- 把「差異」本身估出來:兩個 OR 的比值是 1.48 (0.45–4.91), P for interaction 0.52。
你其實做了幾次檢定
還有一件事讓次族群的層別 p 值更不可靠:它們不只一個。
本頁一共測了 8 個次族群變項。 假設治療效果完全不隨任何一個特徵改變,每個檢定各有 0.05 的機率誤報, 至少出現一個 p 小於 0.05 的機率是
代入之後是 33.7%—— 超過三分之一。而這還只算了交互作用檢定; 如果拿的是層別 p 值,檢定次數要再乘以層數,機率更高。
實務上真實論文的次族群數量往往不只這些,多重比較的代價也就更大。 完整的機制、校正方法與「預先指定」為什麼是唯一有效的防線, 見多重比較與次族群分析。
層別越多,先崩掉的是層別估計,不是檢定
這份試驗還有一個 4 層的次族群變項:收案中心。 它沒有被畫進上面那張 forest plot,理由值得單獨看一次:
| 中心 | n | 事件 | OR | 95% CI |
|---|---|---|---|---|
| 1_UM | 164 | 36 | 0.41 | 0.19–0.91 |
| 2_IU | 413 | 41 | 0.55 | 0.28–1.07 |
| 3_UK | 22 | 2 | 1.22 | 0.07–22.40 |
| 4_Case | 3 | 0 | 估不出來 | —— |
最後一個中心只收了 3 位受試者、 0 個事件,2×2 表有整整一列是零,OR 根本不存在。 倒數第二個中心有 22 位、2 個事件, OR 算得出來,但區間 0.07–22.40 的上界是下界的 336 倍,同樣不含資訊。
而交互作用檢定仍然算得出來,也不被那一層拖累:它是在 3 個自由度上做的整體檢定, P for interaction 0.89。
但要知道那個自由度裡有一格是空的。4_Case 只有
3 位受試者、0 個事件,
與它有關的交互作用參數在概似函數上不可辨識——資料裡沒有任何東西可以決定它的值。
R 是照名目上的參數個數報 3 個自由度,
不是照「資料真的支撐得起幾個參數」報。實際有效的自由度比這個數字小。
在這個例子上結論不受影響:P for interaction 0.89 不論放在 3 還是少一個自由度下都遠離顯著。 但若某個檢定剛好落在門檻附近,這件事就會變成結論的關鍵—— 看到多層次族群的整體檢定,先去數每一層的事件數,再決定那個 df 該不該相信。
LRT 還是 Wald
交互作用檢定有兩條路:
- Wald 檢定:直接看
summary()裡交互作用項那一列的 p 值。 - 概似比檢定(likelihood ratio test, LRT):比較「有交互作用」與「沒有交互作用」兩個模型的概似,
也就是
anova(fit0, fit, test = "LRT")。
在性別這個例子上,兩者幾乎沒有分別: Wald 0.5205、 LRT 0.5222。 本頁全程用 LRT,理由有三個:
- 多層變項只有 LRT 做得到。 收案中心有 4 層, 交互作用是 3 個係數, 要問的是「這 3 個係數是不是同時為零」。 Wald 只給你單一係數的 p 值,逐個看等於又做了一次多重比較。
- Wald 對參數化方式敏感,LRT 不敏感。 換一個參考層、把變項重新編碼, Wald 的 p 值會變,LRT 不會。
- 樣本數小或格子稀疏時 Wald 會失真,而且失真的方向是低估顯著性 (這就是邏輯迴歸那頁完全分離的極端版本: 標準誤爆炸、Wald 的 p 值趨近 1)。次族群分析正好是最容易踩到這件事的場合。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 比較兩個層別各自的 p 值,宣稱效果不同 | 那是兩個各自對照虛無值的檢定;要回答「不同嗎」需要交互作用檢定 |
| P for interaction 大於 0.05 就寫成「各族群效果一致」 | 檢定力不足時無法支持等式;只能寫「未偵測到」 |
| 把含交互作用模型的主效果讀成「整體效果」 | 那是參考層的效果,參考層一換數字就變 |
| 連續修飾因子不置中就讀主效果 | 主效果是「該變項等於 0」時的效果,可能落在資料範圍外 |
| 把資料切開跑兩次當成交互作用分析 | 產不出 P for interaction,且共變項的效果被重複估計 |
| 算另一層的 CI 時忘記共變異數項 | 兩個係數相加的變異數含 2×Cov,漏掉它區間就會算錯,方向由 Cov 的正負決定 |
| 事後挑出次族群才報 | 選擇後的 p 值失效,只能當作產生假說 |
| 次族群數量不揭露 | 讀者無法判斷多重比較的規模 |
| 只報乘法尺度的交互作用就談臨床決策 | 決策看絕對得益,需要風險差尺度的估計 |
| 層別事件數極少仍照樣畫點估計 | 估計可能根本不存在,圖上卻與其他層一樣顯眼 |
| 多層變項逐層兩兩比較 | 應該用自由度大於 1 的整體檢定 |
| 事後拿收案中心切次族群 | 中心混雜了病人組成、操作者與流程,不是一個可以單獨解讀的修飾因子 |
| 用 Wald 檢定多層變項的交互作用 | Wald 只測單一係數,且對參數化方式敏感 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B2-06-interaction.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
次族群森林圖上,女性那一層的 OR 顯著、男性那一層不顯著。要判斷治療效果在兩性之間是否不同,該讀哪一個 p 值?
看答案與解析
正確答案: 0.522——交互作用檢定,它直接問兩層的效果差得夠不夠多
交互作用 p 值是 0.522,離顯著很遠:這份資料沒有偵測到治療效果在兩性之間有差別。0.006 與 0.461 是兩層各自的 p 值,而「一層有星號、另一層沒有」不是交互作用的證據——那多半只反映兩層的人數不一樣,人多的那層檢定力較高。用層內 p 值去回答層間差異,是拿兩個各自的檢定去湊一個從來沒被做過的檢定。
這篇試驗檢定了八個次族群變項。其中六個出現「一層顯著、另一層不顯著」的樣子。這說明了什麼?
看答案與解析
正確答案: 說明「一邊有星號一邊沒有」在 6 個變項上出現,而這種景象幾乎必然會發生
八個變項裡有六個出現這種樣子,而交互作用檢定達到顯著的變項數是 0——六比零這個對比就是重點:只要各層人數不平均,「一邊有星號一邊沒有」幾乎是必然會出現的景象,它讀起來卻很像發現了什麼。8 是檢定過的變項總數。次族群森林圖之所以危險,正是因為它把這種必然的視覺樣式呈現得像是結果。
要把「兩性的治療效果差多少」量化成一個估計值,該用哪一個數字?
看答案與解析
正確答案: 1.48——兩層 OR 的比值,這才是「差多少」本身的估計
「差多少」的估計值是兩個 OR 的比值 1.48,也就是 ratio of odds ratios,它與交互作用檢定問的是同一件事。0.45 與 0.67 是兩層各自的 OR:任何一個單獨拿出來都是該層的效果,不是層間的差;而在比值尺度上要用除的不是減的。這個比值的信賴區間很寬——次族群分析真正的問題不是「有沒有差」,而是這份資料原本就沒有能力回答這個問題。
用到這個方法的章節
素材來源與授權
本頁為原創內容