進階已經雙重審閱,尚未人工抽查

比例風險假設與 Schoenfeld residuals

比例風險假設實際上在假設什麼、cox.zph 的那張表怎麼讀、Schoenfeld 殘差圖與 log-log plot 各自看什麼,以及假設被違反之後分層 Cox、時間分段、時間相依係數三條路各自付出什麼代價。

這個假設在假設什麼

Cox 模型的形式是「基線風險乘上一個倍數」:

h(tx)=h0(t)exp(βx)h(t \mid x) = h_0(t) \cdot \exp(\beta x)

注意 β\beta 沒有帶 tt。這代表那個倍數在整段追蹤期間都是同一個數:下面那個 lung 模型估出來的女性 HR 是 0.602,模型的意思是第 30 天、第 300 天、第 900 天,女性的死亡風險是男性的 0.602 倍。這就是比例風險假設(proportional hazards assumption)。

它不是一個可以「附帶檢查一下」的技術細節,而是那個 HR 有沒有意義的前提。假設不成立時,coxph() 還是會吐出一個數字,但那個數字是把不同時期不同大小(甚至方向相反)的效果,用一個跟事件分布有關的權重平均起來的產物。它不對應到任何一個時點的真實風險比,而且它的大小會隨著追蹤期長短而變——同一個治療追蹤三年跟追蹤十年會得到不同的 HR,即使真實情況完全一樣。

臨床上假設會破功的典型場景:

場景HR 隨時間怎麼變
手術 vs 保守治療早期手術風險高(術後併發症),晚期較低 → HR 先大於 1 再小於 1,曲線交叉
免疫治療 vs 化療前幾個月看不出差異,之後才拉開 → HR 先接近 1 再變小
體能狀態、年齡這類預後因子效果集中在早期,久了之後體弱的人已經死光,剩下的人差異變小 → HR 往 1 收斂
疫苗保護力隨時間衰退 → HR 往 1 收斂

怎麼檢查:Schoenfeld residuals

Schoenfeld 殘差(Schoenfeld residuals)的想法很直接:在每一個事件時點,模型會依各人的共變項算出「誰最可能是這次發生事件的人」的預測值;殘差就是實際發生事件那個人的共變項值,減掉模型在該時點的預測值

關鍵在於:如果比例風險成立,這些殘差與時間應該沒有關係——早期跟晚期的殘差都該在 0 附近隨機散布。如果殘差隨時間有系統性的趨勢,代表 β\beta 其實在隨時間變化。

實務上用的是尺度化 Schoenfeld 殘差(scaled Schoenfeld residuals),它有一個很好的性質:把它對時間作圖,那條平滑曲線就是 β^(t)\hat{\beta}(t) 的估計——直接看得到係數隨時間跑去哪裡。cox.zph() 做的正式檢定,就是在檢定這條線的斜率是不是 0。

動手跑一次

library(survival)
data(cancer, package = "survival")
lung$sex_f <- factor(lung$sex, levels = c(1, 2),
                     labels = c("Male", "Female"))

fit <- coxph(Surv(time, status) ~ age + sex_f + ph.karno + wt.loss,
             data = lung)

zph <- cox.zph(fit)
zph                      # 每個變項一列 + GLOBAL
par(mfrow = c(2, 2)); plot(zph)   # 平滑線就是 beta(t)

# log(-log) 圖:類別變項的圖形檢查,平行 = 假設沒問題
plot(survfit(Surv(time, status) ~ sex_f, data = lung),
     fun = "cloglog", col = c("#4d6a8c", "#c44d4d"), lwd = 2)

# 補救一:把時間切開,讓係數在前後兩段各自估
sp <- survSplit(Surv(time, status) ~ ., data = lung, cut = 180,
                episode = "period")
coxph(Surv(tstart, time, status) ~ age + sex_f + wt.loss +
        ph.karno:strata(period), data = sp)

# 補救二:讓係數是時間的函數 beta(t) = b0 + b1*log(t)
coxph(Surv(time, status) ~ age + sex_f + wt.loss + ph.karno + tt(ph.karno),
      data = lung, tt = function(x, t, ...) x * log(t))

# 補救三:分層——每一層有自己的 baseline hazard
lung$kstrata <- cut(lung$ph.karno, c(-Inf, 70, 80, Inf))
coxph(Surv(time, status) ~ age + sex_f + wt.loss + strata(kstrata), data = lung)

# 補救四(換掉效果量)的程式碼在 B3-07,那一頁是 RMST 的完整說明

驗證環境:R 4.6.0 + survival 3.8.6

cox.zph() 的表怎麼讀

survival::lung 的四變項模型跑出來——資料集 228 人,完整個案分析後進入模型的是 213 人、151 個事件

變項HR95% CIp
年齡,每增加一歲1.0160.996–1.0360.113
女性 vs 男性0.6020.428–0.8480.004
Karnofsky 分數,每增加一分0.9880.976–1.0000.048
體重減輕,每公斤0.9970.985–1.0100.657

cox.zph() 對這個模型的結果:

變項χ²dfp
年齡0.9310.335
性別3.0610.080
Karnofsky 分數7.3810.007
體重減輕0.0410.842
GLOBAL(整體檢定)10.1640.038

這裡的虛無假設是「比例風險成立」,所以 p 小才是壞消息。 Karnofsky 分數的 p = 0.007,明確違反;整體檢定(GLOBAL)p = 0.038,也達到顯著。性別的 p = 0.080 落在邊界,年齡與體重減輕沒有問題。

四個變項的尺度化 Schoenfeld 殘差對時間作圖,Karnofsky 分數的平滑線明顯由負往上走向零,性別的平滑線由負往零緩升、趨勢肉眼可見但未達顯著,年齡與體重變化兩條線大致水平。
四個變項的尺度化 Schoenfeld 殘差。紅色實線是 β(t) 的估計、虛線是其信賴帶、藍色點線是模型報出的那個單一係數。Karnofsky 那一格的紅線從負值往 0 爬升——係數在隨時間縮小,這就是違反的樣子。產圖腳本 figures/scripts/B3-04-ph-assumption.R

log-log plot:類別變項的圖形版本

另一個古典檢查是把 log(logS^(t))\log(-\log \hat S(t))logt\log t 作圖。它的邏輯是:如果兩組符合比例風險(風險比為 θ\theta),那麼

log(logS1(t))=log(logS0(t))+logθ\log(-\log S_1(t)) = \log(-\log S_0(t)) + \log \theta

也就是兩條線垂直相差一個常數——平行。曲線靠攏、發散或交叉,就是假設有問題。

兩張 log-log 圖,左圖依 Karnofsky 分數高低分兩組、兩條線在後期明顯靠攏;右圖依性別分組、兩條線大致平行。
左:依 Karnofsky 分數分成兩組,兩條線在後期靠攏——與 Schoenfeld 圖看到的「係數往 0 收斂」是同一件事的兩種畫法。右:依性別分組,大致平行。標題括號裡是 cox.zph 對該變項的 p 值。產圖腳本 figures/scripts/B3-04-ph-assumption.R

違反了怎麼辦

先問一個問題:這個違反是發生在你關心的變項,還是只是某個共變項?

如果只是共變項(例如你要估治療效果,違反的是年齡),那麼把它分層掉就行,代價很小。如果違反的是主要暴露或治療變項,那你就不能再報單一 HR 了——因為那個數字沒有清楚的意義。以下四條路各有代價:

路一:分層 Cox(stratified Cox)

把違反的變項從「共變項」改成「層」(strata):每一層有自己的基線風險 h0(t)h_0(t),不再要求層與層之間成比例。

把 Karnofsky 分成三層之後,cox.zph() 的 GLOBAL 從 p = 0.038 變成 p = 0.441——假設救回來了。性別的 HR 變成 0.572(0.405–0.809)。

代價:分層的那個變項不再有 HR。 你買回了假設,但失去了對它的估計。所以分層適合用在「這只是個需要控制的因素,我不在乎它的效果量」的變項上。

路二:把時間切開,各段各估一個 HR

survSplit() 在第 180 天把每個人的追蹤時間切成兩段,讓 Karnofsky 的係數在前後兩段各自估:

期間Karnofsky 每分的 HR95% CIp
第 0–180 天0.9660.947–0.9870.001
第 180 天之後0.9980.983–1.0130.756

故事變得清楚了:Karnofsky 分數的保護效果集中在前 180 天(每高一分 HR 0.966,區間不跨 1),180 天之後未偵測到關聯(HR 0.998,區間跨過 1)。這比原本那個單一的 HR 0.988 有資訊得多——原本那個數字其實是這兩段的加權平均。

代價:切點是你選的。 先看資料再挑一個讓 p 值好看的切點,就是在做 data dredging。切點應該由臨床理由決定(例如「術後 30 天」「療程結束」),而且要在論文裡講清楚是怎麼決定的。

路三:讓係數變成時間的函數

tt() 直接假設 β(t)=β0+β1logt\beta(t) = \beta_0 + \beta_1 \log t,把 β1\beta_1 估出來。這裡的 β1\beta_1 = 0.011(SE 0.006,p = 0.062)——正號代表係數隨時間往上跑,與 Schoenfeld 圖看到的方向一致。

代價:logt\log t 這個形式也是你假設的。 換成 ttt\sqrt t 會得到不同的結果。而且輸出變成兩個係數,臨床讀者不容易解讀。

路四:換一個不需要比例風險假設的效果量

前面三條路都還在修 Cox 模型。第四條路是換掉效果量本身:限制平均存活時間(restricted mean survival time, RMST)是 KM 曲線在 0 到 τ\tau 之間的面積——「在前 τ\tau 這段期間,平均活了多久」。它不需要任何比例假設,單位是「天」,而且不必要求兩組的風險比在整段追蹤裡維持固定。

RMST 現在不只是 PH 違反時的補救,在腫瘤與心衰竭試驗裡它已經是常規的次要分析。它的完整說明——面積怎麼來的、τ\tau 為什麼必須事先指定、換一個 τ\tau 結論會變多少、以及怎麼把它講成「平均多活幾個月」——在 限制平均存活時間(RMST)

怎麼讀報表

論文裡跟這一頁有關的訊息通常只有一句話,藏在 Statistical analysis 段落。要找的是三件事:

  1. 有沒有提到檢查過。 完全沒提,而主要結果是 HR,就是一個實質的方法學缺口。寫法通常是「The proportional hazards assumption was assessed using scaled Schoenfeld residuals」。
  2. 檢查結果怎麼處理。 「假設成立」很好;「假設違反,因此改採分層/時間分段/RMST」也很好;「假設違反但我們仍報告單一 HR」則要自己在心裡打折。
  3. KM 圖有沒有交叉。 這是不必看 Methods 就能做的檢查。兩條曲線在追蹤中段明確交叉、而且交叉之後持續分開,是比例風險不成立的強烈訊號——真實的風險比若固定在某個不等於 1 的值,兩條真實的存活曲線不會交叉。但要記得你看到的是估計出來的曲線:尾端只剩下少數人的時候,兩條 KM 曲線光靠抽樣波動就可能互相穿過,這種交叉不構成違反的證據。所以要對照風險人數表看交叉發生在哪一段、幅度多大,再回到 Methods 找 Schoenfeld 殘差檢定或時間交互作用的結果來確認。確認之後,那個單一 HR 就是兩段相反效果的平均,log-rank 的檢定力也會被削弱。

常見誤用

誤用為什麼錯
跑完 Cox 完全不檢查比例風險假設假設不成立時那個 HR 沒有對應到任何時點的真實風險比
cox.zph() p 值不顯著就宣稱「假設成立」檢定力有限,尤其樣本數小時;要配合殘差圖判斷偏離的形狀與大小
大型資料庫裡 p 值一顯著就大改模型樣本數夠大時臨床上可忽略的偏離也會顯著,應看 β(t) 的變化幅度
曲線交叉還是報單一 HR那個數字是兩段方向相反的效果的加權平均,沒有清楚的解釋
對連續變項畫 log-log plot必須先切組,切點是自選的,圖形跟著切點變
假設違反就改用 log-ranklog-rank 對同一個問題同樣敏感,交叉時檢定力更差
把追蹤期截短到假設成立為止用資料決定分析範圍,且可能剛好丟掉治療效果真正出現的時段
看完資料才挑時間切點事後選切點是 data dredging,切點應由臨床理由事先決定
用 RMST 但事後才決定 ττ 必須事先指定並寫進計畫書,否則等同挑選有利結果,見 RMST
把分層變項的 HR 硬報出來分層 Cox 不估計分層變項的效果,那個數字不存在

重跑本頁的所有數字

/opt/homebrew/bin/Rscript figures/scripts/B3-04-ph-assumption.R

讀讀看這張圖

答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。

cox.zph 的輸出每一列對應一個共變項,最後一列是 GLOBAL。要判斷 Karnofsky 這一項有沒有違反比例風險,該讀哪一個 p 值?

看答案與解析

正確答案: 0.007——Karnofsky 自己那一列,檢定的就是它的效果隨不隨時間變動

每一項要各看各的那一列,Karnofsky 是 0.007。0.080 是女性相對於男性那一列。GLOBAL 的 0.038 最容易被誤用:它顯著只說明「模型裡至少有一項的效果隨時間變動」,不指出是哪一項;而它不顯著也不保證每一項都沒問題,因為那是把各項合起來檢定,一個強烈違反可以被幾個乾淨的項稀釋掉。把 GLOBAL 的 p 值當成某一個共變項的 p 值,是這張表最常見的誤讀。

把追蹤時間切在第 180 天前後分開估計之後,Karnofsky 在兩段的風險比不一樣。那麼不分段時報出來的那一個風險比,代表什麼?

看答案與解析

正確答案: 0.988——它是兩段效果的一種加權平均,不等於任何一段

比例風險不成立時,單一的風險比不會突然變成錯誤的數字,但它不再對應任何一個時期的真實效果——它是各時期效果以事件數為權重的平均,而權重取決於這份資料的追蹤長度與事件分布,換一份追蹤更久的資料就會給出不同的值。0.966 是 Day 0 到 180,0.998 是 Day 180 之後。麻煩的地方在於,被寫進摘要、被拿去比較的正是 0.988 那一個。

同一份資料、同一個臨床問題,把 Karnofsky 換成 ECOG 之後 cox.zph 的 GLOBAL 就不顯著了。這代表什麼?

看答案與解析

正確答案: GLOBAL 變成 0.314——比例風險是否成立取決於模型怎麼設定,不是資料的固有性質

換掉的只是體能狀態的編碼方式,臨床問題完全一樣,而比例風險檢定給出相反的結論:0.314 對上原本的 0.038。所以「有沒有違反 PH」問的其實是「你這樣寫的模型有沒有違反」。0.201 是新模型裡 ECOG 自己那一列,不是 GLOBAL。這也是為什麼不該把 cox.zph 當成一個過或不過的關卡——它會隨著設定轉向,而真正該問的是效果隨時間變動到什麼程度、那個變動對結論重不重要。

延伸觀看

The Cox proportional hazards model explained
ENTileStats· 14 min把「比例」這個假設用圖一步步拆開,是本頁第一節最好的視覺補充。
Survival Analysis Part 9 | Cox Proportional Hazards Model
ENMarinStatsLectures· 14 min從模型推導講到假設的位置,看完會知道這個假設不是外加的檢查項目,而是模型定義的一部分。
Cox Proportional Hazard Models
ENEpidemiology Stuff· 10 min流病視角,重點放在假設違反時結論會怎麼歪,與本頁最後兩節搭配。
【Lecture】L20 Survival Analysis (2)
繁中MeDA(臺大公衛洪弘教授)· 51 min繁中唯一講到假設診斷與殘差的完整課程,想補數學細節看這一集。

素材來源與授權

本頁為原創內容

回報內容問題

這個站的統計內容由 AI 撰寫、AI 互審,人工只做抽查。你看得出來的錯,我們不一定看得出來。

寫得越具體越修得動,例如哪一句話跟哪本教科書/哪篇論文的說法不一致。

留了才回得了信;不留也會看。

一併送出的資訊

這些是自動帶上的,每一項都可以取消。