初探存活分析(2)

來看看 life table 跟 Kaplan-Meier 的結構與差異

Survial Analysis
R
作者

紙魚

發佈於

2025年10月20日

摘要
本文藉由 AI 生成的程式碼觀察存活分析的兩大無母數方法 life table 跟 Kaplan-Meier 的計算方式。

上一篇文章中,藉由比較存活分析與時間序列之間的差異,了解存活分析關心的課題。這次,終於要進入估計的部分了。

一般的教科書或是存活分析的課程,大多會從有母數方法(Parametric methods)開始教起,也就是從介紹存活分析常見的機率分布開始,推導它們的存活函數或危險函數。因為它們的模型可解釋性強,而且不需要大量樣本,只要能滿足它們的統計假設,實務上的表現會很優異。

不過本次文章略過它,想要從不需統計假設的無母數方法(Nonparametric method)開始,雖然教授的順序比有母數還要後面,但其實有些無母數方法由來已久。本次介紹的生命量表(life table)最早可以追溯到17世紀1,使用的技術也不複雜,會數數就可以了。另一個方法 Kaplan-Meier 就比較晚,20 世紀上半才由 Kaplan 提出。

生成資料

我們模擬的資料來自指數分配 \(\text{EXP} (0.1)\) 的資料。指數分配簡單介紹如下:

如果事件時間 \(T \sim \text{Exponential}(\lambda)\),其中\(\lambda\)為每單位時間發生該事件的次數,那麼:

  • 機率密度函數: \(f(t) = \lambda e^{-\lambda t}\)
  • 存活函數: \(S(t) = P(T > t) = e^{-\lambda t}\)
  • hazard function: \(h(t) = \lambda\)

這次我們鎖定 存活函數,將\(\lambda\)設為0.1/年,樣本設定為 100 生成資料:

set.seed(1005)

# 模擬參數
n <- 100           # 模擬個體數量
lambda <- 0.1      # 平均存活時間 = 1/lambda = 10

# 模擬真實存活時間 (from exponential)
true_surv_time <- rexp(n, rate = lambda)

# 模擬隨機截尾時間(假設最大觀察時間是 12)
censor_time <- runif(n, min = 5, max = 12)

# 觀察到的時間和事件狀態
obs_time <- pmin(true_surv_time, censor_time)
status <- ifelse(true_surv_time <= censor_time, 1, 0)

# 整理成資料表
data <- data.frame(
  id = 1:n,
  time = round(obs_time, 2),
  status = status,
  true_surv = true_surv_time
)
 

head(data)
  id  time status  true_surv
1  1  9.88      0 16.0519022
2  2  1.57      1  1.5700031
3  3 10.50      0 14.5478572
4  4  0.93      1  0.9324826
5  5  1.20      1  1.1950362
6  6  2.60      1  2.6003750

來看看資料的長相:

  Statistic Obs_Time  True_Surv
1      Min.   0.2400  0.2364795
2   1st Qu.   2.2725  2.2699674
3    Median   5.1300  5.8076985
4      Mean   5.4698 10.6284341
5   3rd Qu.   8.0175 14.2577409
6      Max.  11.9300 69.9463918

Life table

Life table 的先決條件是要為時間切出一樣等長的單位(\(t_1, t_2, , \dots ,t_j\)),用這些做出時間間隔(區間)。再來是在每個個時間區間裡的位於時間段落裡的人數、死亡人數、存活人數,然後就可以推算出存活函數,具體來說,Life table 關心的統計圖是上次提到,原始資料按實際時間排列的圖:

也因此符號的定義會跟上次提供的有點不同,這裡列出 :

  • \(t_i\): 為時間點,區間則為 \(I_i = [t_{i-1},t_i)\)

  • \(z_i\): 第 \(i\) 個區間長度

  • \(n_i\): 第 \(i\) 個區間內可追蹤的樣本數,等於下面的\(d_i\)\(l_i\)的總和

  • \(d_i\): 第 \(i\) 個區間內死亡的樣本數

  • \(l_i\): 第 \(i\) 個區間內 censer (活著離開研究)的樣本數

這裡有時會再細分,例如用 \(l_i\)表示中途離開研究,不知後續生死的樣本數,\(w_i\)表示第 \(i\) 個區間內確定活著離開研究的樣本數。

  • \(w_i=n_i-l_i*0.5\): 第 \(i\) 個區間具有死亡風險的的平均個體數,概念是:因為不確定將活著離開的人未來是否會在符合研究條件下去世,因此假設 censer 時間服從均勻分配,讓活著離開的人加權值為0.5扣除。化簡後得到\(d_i+l_i*0.5\)

其中,\(w_i\)的計算顯然是估計出來的,這也是會導致後續估計存活函數時會有偏差的原因,繼續看下去就知道了。

然後我們就可以求出:

  • \(d_{t_j}\): 在時間點 \(t_j\) 時的死亡人數,即\(P(T \in I_i|T>I_{i-1})\)

  • \(n_{t_j}\): 在 \(t_j\) 時的還活著的人數,即\(P\)

  • \(\hat q_j = \frac{d_j}{n_j}\): 至特定時間點\(t_j\)的死亡率。

  • \(\hat p_j= 1- \hat q_j\): 至特定時間點\(t_j\)的存活率 (\(\to\)這是我們的目標之一

為什麼 \(\hat p_j\) 是我們的目標 ? 這是因為存活函數\(S(t)\)可以拆解成:

\(\begin{array}{lll} S(t) & = & P(T>t_i) \\ & = & P(T>t_1)P(T>t_2|T>t_1)\dots P(T>t_j|T>t_{j-1}) & = \prod _{i=1}^j p_i \end{array}\)

如此一來,我們會需要每個時間點的 \(p_j\) ,才能湊出 \(S(t)\),而估計出來的 \(\hat p_j\) 因為是至某個時間點產生的東西,所以隨之估計出來的 \(\hat S(t)\) 會是一個階梯形狀的函數。

# 分區間(每年)
breaks <- seq(0, 13, by = 1)
data$interval <- cut(data$time, breaks = breaks, right = FALSE, labels = FALSE)

# 初始化生命表資料框
life_table <- data.frame(
  interval = paste0(breaks[-length(breaks)], "-", breaks[-1]),
  n = integer(length(breaks) - 1),
  deaths = integer(length(breaks) - 1),
  censored = integer(length(breaks) - 1),
  w = numeric(length(breaks) - 1),
  q = numeric(length(breaks) - 1),
  p = numeric(length(breaks) - 1),
  lx = numeric(length(breaks) - 1)
)

# 初始存活人數(lx)設為 1,代表比例生命表
lx <- 1
censored <- n

for (i in 1:(length(breaks)-1)) {
  in_interval <- data$interval == i
  deaths <- sum(data$status[in_interval] == 1, na.rm = TRUE)
  
  if(sum(data$status[in_interval] == 0, na.rm = TRUE) != 0){
  censored <- sum(data$status[in_interval] == 0, na.rm = TRUE)
  }
  w <- deaths + 0.5 * censored
  
  # q <- ifelse(w > 0, deaths / w, 0) # 死亡率(單點)
  q <-  deaths/w
  p <- 1 - q # 存活率(單點)
  
  # if(round(p,1) == 0){
  #   lx <- 1
  # }else {
  #    lx <- lx*p
  # }
  lx <- lx * p


  life_table$n[i] <- sum(in_interval, na.rm = TRUE)
  life_table$deaths[i] <- deaths
  life_table$censored[i] <- censored
  life_table$w[i] <- round(w, 2)
  life_table$q[i] <- round(q, 4)
  life_table$p[i] <- round(p, 4)
  life_table$lx[i] <- round(lx, 4)
  
  # lx <- lx * p  # 更新下一期的 lx
}
life_table
   interval  n deaths censored    w      q      p     lx
1       0-1  8      8       10 13.0 0.6154 0.3846 0.3846
2       1-2 14     14       10 19.0 0.7368 0.2632 0.1012
3       2-3  9      9       10 14.0 0.6429 0.3571 0.0361
4       3-4  8      8       10 13.0 0.6154 0.3846 0.0139
5       4-5  8      8       10 13.0 0.6154 0.3846 0.0053
6       5-6 12      5        7  8.5 0.5882 0.4118 0.0022
7       6-7  5      2        3  3.5 0.5714 0.4286 0.0009
8       7-8  9      3        6  6.0 0.5000 0.5000 0.0005
9       8-9  6      2        4  4.0 0.5000 0.5000 0.0002
10     9-10  7      0        7  3.5 0.0000 1.0000 0.0002
11    10-11  7      0        7  3.5 0.0000 1.0000 0.0002
12    11-12  7      0        7  3.5 0.0000 1.0000 0.0002
13    12-13  0      0        7  3.5 0.0000 1.0000 0.0002

欄位解說

欄位名稱 中文名稱 簡單說明
interval 區間(年份) 觀察時間的分段,例如 0–1 年、1–2 年…等。
n 區間樣本數 落在該時間區間的個體數量(包含死亡與審查)。
deaths 死亡人數 在該區間內發生事件(如死亡)的個體數。
censored 審查人數 在該區間內未發生事件,但中途退出觀察的個體數(例如失聯、試驗結束)。
w 平均風險人數 用於估計死亡率的有效樣本數,通常計算為 deaths + 0.5 × censored
q 區間死亡率 在該區間內死亡的機率,公式為 q = deaths / w
p 區間存活率 存活機率,p = 1 - q,表示該區間內個體存活下來的機率。
lx 存活比例 到該時間區間結束時,仍然存活的比例(累積存活率),初始為 1(或 100%)。

圖為:

time <- 0:(length(life_table$lx)-1)
plot(time, life_table$lx, type = "s", col = "blue", lwd = 2, xlab="觀察時間",ylab="存活函數")
#  加入點
points(time, life_table$lx, pch = 16, col = "blue")

# # 加入真實模擬存活曲線(指數分布)
t_seq <- seq(0, max(time), by = 0.1)
true_surv <- exp(-0.1 * t_seq) 

lines(t_seq, true_surv, col = "red", lwd = 2, lty = 2)




# 加上圖例
legend("topright",
       legend = c("生命表 (手刻)", "真實指數分布"),
       col = c("blue", "red"),
       lwd = 2,
       lty = c(1, 2),
       bty = "n"
)

可以看到距離真實值偏離了不少,實務上會試著調整\(w_i\)的計算方式,不過下一個介紹的 Kaplan-Meier 可以很好的改進這點。

Kaplan-Meier

Kaplan-Meier 跟 Life table 最大的差異在於,Life table 的時間區段是事先定義,Kaplan-Meier 的時間區段會發生在\([\text{發生事件的時間點},\text{發生事件的下一個時間點})\),這裡的事件指的即是病患死亡或離開研究。也就是說,每次的時間間隔不一定會一樣,產生的 \(\hat S(t)\) 會是大小不一的階梯狀函數。

Kaplan-Meier 所關心的資料畫成圖就像這樣,已經專注在存活長度本身上

實務上, Life table 常用於政府或研究機構定期發布的調查資料,而 Kaplan-Meier 則用在可以具體知道事件發生時間點的臨床研究資料上。

為了與前面的 Life table 法區隔,我們改用\((v_1, \dots ,v_j)\)標示Kaplan-Meier的時間間隔。

  • \(T_i\) : 按資料存活時間/截尾時間排序後,第\(i\)位對象的存活時間/截尾時間,是個隨機變數。

  • \(c\) :研究開始到研究結束的時間

  • \(\delta_i\) : 研究結束時是否過世/死亡

  • \(U_i = \left\{ \begin{array}{r} T_i \quad \text{if }T_i \leq C_r \\ C_r \quad \text{if }T_i > C_r \end{array} \right .\)

  • \(d_{v_j}\): 在時間點 \(v_j\) 時的死亡人數,即\(P(T \in [v_{j-1},v_j)|T>v_{j-1})\)

  • \(n_{v_j}\): 在 \(v_j\) 時的還活著的人數,即\(P\)

  • \(\hat q_j = \frac{d_{v_j}}{n_{v_j}}\): 至特定時間點\(v_j\)的死亡率。

  • \(\hat p_j= 1- \hat q_j\): 至特定時間點\(v_j\)的存活率 (\(\to\)這是我們的目標之一

而我們的目標 \(\hat S(t) = \prod _{i=1}^j \hat p_i\) , 原因前面已提過,就不再贅述。

# sort data
data <- data[order(data$time), ]

# Step 1: 只取有事件的時間
event_times <- data$time[data$status == 1]
unique_event_times <- sort(unique(event_times))

# Step 2: 建立表格
km_table <- data.frame(
  time = unique_event_times,
  n_risk = NA,
  n_event = NA,
  survival_prob = NA,
  cumulative_survival = NA
)

n <- nrow(data)
cum_surv <- 1

for (i in 1:nrow(km_table)) {
  t <- km_table$time[i]
  at_risk <- sum(data$time >= t)
  events <- sum(data$time == t & data$status == 1)
  
  p <- 1 - events / at_risk # 計算存活機率
  cum_surv <- cum_surv * p # 計算累積存活機率
  
  km_table$n_risk[i] <- at_risk
  km_table$n_event[i] <- events
  km_table$survival_prob[i] <- round(p, 3)
  km_table$cumulative_survival[i] <- round(cum_surv, 3)
}

# 加入真實值
# 求出"某個時間點 t 的「還活著的機率」"
lambda_t <- exp(-lambda * km_table$time)
km_table$true_survival <- round(lambda_t, 3)
km_table
   time n_risk n_event survival_prob cumulative_survival true_survival
1  0.24    100       1         0.990               0.990         0.976
2  0.31     99       1         0.990               0.980         0.969
3  0.35     98       1         0.990               0.970         0.966
4  0.37     97       1         0.990               0.960         0.964
5  0.52     96       1         0.990               0.950         0.949
6  0.81     95       1         0.989               0.940         0.922
7  0.93     94       2         0.979               0.920         0.911
8  1.06     92       1         0.989               0.910         0.899
9  1.08     91       1         0.989               0.900         0.898
10 1.15     90       1         0.989               0.890         0.891
11 1.20     89       1         0.989               0.880         0.887
12 1.25     88       1         0.989               0.870         0.882
13 1.27     87       2         0.977               0.850         0.881
14 1.57     85       2         0.976               0.830         0.855
15 1.62     83       1         0.988               0.820         0.850
16 1.79     82       1         0.988               0.810         0.836
17 1.89     81       1         0.988               0.800         0.828
18 1.90     80       1         0.988               0.790         0.827
19 1.93     79       1         0.987               0.780         0.824
20 2.03     78       1         0.987               0.770         0.816
21 2.06     77       1         0.987               0.760         0.814
22 2.19     76       1         0.987               0.750         0.803
23 2.30     75       1         0.987               0.740         0.795
24 2.37     74       1         0.986               0.730         0.789
25 2.47     73       1         0.986               0.720         0.781
26 2.60     72       1         0.986               0.710         0.771
27 2.71     71       1         0.986               0.700         0.763
28 2.94     70       1         0.986               0.690         0.745
29 3.10     69       1         0.986               0.680         0.733
30 3.13     68       1         0.985               0.670         0.731
31 3.29     67       1         0.985               0.660         0.720
32 3.41     66       1         0.985               0.650         0.711
33 3.42     65       2         0.969               0.630         0.710
34 3.52     63       1         0.984               0.620         0.703
35 3.88     62       1         0.984               0.610         0.678
36 4.25     61       1         0.984               0.600         0.654
37 4.28     60       1         0.983               0.590         0.652
38 4.49     59       1         0.983               0.580         0.638
39 4.72     58       1         0.983               0.570         0.624
40 4.75     57       1         0.982               0.560         0.622
41 4.91     56       1         0.982               0.550         0.612
42 4.92     55       1         0.982               0.540         0.611
43 4.99     54       1         0.981               0.530         0.607
44 5.01     53       1         0.981               0.520         0.606
45 5.19     49       1         0.980               0.509         0.595
46 5.80     44       1         0.977               0.498         0.560
47 5.81     43       1         0.977               0.486         0.559
48 5.85     42       1         0.976               0.475         0.557
49 6.38     40       1         0.975               0.463         0.528
50 6.88     37       1         0.973               0.450         0.503
51 7.11     36       1         0.972               0.438         0.491
52 7.30     32       1         0.969               0.424         0.482
53 7.68     29       1         0.966               0.409         0.464
54 8.31     23       1         0.957               0.392         0.436
55 8.99     22       1         0.955               0.374         0.407
欄位解釋
欄位名稱 意義
time 發生事件的時間點(如死亡、復發)
n_risk 在該時間點仍處於風險中的人數(尚未發生事件或被截尾)
n_event 在該時間點發生事件的人數
survival_prob 單一時間點的存活機率(即 (1 - ))
cumulative_survival 累積存活率,表示從起始時間到該時間點的整體存活機率
true_survival 真實存活率(按照模擬資料指定分布 \(\text{EXP}(0.1)\) 反推)
表格解讀

Kaplan-Meier 曲線是階梯狀的,每次事件發生時下降,未發生事件的時間點則維持不變。這份表格展示了:

  • 初始時刻(time = 0.24):有 100 人在風險中,發生 1 件事件,存活機率為 0.990,累積存活率也為 0.990。
  • 隨時間推移:每次事件發生都會使累積存活率下降。例如:
    • 在 time = 1.27,有 2 件事件,累積存活率從 0.870 降至 0.850。
    • 到 time = 5.01,累積存活率已降至 0.520,表示只有 52% 的人存活至該時間點。
  • true_survival 欄位:用來與 Kaplan-Meier 所估計的 cumulative_survival 做比較。

畫圖觀察

# 畫 Kaplan-Meier 估計的存活機率(step line)
plot(km_table$time, km_table$cumulative_survival, type = "s", 
     col = "blue", lwd = 2, ylim = c(0, 1),
     xlab = "Time", ylab = "Survival Probability",
     main = "Kaplan-Meier vs True Survival Function")

# 加上真實存活函數的線(用對應時間點計算的真實值)
lines(km_table$time, km_table$true_survival, col = "red", lwd = 2, lty = 2)



# 加圖例
legend("topright", legend = c("KM Estimate", "True S(t)"),
       col = c("blue", "red"), lwd = 2, lty = c(1, 2))

畫圖觀察後可以發現,Kaplan-Meier 在樣本數為100下,比起 life table , Kaplan-Meier fiting 得更好。

參考資料與延伸閱讀

劉嘉樺. Life‑table method 與 Kaplan‑Meier method 的不同之處. PDF on the internet. 2020.
林建甫. 存活分析. 雙葉書局. 2008.
無符合的項目

腳註

  1. Greenwood Major 1938The first life tableNotes Rec. R. Soc. Lond.170–72 http://doi.org/10.1098/rsnr.1938.0017↩︎