加權 log-rank、max-combo 與 milestone 存活率
曲線交叉或延遲效果時,log-rank 把每個事件時間等權加總,前後兩段的訊號會互相抵消。這一頁講 Fleming-Harrington 的權重長什麼形狀、免疫治療為什麼需要偏重晚期的權重、max-combo 把「我挑過三個」算進去要付多少代價,以及 milestone 存活率差的兩種信賴區間為什麼差那麼多。真實資料裡看不到「換個檢定就翻盤」,這件事本身也寫在頁面上。
log-rank 把每一個事件時間看得一樣重
Kaplan-Meier 與 log-rank 那一頁把 log-rank 拆開過: 它在每一個事件時間點上算一次「觀察到的死亡數減掉虛無假設下的期望死亡數」, 把這些差加起來,再除以標準誤。寫成式子是這樣:
標準的 log-rank 就是把所有 都設成 1。這是一個選擇,不是一條定律, 而且它只有在風險比整段追蹤都固定時才是最有檢定力的那個選擇。
如果兩組的曲線交叉——前期一組較差、後期反超——那麼早期的 是正的、 晚期是負的,等權加起來會互相抵消,統計量趨近於零。 這不是「兩組真的一樣」,是這個加總方式看不見它。 免疫治療試驗的「前六個月兩條 PFS 曲線幾乎重疊、之後才分開」是 2019 年後的常態, 而那正好是 最不擅長的形狀。
加權 log-rank 做的事只有一件:把 換掉。 其他一切不變。
這一頁的例子:一份比例風險明確不成立的資料
survival::veteran 是美國退伍軍人管理局的肺癌試驗,137 位病人、128 個死亡、只有 9 位設限。
選它不是因為它會給出戲劇性的結果,而是因為它的比例風險假設確實不成立:
| 項目 | χ² | df | p |
|---|---|---|---|
| 治療組別 | 0.21 | 1 | 0.650 |
| Karnofsky 體能分數 | 12.81 | 1 | 3.4e-4 |
| 腫瘤細胞型別 | 14.53 | 3 | 0.002 |
| 整體(GLOBAL) | 22.86 | 5 | 3.6e-4 |
模型是 coxph(Surv(time, status) ~ trt + karno + celltype),檢查用的是 cox.zph() 那一頁的做法。
整體的 p 是 3.6e-4,違反是明確的。
權重長什麼形狀
Fleming-Harrington 這一族用兩個參數 與 決定權重:
是合併兩組之後的 Kaplan-Meier 估計。三個常見的角落:
- :權重恆為 1,就是標準 log-rank。
- :權重等於 ,隨時間遞減,偏重早期(也就是 Peto / Gehan 那一系的精神)。
- :權重等於 ,隨時間遞增,偏重晚期。
抽象的話講到這裡就夠了,直接看數字。下表是這份資料的事件時間分位點上, 合併 KM 估計值與三種權重各是多少:
| 事件時間分位 | t(天) | Ŝ(t⁻) | ρ = 0 | ρ = 1 | FH(0, 1) |
|---|---|---|---|---|---|
| 10% | 10.0 | 0.898 | 1.000 | 0.898 | 0.102 |
| 30% | 29.1 | 0.715 | 1.000 | 0.715 | 0.285 |
| 50% | 62.0 | 0.531 | 1.000 | 0.531 | 0.469 |
| 70% | 125.6 | 0.338 | 1.000 | 0.338 | 0.662 |
| 90% | 295.1 | 0.117 | 1.000 | 0.117 | 0.883 |
看最後一列。到了第 295 天(最晚的 10% 事件所在的位置), 給的權重只剩 0.117——而第 10 天的權重是 0.898。同一份資料,晚期一個死亡對統計量的貢獻被壓成早期的 13%,等於幾乎不看後期。 FH(0, 1) 那一欄剛好相反:同一個時點的權重是 0.883, 而在第 10 天只有 0.102。
figures/scripts/B3-09-weighted-logrank.R延遲效果為什麼需要 FH(0, 1)
免疫治療的典型形狀是這樣:前幾個月兩條曲線黏在一起(藥還沒發揮作用, 而且一部分病人根本不會反應),之後治療組的曲線平坦下來、對照組繼續往下掉。 真正的訊號全部在後段。
此時 的代價是:前段那一大堆「沒有差異」的事件時間, 每一個都以同樣的重量被算進 ,把後段的差異稀釋掉。上表已經說明了問題有多大—— 事件時間的分佈本來就前密後疏,所以「等權」在實務上其實是偏重早期。 FH(0, 1) 只是把這個隱性偏誤反過來校正回去。
換了權重,結論沒有翻盤——而這正是要教的事
先看四組細胞型別的比較(4 組,df = 3):
| 權重 | χ² | df | p | α = 0.05 下 | 來源 |
|---|---|---|---|---|---|
| ρ = 0:標準 log-rank | 25.40 | 3 | 1.3e-5 | 達顯著 | survdiff |
| ρ = 0.5:略偏重早期 | 22.71 | 3 | 4.6e-5 | 達顯著 | survdiff |
| ρ = 1:偏重早期 | 19.71 | 3 | 1.9e-4 | 達顯著 | survdiff |
| FH(0, 1):偏重晚期 | 25.79 | 3 | 1.1e-5 | 達顯著 | hand-computed |
四組的人數與事件數是 squamous(鱗狀細胞癌)35 人、31 個死亡;smallcell(小細胞癌)48 人、45 個死亡;adeno(腺癌)27 人、26 個死亡;large(大細胞癌)27 人、26 個死亡。 再看兩組治療的比較:
| 權重 | χ² | df | p | α = 0.05 下 | 來源 |
|---|---|---|---|---|---|
| ρ = 0:標準 log-rank | 0.01 | 1 | 0.928 | 未達顯著 | survdiff |
| ρ = 0.5:略偏重早期 | 0.47 | 1 | 0.491 | 未達顯著 | survdiff |
| ρ = 1:偏重早期 | 0.87 | 1 | 0.351 | 未達顯著 | survdiff |
| FH(0, 1):偏重晚期 | 0.81 | 1 | 0.369 | 未達顯著 | hand-computed |
細胞型別的比較在四種權重下都達到顯著,結論沒有改變;
治療組別的比較在四種權重下都未達顯著,結論同樣沒有改變。
機器可讀的版本是 anyConclusionChanged 這個欄位,兩個對比都是
false 與 false。
max-combo:多重性的代價是一個具體數字
如果事先不確定訊號會出現在早期還是晚期,一個誠實的做法是同時跑幾個權重, 取其中最極端的那個統計量,然後把「我挑過幾個」算進 p 值裡。這就是 max-combo。
本頁用的三個成分是 rho0、rho1、fh01,
各自的 z 值(以標準治療組的觀察減期望值為準,負號代表該組死亡數少於期望)是:
| 成分 | z | 方向 |
|---|---|---|
| ρ = 0:標準 log-rank | -0.091 | 標準治療組死亡少於期望 |
| ρ = 1:偏重早期 | -0.933 | 標準治療組死亡少於期望 |
| FH(0, 1):偏重晚期 | 0.898 | 標準治療組死亡多於期望 |
這張表把 log-rank 為什麼看不見東西講完了。 偏重早期的 z 是 -0.933、偏重晚期的 z 是 0.898, 兩者符號相反——早期與晚期指向不同的方向,正是下一節那張 milestone 圖裡曲線交叉的形狀。 而標準 log-rank 把這兩段等權加起來,得到的 z 是 -0.091, 幾乎就是零,對應到 p = 0.928。
三個統計量彼此高度相關,因為它們是在同一批事件時間上的加權和:
| 相關 | rho0 | rho1 | fh01 |
|---|---|---|---|
rho0 | 1.000 | 0.891 | 0.855 |
rho1 | 0.891 | 1.000 | 0.526 |
fh01 | 0.855 | 0.526 | 1.000 |
有了相關矩陣就可以積分三元常態,把「取最大值」這個動作的代價算出來:
| 報什麼 | p | 可以寫進論文嗎 |
|---|---|---|
| 事先指定的標準 log-rank | 0.928 | 可以 |
| 三個權重裡最好看的那一個 | 0.351 | 不可以——它是挑出來的 |
| max-combo(把挑選算進去) | 0.549 | 可以,但要事先宣告成分 |
多重性的代價在這裡不是一句「記得校正」,是一個看得到的數字: 從 0.351 變成 0.549。 順帶一提,同樣的三個檢定用 Bonferroni 會校正到 1.052 (實務上截在 1),比 max-combo 保守得多——因為 Bonferroni 假設三個檢定互相獨立, 而上面那張相關矩陣說它們一點也不獨立。 相關性愈高,該付的多重性代價愈小,這一點與 多重比較 那一頁講的是同一件事。
milestone 存活率:第幾天的存活率差幾個百分點
Milestone survival 是最不需要統計訓練就能讀懂的那個寫法: 「一年存活率 X% 對 Y%,差 Z 個百分點」,而且它完全不依賴任何比例假設—— 它只讀 KM 曲線在某一個垂直切面上的高度。
figures/scripts/B3-09-weighted-logrank.R三個時點的數字(差值方向是標準治療組減試驗治療組):
| 時點(天) | 標準治療組 | 試驗治療組 | 差值(百分點) | 95% CI(百分點) |
|---|---|---|---|---|
| 90 | 54.7%(尚在追蹤 37) | 38.0%(尚在追蹤 25) | 16.7 | 0.1 到 33.2 |
| 180 | 21.2%(尚在追蹤 13) | 23.3%(尚在追蹤 14) | -2.0 | -16.5 到 12.4 |
| 365 | 7.1%(尚在追蹤 4) | 11.0%(尚在追蹤 6) | -3.9 | -14.2 到 6.5 |
注意差值的符號翻過去了:第 90 天是 16.7 個百分點、 第 180 天是 -2.0、第 365 天是 -3.9。 這正是 log-rank 的 p = 0.928 掩蓋掉的東西—— 一個把整段時間壓成一個數字的統計量,沒有辦法告訴你符號在中途換過邊。
順帶注意第 90 天那一列:差值的 95% CI 是 0.1 到 33.2 個百分點, 下界只差一點就碰到零。這種剛好落在邊緣的區間最需要問一句「這個時點是事先決定的嗎」—— 如果它是看過圖之後從三個裡挑出來的,就不能照字面讀,理由與上一節的 max-combo 完全相同。
兩種區間轉換給的答案不一樣
存活率的信賴區間有兩種常見算法,差別在先轉換再算區間、還是直接在機率尺度上算。 把第 365 天的兩組攤開來看:
| 組別 | 存活率 | Greenwood(直接在機率尺度) | log-log 轉換 |
|---|---|---|---|
| 標準治療組 | 0.071 | 0.005 到 0.137 | 0.023 到 0.155 |
| 試驗治療組 | 0.110 | 0.030 到 0.190 | 0.046 到 0.204 |
點估計是同一個 0.071,但標準治療組的下界 一個是 0.005、另一個是 0.023, 差了 4.7 倍。 原因是 Greenwood 的區間在機率尺度上是對稱的:點估計已經很接近 0 時, 下界會被推到 0 附近,再低一點就會壓到 0 以下——而存活率不可能是負的。 log-log 轉換先把估計值送到 這個沒有邊界的尺度上算區間、再轉回來, 所以區間永遠落在 0 與 1 之間,而且不對稱。腫瘤 RCT 慣用 log-log 就是為了這個。
自己跑一遍
library(survival)
data(cancer, package = "survival")
vet <- veteran
vet$trt_f <- factor(vet$trt, levels = c(1, 2), labels = c("Standard", "Test"))
# --- FH(rho, 0):survdiff 只接受 rho,權重就是 S(t-)^rho -------------------
survdiff(Surv(time, status) ~ trt_f, data = vet, rho = 0) # 標準 log-rank
survdiff(Surv(time, status) ~ trt_f, data = vet, rho = 1) # 偏重早期
# --- FH(0, 1):survdiff 做不到,要自己組風險集合表 -------------------------
et <- sort(unique(vet$time[vet$status == 1]))
nj <- sapply(et, function(t) sum(vet$time >= t)) # 風險集合大小
dj <- sapply(et, function(t) sum(vet$time == t & vet$status == 1))
Sminus <- c(1, cumprod(1 - dj / nj))[seq_along(et)] # 左連續!
w_late <- 1 - Sminus # FH(0, 1) 的權重
# U = sum(w * (O - E)),V 依權重平方加權;完整實作與健全性檢查見腳本
# figures/scripts/B3-09-weighted-logrank.R
# --- milestone 存活率與兩種區間轉換 ---------------------------------------
summary(survfit(Surv(time, status) ~ trt_f, data = vet, conf.type = "plain"),
times = c(90, 180, 365)) # Greenwood,直接在機率尺度
summary(survfit(Surv(time, status) ~ trt_f, data = vet, conf.type = "log-log"),
times = c(90, 180, 365)) # log-log 轉換驗證環境:R 4.6.0 + survival 3.8.6;max-combo 的三元常態積分另外用 mvtnorm 1.4.2。survdiff() 只吃 rho,做不了 FH(0, 1)。
import pandas as pd
from lifelines import KaplanMeierFitter
from lifelines.statistics import (
logrank_test, survival_difference_at_fixed_point_in_time_test)
# statsmodels 的 get_rdataset 查不到它(Rdatasets 把它登記在別的 bundle 底下),
# 所以直接讀 CSV。
RD = "https://vincentarelbundock.github.io/Rdatasets/csv/"
vet = pd.read_csv(RD + "survival/veteran.csv")
a, b = vet[vet.trt == 1], vet[vet.trt == 2]
# weightings="fleming-harrington" 需要同時給 p 與 q(就是 rho 與 gamma)
logrank_test(a.time, b.time, a.status, b.status) # rho = 0
logrank_test(a.time, b.time, a.status, b.status,
weightings="fleming-harrington", p=1, q=0) # rho = 1
logrank_test(a.time, b.time, a.status, b.status,
weightings="fleming-harrington", p=0, q=1) # FH(0, 1)
# milestone:先各配一條 KM,再比同一個時點
ka = KaplanMeierFitter().fit(a.time, a.status, label="Standard")
kb = KaplanMeierFitter().fit(b.time, b.status, label="Test")
ka.predict(90), kb.predict(90)
survival_difference_at_fixed_point_in_time_test(90, ka, kb)lifelines 的 fleming-harrington 權重用的也是左連續的 KM 估計,三個檢定統計量與上面的 R 逐位相符(本頁實跑對照過)。max-combo 沒有現成實作,相關矩陣要自己算。注意 survival_difference_at_fixed_point_in_time_test() 用的是 log(-log) 轉換,與本頁表格裡直接對差值做的 Wald 區間不是同一件事。
這一頁與其他頁的關係
- log-rank 本體、KM 曲線怎麼讀——見 Kaplan-Meier 曲線與 log-rank 檢定。 那一頁講的是 的版本,這一頁只換掉 。
- 怎麼知道比例風險假設不成立、還有哪些補救路——見
比例風險假設:分層、時間分段、
tt()讓係數隨時間變。 那一頁也說明了為什麼「把追蹤期截短到假設成立為止」不是解法。 - 想要一個有單位、可以對病人說的效果量——見 限制平均存活時間(RMST)。 加權 log-rank 給的仍然只是一個 p 值;它沒有效果量,這一點與標準 log-rank 完全一樣。 非比例風險的情境下,「差幾個月」或「第幾年差幾個百分點」比任何 p 值都好用。
- 挑權重、挑時點的多重性怎麼算——見 多重比較與型一錯誤。
- 臨床試驗的統計章節整體怎麼讀——見 隨機對照試驗。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 標準 log-rank 沒過,再試幾個權重直到某個過了 | 這是沒有校正的多重檢定;權重必須事先指定,或改用 max-combo |
| 報 max-combo 的成分是事後決定的 | 校正的是「取最大值」這個動作,不是「換一組成分再算一次」 |
| 把加權 log-rank 的 p 當成效果量的證據 | 它跟 log-rank 一樣沒有效果量;要效果量請用 RMST 或 milestone 差值 |
| 曲線交叉還是報單一 HR | 那個 HR 是兩段相反效果的加權平均,沒有清楚的解釋 |
| milestone 時點事後才決定 | 與事後挑 τ、事後挑權重是同一種選擇性呈現 |
| 報 milestone 存活率不報該時點的風險人數 | 讀者無法判斷那個百分比是幾個人撐起來的 |
| 存活率接近 0 或 1 時用 Greenwood 區間 | 機率尺度上的對稱區間會越界;改用 log-log 轉換 |
| 兩種區間都算一遍,報比較窄的那個 | 轉換方式要事先決定,事後挑等於挑結果 |
| 用 Ŝ(t) 而不是 Ŝ(t⁻) 當權重 | 慣例是左連續;用錯不會報錯,統計量會系統性偏掉 |
| 加權檢定未達顯著就說「兩組效果相同」 | 只能說未偵測到差異;要主張相當需要非劣性設計與事先設定的 margin |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B3-09-weighted-logrank.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
veteran 資料的治療分組,四種加權 log-rank 給出不同的 p 值,四個都不顯著。標準 log-rank 的 p 值是哪一個,它與另外幾個差在哪?
看答案與解析
正確答案: 0.928——標準 log-rank,對每一個事件時間一視同仁地加權
標準 log-rank 的定義就是不加權:每一個事件時間的貢獻同等看待,得到 0.928。0.491 與 0.351 是把權重往後期偏移的兩個版本——它們對「兩條曲線晚期才分開」比較敏感,代價是對早期的差別比較遲鈍。同一個對比、同一份資料,p 值從 0.928 一路走到 0.351:這裡三個都不顯著,所以結論沒有變;但只要其中一個掉到臨界值以下,而論文只報那一個,讀者就無從得知另外兩個長什麼樣子。
max-combo 檢定把數個加權統計量合起來。它的 p 值比其中最小的那個大,這個差額代表什麼?
看答案與解析
正確答案: 0.549——它是校正了「試過好幾種權重」之後的 p 值
0.549 是 max-combo 校正之後的 p 值,0.351 是那幾個檢定裡最小的一個。直接拿最小的當結論就是事後挑選:試了四種權重再挑最好看的一種,型一誤差已經不是名目上的那個水準了。兩者的差額正是這個多重性的代價,所以 max-combo 是一個誠實面對「試了好幾種權重」的工具,不是一個把結果變好看的工具。0.928 是標準 log-rank,max-combo 沒有理由回到它。
veteran 的 cox.zph 報表裡 GLOBAL 是顯著的。要知道是哪幾項違反了比例風險,該怎麼讀?
看答案與解析
正確答案: 逐列看,karno 的卡方是 12.81,它與另一項都違反,治療分組沒有
GLOBAL 只告訴你「模型裡有東西違反」,要知道是誰就得回到各項那幾列:karno 的卡方是 12.81,celltype 是 14.53,兩項都違反;治療分組的 0.21 沒有。只挑最大的那一項會漏掉另一個同樣違反的項,而最小的那個既不是整體也不是任何一種摘要。這一頁之所以要談加權 log-rank,正是因為比例風險在這份資料上站不住——標準 log-rank 在那種情況下不是錯,只是它把所有時間點一視同仁地加權。
用到這個方法的章節
素材來源與授權
本頁為原創內容