Censoring 與存活資料的結構
為什麼「還沒發生事件」不是遺漏值、右/左/區間設限各自長什麼樣、風險集合怎麼隨時間縮小、非資訊性設限這個假設撐著整套方法,以及 immortal time bias 是怎麼把一個未偵測到效果的治療變成救命神藥。
這一頁在解決什麼問題
你收了 228 位晚期肺癌病人,追蹤到研究結束。有些人死了,你知道確切日期;有些人到最後一次回診都還活著;還有人搬到中南部、電話打不通了。現在要回答「這群人活多久」。
第一個直覺是算平均存活天數。但你馬上會卡住:那些還活著的人,存活天數該填多少?填最後一次回診的日子,等於假設他們那天就死了;空著不算,等於把所有活得久的人都刪掉。
這不是資料品質的問題,是這類資料本來的形狀。 一個追蹤了 400 天還沒死的人,他的資料不是遺漏值,而是一句明確的陳述:「這個人的存活時間 > 400 天。」存活分析整套工具存在的理由,就是把這種「只知道一邊界限」的觀察算進去,而不是丟掉。
我們用 survival::lung 這份資料當例子:228 人,其中 165 人在追蹤期間死亡、63 人(27.6%)到最後仍未觀察到事件。
figures/scripts/B3-01-censoring.R三種設限,長得不一樣
「設限」(censoring)在中文文獻裡也譯作「censor」「檢查」「設限」,指的是事件時間沒有被精確觀察到,只知道它落在某個範圍。臨床研究裡幾乎只會遇到第一種,但知道另外兩種存在,你才知道自己遇到的是哪一種。
| 類型 | 你知道的事 | 典型情境 |
|---|---|---|
| 右設限(right-censoring) | 事件時間 > 某個已知時點 | 研究結束時仍存活、失去追蹤、退出研究 |
| 左設限(left-censoring) | 事件時間 < 某個已知時點 | 第一次檢驗就已經陽性,不知道何時感染的 |
| 區間設限(interval-censoring) | 事件時間落在兩次觀察之間 | 每半年做一次影像,這次發現復發——只知道在前後兩次之間 |
另外要跟設限區分開的是截切(truncation)。設限是「這個人在資料裡,只是他的時間不完整」;截切是「這個人根本不會出現在資料裡」。例如只收「來到醫學中心的病人」的登錄資料,發病後立刻死在急診的人永遠不會被納入——這是左截切,資料裡看不到缺口,但族群已經被篩選過了。
左截切還有一個較溫和、也更常撞到的形態:人進得來,只是進得晚——健保或院內登錄的收案日晚於診斷日,或者以年齡當時間軸時每個人在自己的年齡處才進入風險集合。這種情況下 Surv(time, event) 不會報錯,只會把存活率系統性地高估。做法、代價與實際偏誤有多大,見 左截切與延遲進入。
風險集合:曲線的分母隨時間在縮小
存活分析的每一個估計,本質上都是在一連串時點上問同一件事:在這一刻還「有可能被觀察到發生事件」的人裡面,發生了幾件? 這群人叫做該時點的風險集合(risk set)。
風險集合只會縮小,理由有兩個:有人發生了事件(離開)、有人被設限(也離開)。在上面那張圖裡,日 300 那條虛線就是在示範——橫線有跨過虛線的人,才屬於第 300 天的風險集合。
survival::lung 的風險集合實際縮成這樣:
| 時點(天) | 仍在風險集合 | KM 估計的存活率 |
|---|---|---|
| 100 | 196 | 86.4% |
| 300 | 92 | 53.1% |
| 500 | 41 | 29.3% |
| 800 | 8 | 7.8% |
非資訊性設限:整套方法的地基
上面所有做法都靠同一個假設撐著:非資訊性設限(non-informative censoring,也叫獨立設限 independent censoring)。
白話說法是:一個人在時間 被設限,不能夠透露他在時間 的風險高低。 更精確一點,被設限的人在被設限那一刻的預後,應該與那一刻仍在追蹤、條件相同的人一樣。
這個假設在什麼時候成立、什麼時候破功:
| 設限原因 | 假設成立嗎 | 為什麼 |
|---|---|---|
| 研究結束、大家一起收工(行政設限) | ✅ 通常成立 | 收工日期與病人狀況無關 |
| 隨機的失聯(搬家、換醫院) | ✅ 大致成立 | 前提是搬家與病情無關 |
| 病情惡化所以退出試驗 | ❌ 破功 | 被設限的人風險比留下來的高,KM 會高估存活率 |
| 感覺好了所以不回診 | ❌ 破功 | 方向相反,KM 會低估存活率 |
| 換到另一種治療所以停止追蹤 | ❌ 破功 | 換藥的理由通常就是療效不好 |
中位追蹤時間:一個很多人算錯的數字
論文 Results 第一句常常是「中位追蹤時間 X 個月」。這個數字不是所有人觀察時間的中位數——因為死掉的人的觀察時間是他的存活時間,不是他被追蹤了多久。
正確做法是反轉 Kaplan-Meier(reverse KM):把事件指標對調,讓「還在被追蹤」變成事件,再取中位數。
在 survival::lung 裡兩個算法差很多:
- 直接取觀察時間的中位數:255.5 天(錯的)
- 反轉 KM 的中位追蹤時間:588 天(95% CI 511–1010)
差了兩倍以上。前者其實比較接近中位存活時間,拿它當追蹤時間會讓讀者以為追蹤期比實際短得多。
Immortal time bias:把未偵測到效果的治療變成神藥
這是臨床研究裡最常見、也最容易在自己的分析裡犯的設計錯誤。它的機制其實很單純:如果分組的依據要等到基線之後才知道,那麼「等到知道」的那一段時間,被分到某一組的人是不可能死的——不然他們不會被分到那組。
那一段時間叫做 immortal time(不死時間)。它被錯誤地計入了治療組的「存活時間」,於是治療組憑空多出一截保證活著的時間。
最乾淨的教學例子是史丹佛換心試驗(survival::jasa):103 位病人上了等待名單,其中 69 位最後接受了移植、34 位沒有。
figures/scripts/B3-01-censoring.R兩種分析法,同一份資料:
| 分析方式 | 移植的 HR | 95% CI | p |
|---|---|---|---|
| 天真做法:依「是否曾移植」在基線分組 | 0.27 | 0.17–0.43 | < 0.001 |
| 正確做法:移植當作時間相依變項 | 1.14 | 0.63–2.05 | 0.67 |
天真做法說移植讓死亡風險降到 26.6%,p 值小到不行;正確做法未偵測到差異(信賴區間跨過 1)。兩者的差別,核心機制就是那 2600 人天的等待時間(中位數 25 天)被算成了移植組的存活——這段時間裡他們依定義不可能死,卻被記在移植組的帳上。
動手跑一次
library(survival)
data(cancer, package = "survival") # lung 放在 cancer 這包資料裡
data(heart, package = "survival") # 同時載入 jasa 與 heart
# 存活資料的最小單位:Surv(時間, 事件指標)
# lung 的 status:1 = censored, 2 = dead
head(Surv(lung$time, lung$status)) # 有 "+" 號的就是設限
# 風險集合怎麼縮小:KM 物件的 n.risk 欄
fit <- survfit(Surv(time, status) ~ 1, data = lung)
summary(fit, times = c(100, 300, 500, 800))
# 中位追蹤時間:反轉事件指標,讓「還在追蹤」變成事件
survfit(Surv(time, 1 - (status == 2)) ~ 1, data = lung)
# ── immortal time bias ──
# 錯的:把「是否曾移植」當基線分組
jasa$tx <- factor(jasa$transplant, 0:1, c("No", "Yes"))
coxph(Surv(futime, fustat) ~ tx, data = jasa)
# 對的:heart 已經是 (start, stop] 長表,transplant 隨時間改變
head(heart[heart$id %in% c(2, 4), ])
coxph(Surv(start, stop, event) ~ transplant, data = heart)驗證環境:R 4.6.0 + survival 3.8.6
import statsmodels.api as sm
from lifelines import KaplanMeierFitter
from lifelines.utils import median_survival_times
# 注意命名陷阱:lung 在 Rdatasets 上掛在 "cancer" 這個 help page 底下
lung = sm.datasets.get_rdataset("cancer", "survival").data
lung["event"] = (lung["status"] == 2).astype(int)
kmf = KaplanMeierFitter().fit(lung["time"], lung["event"])
# 風險集合。不要用 event_table.loc[[100, ...]](那幾個時點不一定有事件,
# 會 KeyError),也不要用 .asof()(它給的是「最後一個 <= t 的觀測時點」,
# 差一格,四個數字有三個對不上上面那張表)。
for t in (100, 300, 500, 800):
print(t, int((lung["time"] >= t).sum()))
# 中位追蹤時間:把事件指標反過來
rev = KaplanMeierFitter().fit(lung["time"], 1 - lung["event"])
print("median follow-up:", rev.median_survival_time_)
# 時間相依變項(immortal time 的正解)在 lifelines 走 CoxTimeVaryingFitter,
# 輸入是 (start, stop] 長表,欄位名與 R 的 heart 一致
from lifelines import CoxTimeVaryingFitter
# ctv = CoxTimeVaryingFitter().fit(long_df, id_col="id",
# event_col="event", start_col="start", stop_col="stop")lifelines 的資料結構與 R 相同:一個 duration 欄、一個 0/1 event 欄。
怎麼讀報表
看到一篇存活分析的論文,這一頁對應的是 Methods 與 Results 開頭那幾句,重點看四件事:
- 時間零是哪一天,全部人都一樣嗎。 隨機分配日?診斷日?手術日?如果兩組的時間零定義不同,後面所有數字都不用看了。
- 中位追蹤時間有沒有給,而且是怎麼算的。 有些論文寫「中位追蹤 X 個月(IQR …)」但實際上是觀察時間的中位數;如果這個數字明顯小於中位存活時間,通常就是算錯了。
- 失去追蹤多少人、為什麼。 CONSORT/STROBE 流程圖要看得到。比例高又沒交代原因,非資訊性設限的假設就是懸空的。
- 分組依據是不是基線就知道。 只要看到「反應組 vs 無反應組」「完成療程 vs 未完成」「接受手術 vs 未接受」這種標題,先假設有 immortal time bias,再去 Methods 找他們有沒有用 landmark 或時間相依變項處理。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 把設限的人當作「沒發生事件」直接算比例 | 低估風險;他們可能在你沒看到的時候發生了事件 |
| 把設限的人整個排除 | 通常高估風險,剩下的都是已發生事件的人,且破壞了隨機化 |
| 用觀察時間的中位數當作中位追蹤時間 | 死亡者的觀察時間是存活時間不是追蹤時間;正解是反轉 KM |
| 依基線之後才知道的狀態分組(反應/完成療程/接受手術) | Immortal time bias,效果會被系統性地誇大 |
| 從診斷日開始計算追蹤,卻用「首次領藥」定義暴露組 | 同上,藥物流行病學最常見的版本 |
| 把定期檢查發現的日期直接當成精確事件時間,且不提檢查間隔 | 實際是區間設限,事件時間被系統性地往後推;兩組檢查頻率不同時還會製造組間差異 |
| 失去追蹤比例高卻不做敏感度分析 | 非資訊性設限假設無法檢定,只能靠敏感度分析畫出結論的可能範圍 |
| KM 圖不附風險人數表 | 讀者無法判斷曲線尾端是幾個人畫出來的 |
| 用「失去追蹤的人與留下來的人基線特徵相似」證明設限是非資訊性的 | 基線相似不代表設限那一刻的預後相似;這個假設關心的是後者 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B3-01-censoring.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
報表同時印了好幾個以天為單位的數字。要回答「這份研究到底看了多久」,該讀哪一個?
看答案與解析
正確答案: 588.0 天——把設限與事件互換之後算出來的追蹤中位數
追蹤中位數問的是「還在追蹤中的人被看了多久」,所以要把設限當成事件、事件當成設限再跑一次 KM,得到 588.0 天。255.5 天是觀察時間的中位數:一個追蹤第三天就失去聯絡的人,在這個算法裡只貢獻三天,於是研究看起來比實際短很多——一份追蹤其實不夠久的研究,會因此被誤讀成追蹤太短。而 511.0 天只是 588.0 那個估計的信賴區間下界,換一批人就會變,它不是任何人被追蹤的時間。
同一份心臟移植資料,把「有沒有接受移植」當成固定分組跑一次,當成時間相依變項再跑一次。哪一個結果可以拿來說移植的效果?
看答案與解析
正確答案: 風險比 1.14,來自時間相依——每個人在手術前算未移植、手術後才算移植
固定分組給的 0.27 看起來像移植讓死亡風險降到四分之一,但那段等待器官的時間裡病人依定義不會死,而它被算給了移植組——這就是 immortal time bias。0.43 是同一個有偏估計的信賴區間上界,一樣繼承了那個偏差。把同一個變項改成隨時間變動之後得到 1.14,它的 95% 信賴區間同時涵蓋可觀的益處與可觀的害處,未達統計顯著;這份資料既不足以支持移植改變死亡風險,也不足以排除。
有人看著報表上被設限的人數說「所以有這麼多人活下來了」。下面哪一個說法才站得住?
看答案與解析
正確答案: 被設限的是 63 人,而設限只代表從那個時間點之後我們不知道發生了什麼
被設限的意思只有一個:從這個時間點之後,我們不知道發生了什麼。63 人裡既有追蹤結束時仍然存活的人,也有中途失去聯絡、甚至可能已經過世卻沒被記錄的人,所以不能整批當成存活者。165 是確實觀察到的死亡數,但「其餘的都還活著」正是同一個誤讀換句話說;228 減去 165 得到的也只是被設限的人數,不是存活人數。設限比例高代表資訊少,不代表預後好。
用到這個方法的章節
延伸觀看
Censoring and Truncation [Survival Analysis 2/8]
Survival Analysis [Simply Explained]
存活分析(Survival Analysis)
【Lecture】L19 Survival Analysis (1)素材來源與授權
本頁為原創內容