6  Part 6(Bonus):Figure 3 — Time to first absence of fluid

預計時間:20 分鐘。時間吃緊可整段跳過。 deliverable:一張 Kaplan–Meier curve(time to first absence of IRF and SRF)+ Cox hazard ratio。

📚 Survival 方法學引用
  • Kaplan–Meier 教學:Jager 2008 Kidney Int1、Rich 2010 practical guide2
  • Survival in clinical trials:Fleming & Lin 2000 Biometrics3
  • Proportional hazards 假設檢定(Schoenfeld residuals 等):Hess 1995 Stat Med4
  • 非比例 hazards(NPH)的處理:Gregson 2019 JACC5

AI 對 Cox 模型的 PH 假設、log-rank 與 weighted log-rank 的差別、以及 cumulative incidence 與 KM survival 的視覺翻轉常常給錯。本章把 Cheung 2025 paper 的 KM cumulative incidence 風格 reproduce,PH assumption 用 cox.zph() 檢查(範例見 appendix.qmd)。

我有 R data frame baselinefollowup。請給我 R code:

  1. 從 baseline 篩出「有 IRF 或 SRF」的病人(樣本量 ≈ 1170)
  2. 把 followup 加上 baseline 的 arm 與 baseline IRF/SRF
  3. 對每個病人找出第一個 IRF=0 且 SRF=0 的 visit (week 4/8/12),作為 time-to-event;沒事件則 censored at last visit
  4. survival::survfit(Surv(time, event) ~ arm) 跑 KM
  5. survminer::ggsurvplot() 畫圖(cumulative incidence,亦即 1-KM)
  6. survival::coxph() 拿 hazard ratio + 95% CI + log-rank p

6.1 任務 25:什麼是 time-to-event analysis?

📋 複製這段話,貼給 AI(🆕 開新對話):

用「給沒學過統計的眼科醫師聽得懂」的方式 100 字內解釋: 1. 什麼是 time-to-event? 2. 什麼是 censoring? 3. 為什麼不能用 mean / proportion 比較「事件發生時間」? 4. 什麼是 hazard ratio?

速記
  • 事件:第一次 IRF + SRF 都消失(在 OCT 上看不到 fluid)
  • censoring:到追蹤結束都沒事件的病人;不能直接刪、會偏 bias1
  • Kaplan–Meier:把「直到 t 時間還沒事件」的比例畫出來,會階梯狀下降2,3
  • Hazard ratio = 1.47:faricimab 比 aflibercept 的「事件發生 instantaneous rate」高 47%
📖 名詞|survival vs cumulative incidence

KM 預設畫的是 S(t) survival function:「t 時還沒發生事件的比例」,從 100% 開始往下掉。 fun = "event" 把它翻過來變 cumulative incidence = 1 − S(t):「t 時已發生事件的比例」,從 0% 開始往上爬。

我們的 outcome 是「fluid 消失」(好事),所以畫 cumulative incidence 比較直觀——曲線越高代表「越多人達標」。Paper Fig 3 也是這樣畫。

📖 名詞|PH 假設(proportional hazards assumption)

Cox model 假設:「兩 arm 的 hazard 比值在整段時間都一樣」4白話:兩條 KM 曲線不會交叉、也不會在某個時點突然反轉。 12 週 head-to-head phase 通常 OK;如果你拉長到 2 年看,就要查 PH 是否還成立(用 cox.zph())。 碰到 NPH(曲線交叉、後段拉開)→ 改用 RMST、weighted log-rank、time-varying coefficient 等5


6.2 任務 26:建立 time-to-event dataset

# ── 前置(可單獨執行):套件 + 資料 ──
library(readr); library(dplyr); library(tidyr)
library(survival); library(survminer); library(broom)
baseline <- read_csv("data/faricimab_baseline.csv", show_col_types = FALSE)
followup <- read_csv("data/faricimab_followup.csv", show_col_types = FALSE)
# Cohort:baseline 有 IRF 或 SRF 的病人(per paper:n_fari=581, n_afli=591)
elig <- baseline |>
  filter(irf_baseline == 1 | srf_baseline == 1) |>
  select(patient_id, arm)

cat("Eligible n:", nrow(elig), "\n")
Eligible n: 1089 
cat("  faricimab :", sum(elig$arm == "faricimab"), "\n")
  faricimab : 539 
cat("  aflibercept:", sum(elig$arm == "aflibercept"), "\n")
  aflibercept: 550 
# 對每個病人找第一個 IRF=0 & SRF=0 的 visit
tte <- followup |>
  filter(patient_id %in% elig$patient_id) |>
  mutate(both_absent = (irf == 0 & srf == 0)) |>
  group_by(patient_id) |>
  arrange(week) |>
  summarise(
    first_event_week = suppressWarnings(
      min(week[!is.na(both_absent) & both_absent])
    ),
    last_visit_week  = suppressWarnings(
      max(week[!is.na(both_absent)])
    ),
    .groups = "drop"
  ) |>
  mutate(
    event = is.finite(first_event_week),
    time  = if_else(event, first_event_week, last_visit_week),
    time  = if_else(is.infinite(time), 12, time)  # default censor at end
  ) |>
  left_join(elig, by = "patient_id") |>
  mutate(arm = factor(arm, levels = c("aflibercept","faricimab")))

head(tte)
# A tibble: 6 × 6
  patient_id first_event_week last_visit_week event  time arm        
  <chr>                 <dbl>           <dbl> <lgl> <dbl> <fct>      
1 P0001                     4              12 TRUE      4 faricimab  
2 P0002                     4              12 TRUE      4 aflibercept
3 P0004                     4              12 TRUE      4 faricimab  
4 P0005                     8              12 TRUE      8 faricimab  
5 P0006                     4              12 TRUE      4 faricimab  
6 P0007                     8              12 TRUE      8 aflibercept

6.3 任務 27:畫 KM curve(cumulative incidence 形式)

📋 複製這段話,貼給 AI(🆕 開新對話):

survminer::ggsurvplot() 畫 cumulative incidence(fun=“event”),by arm。x 從 0 到 12 週。 faricimab 深藍 #1F5C8B、aflibercept 赭橙 #C75D38。

6.3.1 參考程式碼

# ── 前置(可單獨執行):套件 + 資料 ──
library(readr); library(dplyr); library(tidyr)
library(survival); library(survminer); library(broom)
baseline <- read_csv("data/faricimab_baseline.csv", show_col_types = FALSE)
followup <- read_csv("data/faricimab_followup.csv", show_col_types = FALSE)
# ⚠️ 沿用前面任務的物件:tte(請先跑完任務26,本段不重算)
fit_km <- survfit(Surv(time, event) ~ arm, data = tte)
fit_km
Call: survfit(formula = Surv(time, event) ~ arm, data = tte)

                  n events median 0.95LCL 0.95UCL
arm=aflibercept 550    513      8       4       8
arm=faricimab   539    521      4       4       4
plot_km <- ggsurvplot(
  fit_km,
  data = tte,
  fun = "event",  # cumulative incidence (1 - S(t))
  conf.int = TRUE,
  risk.table = TRUE,
  risk.table.height = 0.25,
  palette = c("#C75D38", "#1F5C8B"),
  legend.labs = c("Aflibercept Q8W", "Faricimab up to Q16W"),
  xlab = "Time (Weeks)",
  ylab = "Cumulative Incidence (%) of First Absence of IRF and SRF",
  break.time.by = 4,
  ggtheme = theme_minimal(base_size = 12) +
    theme(legend.position = "bottom")
)
plot_km

Figure 3. Reproduced — Time to first absence of IRF and SRF in patients with IRF or SRF at baseline.

6.4 任務 28:Cox 回歸 — Hazard ratio

📋 複製這段話,貼給 AI(↩︎️ 續前對話):

survival::coxph(Surv(time, event) ~ arm + study + bcva_strat + lld_strat + region, data = tte) 算 stratified Cox model。回傳 hazard ratio + 95% CI + log-rank p。

6.4.1 參考程式碼

# ── 前置(可單獨執行):套件 + 資料 ──
library(readr); library(dplyr); library(tidyr)
library(survival); library(survminer); library(broom)
baseline <- read_csv("data/faricimab_baseline.csv", show_col_types = FALSE)
followup <- read_csv("data/faricimab_followup.csv", show_col_types = FALSE)
# ⚠️ 沿用前面任務的物件:tte(請先跑完任務26,本段不重算)
tte_full <- tte |>
  left_join(
    baseline |> select(patient_id, study, bcva_strat, lld_strat, region),
    by = "patient_id"
  ) |>
  mutate(
    study      = factor(study),
    bcva_strat = factor(bcva_strat),
    lld_strat  = factor(lld_strat),
    region     = factor(region)
  )

m_cox <- coxph(
  Surv(time, event) ~ arm + study + bcva_strat + lld_strat + region,
  data = tte_full
)
summary(m_cox)
Call:
coxph(formula = Surv(time, event) ~ arm + study + bcva_strat + 
    lld_strat + region, data = tte_full)

  n= 1089, number of events= 1034 

                    coef exp(coef) se(coef)      z Pr(>|z|)    
armfaricimab     0.24118   1.27275  0.06250  3.859 0.000114 ***
studyTENAYA     -0.02334   0.97693  0.06243 -0.374 0.708506    
bcva_strat>=74   0.04201   1.04290  0.09973  0.421 0.673583    
bcva_strat55-73 -0.08774   0.91600  0.06800 -1.290 0.196946    
lld_strat>=33   -0.04954   0.95167  0.06631 -0.747 0.455025    
regionUS-Canada  0.01186   1.01193  0.06300  0.188 0.850668    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

                exp(coef) exp(-coef) lower .95 upper .95
armfaricimab       1.2728     0.7857    1.1260     1.439
studyTENAYA        0.9769     1.0236    0.8644     1.104
bcva_strat>=74     1.0429     0.9589    0.8577     1.268
bcva_strat55-73    0.9160     1.0917    0.8017     1.047
lld_strat>=33      0.9517     1.0508    0.8357     1.084
regionUS-Canada    1.0119     0.9882    0.8944     1.145

Concordance= 0.562  (se = 0.015 )
Likelihood ratio test= 18.34  on 6 df,   p=0.005
Wald test            = 18.37  on 6 df,   p=0.005
Score (logrank) test = 18.45  on 6 df,   p=0.005
tidy(m_cox, exponentiate = TRUE, conf.int = TRUE) |>
  filter(grepl("^arm", term)) |>
  select(term, hazard_ratio = estimate, lower = conf.low,
         upper = conf.high, p.value)
# A tibble: 1 × 5
  term         hazard_ratio lower upper  p.value
  <chr>               <dbl> <dbl> <dbl>    <dbl>
1 armfaricimab         1.27  1.13  1.44 0.000114

對照 paper:HR = 1.47 (1.24–1.73)、log-rank p < 0.0001。


6.5 整合:你下課帶回家的東西

工作坊結束時,你的 Posit.Cloud project 裡應該有:

檔案 內容
_book/ 渲染好的整本 HTML 教材,可瀏覽
data/faricimab_baseline.csv 模擬資料
scripts/01_table1.R 5 支獨立可跑的 R 腳本
你自己跑出的圖 Table 1、Fig 1A/B、Fig 2 三 panel、Fig 3 KM
data/data_dictionary.md 院內 csv schema
appendix.qmd 院內換資料時的常見錯誤

下一步:把這個 project 用 Posit.Cloud 「Save a Permanent Copy」存起來,去找院內 IT。


本章重點
  1. Time-to-event 多了一個維度:「事件發生有多快」
  2. KM 是 non-parametric、Cox 是 semi-parametric;前者畫圖、後者算 HR
  3. Cumulative incidence (1-KM) 是 paper 慣用的展示方式
  4. 三個 method(MMRM、CMH、Cox)放一起就是一篇 phase 3 nAMD paper 的骨架
1.
Jager KJ, Dijk PC van, Zoccali C, Dekker FW. The analysis of survival data: The Kaplan–Meier method. Kidney International. 2008;74(5):560-565. doi:10.1038/ki.2008.217
2.
Rich JT, Neely JG, Paniello RC, Voelker CCJ, Nussenbaum B, Wang EW. A practical guide to understanding Kaplan–Meier curves. Otolaryngology–Head and Neck Surgery. 2010;143(3):331-336. doi:10.1016/j.otohns.2010.05.007
3.
Fleming TR, Lin DY. Survival analysis in clinical trials: Past developments and future directions. Biometrics. 2000;56(4):971-983. doi:10.1111/j.0006-341X.2000.0971.x
4.
Hess KR. Graphical methods for assessing violations of the proportional hazards assumption in Cox regression. Statistics in Medicine. 1995;14(15):1707-1723. doi:10.1002/sim.4780141510
5.
Gregson J, Sharples L, Stone GW, Burman CF, Öhrn F, Pocock S. Nonproportional hazards for time-to-event outcomes in clinical trials. Journal of the American College of Cardiology. 2019;74(16):2102-2112. doi:10.1016/j.jacc.2019.08.1034