Bài 1: Ôn tập phân tích sống còn cho người làm mô hình

Đọc đường Kaplan–Meier như người làm mô hình: biết nó cho gì và còn thiếu gì

Vì sao bạn nên quan tâm?

Cuối năm, khoa lao/HIV của một bệnh viện Phổi tỉnh tổng kết số liệu và tự hỏi người bệnh của mình có giống con số cả nước không. Trong đoàn hệ điều trị năm 2023 của cả nước, 177 trong số 2,310 người bệnh lao có HIV tử vong khi đang điều trị (7.7%), và 117 người khác bỏ điều trị (lost to follow-up) mà không ai biết còn sống hay không (World Health Organization 2026). Thuốc lao cho cả đợt 6 tháng chỉ khoảng 1,200,000 VNĐ, bằng giá 30 bát phở (McConnell 2025; Nhadautu.vn 2025); điều quyết định giá trị của một gói chăm sóc thêm là nó giữ được bao nhiêu tháng sống. Bài này giúp bạn đọc đường Kaplan–Meier: người chết, người bị mất dấu nói gì, và mô hình còn cần gì trước khi quyết định có nên chi cho gói chăm sóc tăng cường.

Mục tiêu học tập

Sau bài này, bạn có thể:

  1. Định nghĩa hàm sống còn S(t), nguy cơ tức thời (hazard) h(t), nguy cơ tích lũy H(t) và chuyển đổi giữa chúng.
  2. Tính ước lượng Kaplan–Meier bằng tay và đối chiếu với survfit().
  3. Nhận diện các kiểu kiểm duyệt và giả định kiểm duyệt không mang thông tin.
  4. Ước lượng nguy cơ tức thời thô theo khoảng thời gian để thấy hình dạng nguy cơ.
  5. Giải thích vì sao mô hình quyết định cần cả đường S(t) tới hết khung thời gian, không chỉ trung vị hay HR.

Ý chính

  • Dữ liệu sống còn là cặp (thời gian, trạng thái). Người bị kiểm duyệt (censoring) vẫn cho thông tin: họ sống ít nhất tới lúc bị kiểm duyệt.
  • S(t), h(t), H(t) và f(t) mô tả cùng một phân phối: S(t) = exp(−H(t)).
  • Ước lượng Kaplan–Meier (KM) là tích các xác suất sống qua từng thời điểm có sự kiện; nó đúng khi kiểm duyệt không liên quan tới tiên lượng.
  • Hình dạng nguy cơ tức thời (giảm, tăng, không đổi) quyết định cách ngoại suy, quan trọng hơn trung vị.
  • Mô hình chu kỳ tháng cần p(t) = 1 − S(t+1)/S(t) cho mọi tháng tới hết khung thời gian.
  • Thời gian sống trung bình là diện tích dưới S(t). KM dừng khi phần lớn người bệnh còn sống, nên trung bình chưa biết nếu không ngoại suy.
  • Thời gian sống trung bình giới hạn (RMST) là diện tích dưới KM tới mốc τ: đo được ngay, không cần giả định nguy cơ tỷ lệ.

Nội dung

Dữ liệu sống còn và kiểm duyệt

Phân tích sống còn (survival analysis) nghiên cứu thời gian từ một mốc (vào nghiên cứu) tới một sự kiện (tử vong, tái phát). Đặc trưng của nó là kiểm duyệt (censoring): với một số người, ta chỉ biết sự kiện chưa xảy ra tới lần liên lạc cuối, vì nghiên cứu đã đóng cơ sở dữ liệu (kiểm duyệt hành chính, người vào muộn được theo dõi ngắn hơn) hoặc vì họ bị mất dấu theo dõi. Trong nghiên cứu, “lost to follow-up” nghĩa là mất dấu theo dõi; trong báo cáo của chương trình chống lao, cùng cụm từ là kết quả “bỏ điều trị”.

Trong thử nghiệm mô phỏng của module, nhánh chăm sóc chuẩn có 91 tử vong và 209 người bị kiểm duyệt trên tổng số 300 người. Bỏ người bị kiểm duyệt làm ước lượng sống còn quá thấp; coi họ sống tới cuối làm ước lượng quá cao. KM xử lý đúng, với điều kiện kiểm duyệt không mang thông tin (non-informative censoring): người bị kiểm duyệt có tiên lượng giống người còn được theo dõi. Nếu người nặng hay bỏ khám hơn, giả định này sai và không phương pháp nào ở đây sửa được: 117 người bỏ điều trị trong đoàn hệ năm 2023 ở trên chính là câu hỏi đó.

Thế giới mô phỏng được neo vào CAMELIA (Blanc và c.s. 2011). Trong một đoàn hệ mô phỏng rất lớn, không bị kiểm duyệt, tới tháng 25 (thời gian theo dõi trung vị của CAMELIA) có 27.2% người ở nhánh chuẩn và 17.8% ở nhánh tăng cường đã tử vong, so với 27.4% ở nhánh ART muộn và 17.8% ở nhánh ART sớm của thử nghiệm. Con số 7.7% của đoàn hệ Việt Nam thấp hơn cả hai. Ba lý do khả dĩ: quần thể rộng hơn (không giới hạn ở CD4 ≤200), chỉ đếm tử vong trong thời gian điều trị lao, và một phần người bỏ điều trị có thể đã tử vong mà không được ghi nhận.

Bốn hàm, một phân phối

Gọi T là thời gian tới sự kiện:

\[ S(t) = P(T > t), \qquad f(t) = -\frac{dS(t)}{dt}, \qquad h(t) = \frac{f(t)}{S(t)}, \qquad H(t) = \int_0^t h(u)\,du, \qquad S(t) = e^{-H(t)}. \]

Nguy cơ tức thời (hazard) h(t) là tỷ suất xảy ra sự kiện tại thời điểm t ở những người chưa gặp sự kiện. Nó có đơn vị nghịch đảo thời gian, như mọi tỷ suất ở M02 Bài 2. Với chu kỳ một tháng, xác suất tử vong trong tháng thứ t+1 ở người còn sống đầu tháng là

\[ p(t) = 1 - \frac{S(t+1)}{S(t)} = 1 - e^{-[H(t+1) - H(t)]}. \]

Đây là cầu nối giữa phân tích sống còn và mô hình chuyển trạng thái: chỉ cần S(t), hoặc H(t), là có xác suất chuyển trạng thái cho từng chu kỳ.

Ước lượng Kaplan–Meier

Tại mỗi thời điểm có sự kiện \(t_j\), gọi \(n_j\) là số người còn nguy cơ (number at risk: chưa chết, chưa bị kiểm duyệt ngay trước \(t_j\)) và \(d_j\) là số tử vong. Khi đó (Kaplan và Meier 1958):

\[ \hat S(t) = \prod_{t_j \le t} \left(1 - \frac{d_j}{n_j}\right). \]

Người bị kiểm duyệt tại \(t\) vẫn được tính vào \(n_j\) ở chính thời điểm đó, rồi rời nhóm nguy cơ. Đường KM là hàm bậc thang: chỉ rơi tại thời điểm có tử vong, đi ngang giữa các lần đó. Ví dụ tám người trong phần thực hành cho thấy rõ từng bước.

Hình dạng nguy cơ tức thời

Chia trục thời gian thành các khoảng và lấy số tử vong chia cho tổng người-tháng nguy cơ trong mỗi khoảng, ta có nguy cơ tức thời thô. Ở nhánh chăm sóc chuẩn, nguy cơ trong khoảng 3 tháng đầu là 0.050 mỗi tháng, gấp 9.1 lần mức trung bình từ tháng 12 tới tháng 36 (0.0055). Trong dữ liệu mô phỏng, nguy cơ cao lúc đầu rồi giảm. Ở thử nghiệm CAMELIA, yếu tố liên quan tới tử vong trong nửa năm đầu cũng khác với sau khoảng một năm (Marcy và c.s. 2014), tức nguy cơ không đồng nhất theo thời gian. Bài 2 dùng hình dạng này để loại các phân phối không phù hợp.

Mô hình quyết định cần gì từ dữ liệu sống còn

Bài báo thường báo cáo trung vị sống còn và tỷ số nguy cơ tức thời (hazard ratio, HR). Mô hình kinh tế cần nhiều hơn:

  1. S(t) ở mọi chu kỳ tới hết khung thời gian, để tính xác suất chuyển trạng thái và thời gian ở từng trạng thái.
  2. Diện tích dưới đường cong, vì năm sống trung bình bằng \(\int_0^\infty S(t)\,dt\). QALY và chi phí tính trên cùng diện tích đó.
  3. Một đường cong trơn. KM cho xác suất tử vong bằng 0 ở những tháng không có ai chết (ở nhánh chăm sóc chuẩn là 5 trong 30 tháng đầu), và các bước rơi ở phần đuôi chỉ dựa trên vài người.

Trong thử nghiệm này, KM của nhánh chăm sóc chuẩn dừng ở 0.655, nhánh chăm sóc tăng cường ở 0.814. Trung vị sống còn chưa đạt, và thời gian sống trung bình chưa tính được nếu không ngoại suy. Thứ đo được ngay là RMST (restricted mean survival time), diện tích dưới KM tới một mốc τ (Royston và Parmar 2013). Tới 30 tháng, RMST là 23.03 tháng ở nhánh chăm sóc chuẩn và 25.93 tháng ở nhánh tăng cường, tức thêm 2.91 tháng sống. Mô hình cần con số tương tự nhưng cho cả đời người, và đó là việc của Bài 2 và Bài 3.

flowchart LR
  A["Dữ liệu cá thể<br/>(thời gian, trạng thái)"] --> B["KM và nguy cơ<br/>tức thời thô (Bài 1)"]
  B --> C["Mô hình tham số<br/>(Bài 2)"]
  C --> D["Ngoại suy có<br/>tử vong nền (Bài 3)"]
  D --> E["Mô hình quyết định<br/>PSM hoặc STM (Bài 5)"]
  F["Chỉ có bài báo:<br/>tái tạo IPD (Bài 4)"] --> B

flowchart LR
  A["Dữ liệu cá thể<br/>(thời gian, trạng thái)"] --> B["KM và nguy cơ<br/>tức thời thô (Bài 1)"]
  B --> C["Mô hình tham số<br/>(Bài 2)"]
  C --> D["Ngoại suy có<br/>tử vong nền (Bài 3)"]
  D --> E["Mô hình quyết định<br/>PSM hoặc STM (Bài 5)"]
  F["Chỉ có bài báo:<br/>tái tạo IPD (Bài 4)"] --> B

Thực hành

Chạy cả bài bằng Rscript code/l01-survival-basics.R từ thư mục module. Đoạn đầu nạp hàm, tham số và sinh thử nghiệm mô phỏng với hạt giống cố định.

# Vietnamese text in figures needs a UTF-8 locale (Rscript may start in the "C" locale)
if (!isTRUE(l10n_info()[["UTF-8"]])) invisible(Sys.setlocale("LC_CTYPE", "en_US.UTF-8"))
source("../_shared/R/helpers.R")
source("../_shared/R/vn_data.R")   # course-wide context, unit prices and life table
source("code/model.R")
suppressPackageStartupMessages({
  library(ggplot2)
  library(survival)
})
ps <- read_params("params/sim_truth.csv")     # the hidden "true world" of the simulation
pm <- read_params("params/model_inputs.csv")  # inputs of the decision model
cfg <- read_params("params/lesson_settings.csv")  # settings of the lessons (band widths, tau, ...)
ctx <- vn_context()               # course-wide: discount rate, threshold, exchange rate, wages
pr <- vn_prices()                 # course-wide unit prices (BHYT fees, tests, ART, bed-days)
inp <- build_inputs(pm, ctx, pr)  # decision-model inputs: module values + shared values and prices
lt <- vn_lifetable("both")        # WHO life table, Viet Nam 2023, both sexes
trial <- simulate_trial(ps, lt, seed = SEED_TRIAL)

Mỗi người một dòng. Bảng đếm cho số tử vong, số bị kiểm duyệt và thời gian theo dõi dài nhất mỗi nhánh; stopifnot() xác nhận đủ người, thời gian dương, EFS không dài hơn OS.

# One row per person: follow-up time (months) and event indicator (1 = event, 0 = censored)
head(trial, 4)
  id arm age efs_time efs_event prog_event  os_time os_event
1  1 soc  39 35.39712         0          0 35.39712        0
2  2 soc  27 34.41677         0          0 34.41677        0
3  3 soc  36 34.69223         0          0 34.69223        0
4  4 soc  34 28.40555         0          0 28.40555        0
counts <- data.frame(
  arm = ARMS,
  n = as.vector(table(trial$arm)),
  deaths = as.vector(tapply(trial$os_event, trial$arm, sum)),
  censored = as.vector(tapply(1 - trial$os_event, trial$arm, sum)),
  max_follow_up = as.vector(tapply(trial$os_time, trial$arm, max))
)
counts
  arm   n deaths censored max_follow_up
1 soc 300     91      209      35.85782
2 enh 300     54      246      35.88463
# Checks: counts agree with survfit(), which counts events its own way; times are positive;
# EFS never lasts longer than OS (an event-free person must be alive)
sf_count <- summary(survfit(Surv(os_time, os_event) ~ arm, data = trial))$table
stopifnot(nrow(trial) == 2 * ps$n_per_arm,
          all(trial$os_time > 0),
          all(trial$efs_time <= trial$os_time + 1e-12),
          all(counts$deaths == sf_count[, "events"]),
          all(counts$n == sf_count[, "records"]))

Kiểm tra mô phỏng có “giống thật” không: tính tử vong thật tới tháng 25 trong một đoàn hệ mô phỏng 100,000 người mỗi nhánh rồi so với hai nhánh CAMELIA. stopifnot() dừng script nếu chênh quá 3 điểm phần trăm; cuối chunk là tỷ lệ tử vong trong đoàn hệ Việt Nam năm 2023 để so.

# The simulated world is anchored to CAMELIA (TB/HIV, CD4 <= 200, Cambodia): true deaths by
# the trial's median follow-up, from a very large uncensored simulated cohort, against the
# published proportions (late ART ~ standard care, early ART ~ enhanced care)
truth_fu <- true_curves(ps, lt, pm$fu_camelia_months, seed = SEED_TRUTH)
anchor <- data.frame(
  arm = ARMS,
  sim_true_deaths = sapply(ARMS, function(g) 1 - tail(truth_fu$os[truth_fu$arm == g], 1)),
  camelia = c(pm$deaths_camelia_late / pm$n_camelia_late, pm$deaths_camelia_early / pm$n_camelia_early))
anchor$gap <- anchor$sim_true_deaths - anchor$camelia
round(anchor[, -1], 3)
    sim_true_deaths camelia    gap
soc           0.272   0.274 -0.001
enh           0.178   0.178  0.000
stopifnot(all(abs(anchor$gap) < 0.03))   # stated tolerance: within 3 percentage points
# Vietnamese routine data for comparison: deaths during TB treatment, HIV-positive TB, 2023 cohort
p_died_vn <- pm$vn_tbhiv_died_2023 / pm$vn_tbhiv_cohort_2023
p_died_vn
[1] 0.07662338
stopifnot(p_died_vn < min(anchor$camelia))   # prose claim: the national figure is below both CAMELIA arms

Ví dụ tám người giúp thấy cách tính KM. Ở tháng 2 có một người chết và một người bị kiểm duyệt: cả hai đều được tính vào số còn nguy cơ tại tháng 2. Hàm tự viết km_by_hand() phải cho kết quả trùng với survfit().

# Eight people, times in months; status 1 = died, 0 = censored (alive at last contact)
toy <- data.frame(time = c(1, 2, 2, 4, 5, 7, 8, 10),
                  status = c(1, 0, 1, 1, 0, 1, 0, 0))
km_toy <- km_by_hand(toy$time, toy$status)
km_toy
  time n_risk n_event  surv
1    1      8       1 0.875
2    2      7       1 0.750
3    4      5       1 0.600
4    7      3       1 0.400
# Same numbers from the survival package
fit_toy <- survfit(Surv(time, status) ~ 1, data = toy)
stopifnot(all(abs(km_toy$surv - summary(fit_toy)$surv) < 1e-12))

Cùng hàm đó áp lên nhánh chăm sóc chuẩn. Chênh lệch lớn nhất với survfit() phải bằng 0 (sai số làm tròn của máy).

# The same hand-written estimator on the standard-care arm, checked against survfit()
d_soc <- endpoint_data(trial, "os", "soc")
km_soc <- km_by_hand(d_soc$time, d_soc$status)
sf_soc <- survfit(Surv(time, status) ~ 1, data = d_soc)
max_diff <- max(abs(km_soc$surv - summary(sf_soc)$surv))
max_diff
[1] 1.110223e-16
stopifnot(max_diff < 1e-12)

Vẽ KM hai nhánh với dấu kiểm duyệt và in số người còn nguy cơ mỗi 6 tháng; số này giảm nhanh ở cuối do kiểm duyệt hành chính.

km_list <- list()
cens_list <- list()
for (g in ARMS) {
  d <- endpoint_data(trial, "os", g)
  km <- km_by_hand(d$time, d$status)
  km_list[[g]] <- data.frame(time = c(0, km$time), surv = c(1, km$surv), arm = ARM_LABELS[[g]])
  cens_t <- d$time[d$status == 0]
  cens_list[[g]] <- data.frame(time = cens_t, surv = km_at(km, cens_t), arm = ARM_LABELS[[g]])
}
km_df <- do.call(rbind, km_list)
cens_df <- do.call(rbind, cens_list)
fig_km <- ggplot(km_df, aes(time, surv, colour = arm)) +
  geom_step(linewidth = 0.9) +
  geom_point(data = cens_df, shape = 3, size = 1.2) +
  scale_colour_manual(values = pal_course[1:2], name = NULL) +
  scale_x_continuous(breaks = seq(0, ps$study_months, 6)) +
  coord_cartesian(ylim = c(0.5, 1)) +
  labs(x = "Tháng kể từ khi vào nghiên cứu", y = "Tỷ lệ còn sống (KM)",
       title = "Sống còn toàn bộ trong thử nghiệm mô phỏng\n(dấu + là kiểm duyệt)") +
  theme_course()
save_fig(fig_km, "l01-km.png")

# Numbers at risk, as printed under a published KM figure (one column every step_show months)
step_show <- 6
sf_both <- survfit(Surv(os_time, os_event) ~ arm, data = trial)
at_risk <- summary(sf_both, times = seq(0, cfg$rmst_tau, by = step_show))
risk_tab <- data.frame(arm = at_risk$strata, month = at_risk$time,
                       n_risk = at_risk$n.risk, surv = round(at_risk$surv, 3))
risk_tab
       arm month n_risk  surv
1  arm=soc     0    300 1.000
2  arm=soc     6    244 0.816
3  arm=soc    12    227 0.766
4  arm=soc    18    212 0.732
5  arm=soc    24    204 0.714
6  arm=soc    30    100 0.697
7  arm=enh     0    300 1.000
8  arm=enh     6    267 0.900
9  arm=enh    12    259 0.883
10 arm=enh    18    244 0.842
11 arm=enh    24    235 0.824
12 arm=enh    30    121 0.814

Đường KM của hai nhánh trong thử nghiệm mô phỏng

Nguy cơ tức thời thô trong từng khoảng 3 tháng. Kiểm tra rằng tổng số tử vong theo khoảng bằng tổng số tử vong quan sát, và để ý nguy cơ ở khoảng đầu cao hơn hẳn các khoảng sau.

# Crude hazard in 3-month bands: deaths / person-months at risk
breaks <- seq(0, ps$study_months, by = cfg$haz_band_months)
hz_list <- list()
for (g in ARMS) {
  d <- endpoint_data(trial, "os", g)
  hz <- interval_hazard(d$time, d$status, breaks)
  hz$arm <- ARM_LABELS[[g]]
  hz_list[[g]] <- hz
}
hz_df <- do.call(rbind, hz_list)
hz_df[hz_df$start %in% c(0, cfg$haz_band_months, 12, 24), c("arm", "start", "end", "events", "person_time", "hazard")]
                      arm start end events person_time      hazard
soc.1      Chăm sóc chuẩn     0   3     41    815.7970 0.050257601
soc.2      Chăm sóc chuẩn     3   6     14    751.8568 0.018620566
soc.5      Chăm sóc chuẩn    12  15      4    674.1659 0.005933257
soc.9      Chăm sóc chuẩn    24  27      3    525.5293 0.005708530
enh.1 Chăm sóc tăng cường     0   3     22    848.7127 0.025921611
enh.2 Chăm sóc tăng cường     3   6      8    811.9704 0.009852576
enh.5 Chăm sóc tăng cường    12  15      9    755.8048 0.011907837
enh.9 Chăm sóc tăng cường    24  27      1    607.0381 0.001647343
# Check: band-wise events add up to all observed deaths
stopifnot(sum(hz_df$events) == sum(trial$os_event))
# Standard care: hazard in the first band versus months 12 to the end, pooled
hz_soc_first <- interval_hazard(d_soc$time, d_soc$status, c(0, cfg$haz_band_months))$hazard
hz_soc_late <- interval_hazard(d_soc$time, d_soc$status, c(12, ps$study_months))$hazard
c(first_band = hz_soc_first, months_12_to_end = hz_soc_late, ratio = hz_soc_first / hz_soc_late)
      first_band months_12_to_end            ratio 
     0.050257601      0.005547292      9.059844775 
stopifnot(hz_soc_first > hz_soc_late)   # prose claim: the hazard is highest at the start

fig_hz <- ggplot(hz_df, aes(x = start, xend = end, y = hazard, yend = hazard, colour = arm)) +
  geom_segment(linewidth = 0.9) +
  scale_colour_manual(values = pal_course[1:2], name = NULL) +
  scale_x_continuous(breaks = seq(0, ps$study_months, 6)) +
  labs(x = "Tháng", y = "Tử vong / người-tháng",
       title = "Nguy cơ tức thời ước lượng thô theo khoảng thời gian: cao nhất lúc đầu") +
  theme_course()
save_fig(fig_hz, "l01-hazard.png")

Nguy cơ tức thời thô theo từng khoảng thời gian

Cuối cùng, chuyển KM thành thứ mô hình cần: S(t) và xác suất tử vong mỗi tháng. Kiểm tra các xác suất nằm trong đoạn 0–1, xem các tháng có xác suất bằng 0, và so RMST giữa hai nhánh.

# What a monthly decision model needs: S(t) at every cycle and the conditional
# probability of dying in each cycle among those alive at its start
months <- 0:30
S_km <- km_at(km_soc, months)
p_month <- 1 - S_km[-1] / S_km[-length(S_km)]
needs <- data.frame(month = months[-1], S = round(S_km[-1], 3), p_death_month = round(p_month, 4))
needs[needs$month %in% c(1, 2, 3, 6, 12, 24, 30), ]
   month     S p_death_month
1      1 0.926        0.0735
2      2 0.886        0.0433
3      3 0.863        0.0264
6      6 0.816        0.0201
12    12 0.766        0.0130
24    24 0.714        0.0000
30    30 0.697        0.0000
stopifnot(all(p_month >= 0 & p_month <= 1))

# Restricted mean survival time (area under KM) up to 30 months, both arms
tau <- cfg$rmst_tau
km_enh <- km_by_hand(endpoint_data(trial, "os", "enh")$time, endpoint_data(trial, "os", "enh")$status)
rmst <- c(soc = rmst_km(km_soc, tau), enh = rmst_km(km_enh, tau))
rmst
     soc      enh 
23.02524 25.93342 
# Check against survival's own restricted mean (same area, computed by the package)
rmst_pkg <- summary(survfit(Surv(os_time, os_event) ~ arm, data = trial), rmean = tau)$table[, "rmean"]
stopifnot(all(abs(unname(rmst_pkg) - unname(rmst)) < 1e-8))
rmst_gain <- rmst[["enh"]] - rmst[["soc"]]
# Survival at the end of follow-up: far from zero, so the mean is unknown without extrapolation
s_end <- c(soc = tail(km_soc$surv, 1), enh = tail(km_enh$surv, 1))
s_end
      soc       enh 
0.6553871 0.8140573 
stopifnot(all(s_end > 0.5))   # prose claim: the median is not reached, KM stops far from zero
Mẹo thực hành
  • Luôn vẽ KM kèm dấu kiểm duyệt và bảng số người còn nguy cơ; đừng diễn giải các bước rơi cuối khi chỉ còn vài chục người.
  • Báo cáo thời gian theo dõi bằng KM đảo ngược (reverse KM), không bằng trung vị của mọi thời gian quan sát.
  • Tự viết lại một phương pháp một lần (như km_by_hand()) rồi đối chiếu với gói chuẩn: cách nhanh nhất để hiểu gói đó làm gì.
Lỗi thường gặp
  • Lấy số tử vong chia số người làm xác suất tử vong cuối kỳ. Cách này bỏ qua kiểm duyệt và ước lượng thấp nguy cơ. Dùng KM.
  • Dùng KM trực tiếp làm xác suất chuyển trạng thái. Tháng không có tử vong cho xác suất 0, còn sau khi hết theo dõi thì không có số liệu. Dùng mô hình tham số (Bài 2).
  • Nhầm trung vị theo dõi với trung vị sống còn. Hai đại lượng khác nhau; trung vị sống còn có thể chưa đạt.
  • Quên giả định kiểm duyệt không mang thông tin. Nếu người bỏ khám là người nặng, KM ước lượng sống còn quá cao. Kiểm tra lý do mất dấu trong dữ liệu.

Bài tập

Bài tập 1 (dễ). Trong ví dụ tám người, giả sử người thứ hai (tháng 2) thực ra đã tử vong. Tính lại KM bằng tay rồi kiểm tra bằng km_by_hand(). Đường KM thay đổi từ thời điểm nào?

Tại tháng 2 có 7 người còn nguy cơ và nay có 2 tử vong, nên S(2) = 0.875 × (1 − 2/7) = 0.625. Mọi giá trị từ tháng 2 trở đi đều giảm theo; trước tháng 2 không đổi.

# Exercise 1: person 2 (month 2) actually died instead of being censored
toy2 <- toy
toy2$status[2] <- 1
km_by_hand(toy2$time, toy2$status)
  time n_risk n_event      surv
1    1      8       1 0.8750000
2    2      7       2 0.6250000
3    4      5       1 0.5000000
4    7      3       1 0.3333333

Bài tập 2 (trung bình). Tính RMST tới 12 và 24 tháng cho hai nhánh và đổi phần chênh lệch ra số ngày. Vì sao chênh lệch tăng theo τ?

Chênh lệch RMST là diện tích giữa hai đường KM. Hai đường tách nhau sớm và không gặp lại, nên diện tích cộng dồn theo thời gian.

# Exercise 2: RMST at 12 and 24 months, and the gain from the enhanced package
rmst_tab <- data.frame(tau = c(12, 24),
                       soc = c(rmst_km(km_soc, 12), rmst_km(km_soc, 24)),
                       enh = c(rmst_km(km_enh, 12), rmst_km(km_enh, 24)))
rmst_tab$gain_days <- (rmst_tab$enh - rmst_tab$soc) * DAYS_PER_MONTH
round(rmst_tab, 2)
  tau   soc   enh gain_days
1  12  9.97 10.88     27.70
2  24 18.80 21.01     67.18
stopifnot(all(rmst_tab$enh > rmst_tab$soc), diff(rmst_tab$gain_days) > 0)   # prose claim: the gap grows with tau

Bài tập 3 (khó). Ước lượng thời gian theo dõi trung vị bằng KM đảo ngược và so với trung vị của mọi thời gian quan sát. Giải thích vì sao cách thứ hai cho số nhỏ hơn.

Trung vị của thời gian quan sát gộp cả người chết sớm, nên phản ánh tử vong chứ không phải độ dài theo dõi. KM đảo ngược coi tử vong là kiểm duyệt và ước lượng thời gian người bệnh lẽ ra được theo dõi.

# Exercise 3: median follow-up by reverse KM (censoring treated as the "event")
rev_km <- survfit(Surv(os_time, 1 - os_event) ~ 1, data = trial)
c(reverse_km_median = unname(summary(rev_km)$table["median"]),
  naive_median_of_times = median(trial$os_time))
    reverse_km_median naive_median_of_times 
             29.80823              27.82511 
stopifnot(summary(rev_km)$table["median"] > median(trial$os_time))   # prose claim: the naive median is smaller

Tài liệu tham khảo

Đọc thêm: bài tổng quan về khái niệm sống còn của Clark và c.s. (2003); Guyot và c.s. (2012) về vì sao trung vị và HR chưa đủ cho mô hình kinh tế; Latimer (2013) về vì sao lựa chọn mô hình ngoại suy phải được biện minh.

# Slide Lời giảng chính Hình/Code
1 Vì sao module này Hiệu quả của mô hình thường đến từ đường sống còn; thử nghiệm ngắn, mô hình trọn đời Bản đồ module (index)
2 Ví dụ lao/HIV Đoàn hệ Việt Nam 2023: tử vong và bỏ điều trị; mô phỏng neo vào CAMELIA nên vừa giống thật vừa biết giá trị thật Chunk l01-anchor
3 Kiểm duyệt Hành chính vs mất dấu; giả định không mang thông tin; bỏ hay giữ đều sai Chunk l01-data
4 Bốn hàm S, f, h, H; S = exp(−H); p(t) = 1 − S(t+1)/S(t) Công thức
5 KM bằng tay Tám người; ai còn nguy cơ tại tháng 2 Chunk l01-toy
6 KM thử nghiệm Hai nhánh; dấu kiểm duyệt; bảng số người còn nguy cơ figs/l01-km.png
7 Hình dạng nguy cơ Cao lúc đầu, giảm dần; đây là thông tin cho Bài 2 figs/l01-hazard.png
8 Mô hình cần gì S(t) mọi tháng, diện tích, đường cong trơn; tháng có p = 0 Chunk l01-model-needs
9 RMST Diện tích dưới KM tới τ; năm sống “đo được” Chunk l01-model-needs
10 Chuyển tiếp KM dừng khi phần lớn còn sống → cần mô hình tham số và ngoại suy Sơ đồ quy trình