survivalCCW包教程

教程首页
包图标

教程地址:https://genentech.github.io/survivalCCW/

survivalCCW包是一个轻量级的包,有助于构建R中时间到事件端点的克隆删失加权(CCW)分析数据。

背景

在观察性研究中估计非随机化治疗的效果可能具有挑战性,因为在患者的治疗策略启动之前,患者的治疗分配通常是未知的。天真地使用观察性治疗策略通常会导致不朽的时间偏差和其他偏差。例如,考虑治疗持续时间对总体生存期的影响。比较观察到接受治疗时间更长的患者的分析会遭受不朽的时间偏差,因为治疗持续时间更长与生存期更长有关。解决这种偏差的传统方法包括地标分析(landmark analysis),它受到选择偏差和统计学效能降低的影响,以及时间相关的cox模型,它阻止了对绝对风险的估计,导致可解释性更差。克隆删失加权(CCW)是一种稳健的方法,可以解决不朽的时间偏差,同时保持研究效能和可解释性,并允许绝对风险估计。

实施CCW可能很耗时,不仅因为这是一种大多数应用生物统计学家不熟悉的新方法,还因为它涉及细致入微的数据操作。所有患者都被克隆并分配到基线治疗或未治疗;然后他们被转换成“长”随访格式,每个patient-clone-period组合一行;最后,计算每个patient-clone-period的删失权重的反比概率。

{survivalCCW}

为了便于CCW的轻松实现并提供标准方法,我们引入了survivalCCW包,这是一个轻量级、开源的R包,旨在简化使用CCW进行生存分析的过程。该软件包提供了格式化数据、计算权重和轻松执行生存分析的功能。{survivalCCW包的工作流程直观且用户友好,便于快速数据操作和推理。

下面显示了一个说明性示例。


# 使用管道操作符处理数据框df
df |>
  # 创建克隆数据
  # create_clones函数用于生成克隆数据集,主要用于时间依赖性分析
  # id: 指定个体ID列名
  # event: 指定事件状态列名 
  # time_to_event: 指定到事件发生的时间列名
  # exposure: 指定暴露状态列名
  # time_to_exposure: 指定到暴露发生的时间列名
  # ced_window: 设置累积暴露时间窗口为100个时间单位
  create_clones(
    id = 'id',
    event = 'event', 
    time_to_event = 'timetoevent',
    exposure = 'exposure',
    time_to_exposure = 'timetoexposure',
    ced_window = 100
  ) |>
  # 将克隆数据转换为长格式
  # cast_clones_to_long()函数将宽格式的克隆数据转换为长格式,便于后续分析
  cast_clones_to_long() |>
  # 生成累积暴露权重
  # generate_ccw()函数计算累积暴露权重
  # 参数为需要调整的协变量向量c('cov1', 'cov2')
  generate_ccw(c('cov1', 'cov2'))

如何使用 survivalCCW 进行克隆删失加权生存分析

克隆删失加权

这个轻量级的包描述了如何进行克隆删失加权(CCW),以解决生存分析中不朽的时间偏差问题。这个小插曲将浏览马林格等人2020年发布的应用教程。

前言

CCW在观察性研究中存在不朽的人时偏差是有用的。例如,当比较非小细胞肺癌(NSCLC)中手术接受者和非接受者时,手术组将比非手术组有更长的生存时间,因为非手术组包括在接受手术之前死亡的患者。这是一种不朽时间偏差。

马林格发布的ccw数据集使用这个精确的设置作为示例。让我们探索一下数据集。

library(tidyverse)
library(DT) # For better printing of data.frames
data(dummy_data)

head(dummy_data) |>
  DT::datatable(
    rownames = FALSE, 
    options = list(
      scrollX = TRUE,
      paging = FALSE
    )
  )

glimpse(dummy_data)
测试数据
数据类型分布

数据说明

  • id:患者标识符
  • timetoexposure:暴露时间,连续,非暴露患者的NA
  • exposure:二进制,1=暴露,0=未暴露
  • event:事件发生,二进制,1=事件,0=删失
  • timetoevent:事件之间的时间,连续,上限为365
  • cov1:二元协变量——与暴露时间和事件时间正相关
  • cov2:连续协变量——与暴露时间和事件时间正相关

请注意,此包解决了协变量都在基线定义的情况。

创建克隆

第一步是创建克隆。这可以使用create_clones的SurvivalCCW函数对任何时间到事件的结果完成。为了create_clones工作,我们需要传递一个包含以下列的每患者一行的data. frame:

  • id变量(在本例中为id)
  • 表示删失(0)或事件(1)(在本例中为event)的传统结果变量。请注意,尚不允许附加值。
  • 第一个事件的时间(在本例中为timetoEvent)
  • 暴露变量,在删失/事件之前的任何时间定义暴露(在这种情况下是exposure)。必须是(0)或(1)
  • 暴露时间变量(在本例中为timetoexposure)
  • 临床资格窗口(在这种情况下,我们将做100个时间单位或天)

让我们看看这在实践中是什么样子


# 创建克隆数据集
# create_clones()函数用于生成时间依赖性分析所需的克隆数据
clones <- create_clones(
  # df: 输入的原始数据框
  df = dummy_data,
  # id: 指定个体ID列名
  id = 'id', 
  # event: 指定事件状态列名(0/1)
  event = 'event',
  # time_to_event: 指定从基线到事件发生的时间列名
  time_to_event = 'timetoevent',
  # exposure: 指定暴露状态列名(0/1) 
  exposure = 'exposure',
  # time_to_exposure: 指定从基线到暴露发生的时间列名
  time_to_exposure = 'timetoexposure',
  # ced_window: 设置累积暴露时间窗口长度(时间单位)
  ced_window = 100
)

# 展示克隆数据集的前几行
# 使用DT包的datatable函数创建交互式数据表格
head(clones) |>
  DT::datatable(
    # rownames=FALSE: 不显示行名
    rownames = FALSE,
    options = list(
      # scrollX=TRUE: 允许水平滚动
      scrollX = TRUE,
      # paging=FALSE: 不分页显示
      paging = FALSE
    )
  )
数据示例

请注意,此对象只是一个data. frame,带有一个额外的自定义类,未来的函数将评估该类:

class(clones)

根据文档内容,我来解释 create_clones 函数的数据转换过程:

# create_clones 函数的主要步骤:

# 1. 为每个病人创建两个克隆(clone=0和clone=1)
df_0 <- df_1 <- df  # 复制原始数据框两份

# 2. 初始化新的列
df_0$outcome <- df_1$outcome <- rep(NA_integer_, NROW(df))  # 结局列
df_0$fup_outcome <- df_1$fup_outcome <- rep(NA_real_, NROW(df))  # 随访时间列 
df_0$censor <- df_1$censor <- rep(NA_integer_, NROW(df))  # 删失状态列
df_0$fup_censor <- df_1$fup_censor <- rep(NA_real_, NROW(df))  # 删失时间列

# 3. 添加克隆标识
df_0$clone <- 0L  # 未暴露组
df_1$clone <- 1L  # 暴露组

# 4. 合并数据并添加类和属性
df_clones <- rbind(df_0, df_1)  # 合并两个克隆组
class(df_clones) <- c("ccw_clones", class(df_clones))  # 添加ccw_clones类

# 5. 传递重要属性
attributes(df_clones)$id <- id  # 病人ID
attributes(df_clones)$event <- event  # 事件
attributes(df_clones)$time_to_event <- time_to_event  # 事件时间
attributes(df_clones)$exposure <- exposure  # 暴露状态

主要转换过程是:

  1. 对每个病人创建两个克隆(clone=0和clone=1),分别代表未暴露和暴露状态

  2. 为每个克隆添加以下新列:

    • outcome: 结局状态
    • fup_outcome: 随访时间
    • censor: 删失状态
    • fup_censor: 删失时间
  3. 合并两个克隆组的数据,并添加ccw_clones类和相关属性

  4. 这种转换允许我们:

    • 比较同一病人在暴露和未暴露状态下的结局差异
    • 计算因果权重
    • 进行生存分析

这种克隆权重方法可以帮助我们评估暴露对结局的因果效应。


您可以在创建克隆后随着时间的推移可视化删失情况分布:

# 绘制随时间变化的审查情况图
# plot_censoring_over_time()函数用于可视化展示数据中随时间推移的审查模式
# 参数clones: 包含时间依赖性数据的克隆数据集对象
plot_censoring_over_time(clones)

转换为长格式

现在我们只需要将数据转换为长格式。cast_to_long函数将为我们执行此操作。不需要额外的参数(clones对象是一个工件,可以让您更好地查看和理解该方法):

clones_long <- cast_clones_to_long(clones)

head(clones_long, row.names = FALSE) |>
  DT::datatable(
      rownames = FALSE, 
      options = list(
        scrollX = TRUE,
        paging = FALSE
      )
    )

让我详细解释 cast_clones_to_long 函数的数据转换过程:

# cast_clones_to_long 函数主要步骤:

# 1. 获取所有事件时间点
event_times <- sort(unique(c(
    df[,time_to_event],  # 结局事件时间
    df[,time_to_exposure],  # 暴露事件时间 
    ced_window  # 临床资格窗口时间
)))

# 2. 分别处理未暴露组(clone=0)和暴露组(clone=1)的数据
# 以未暴露组为例:
df_0 <- df[df[, "clone"] == 0L, ]  # 获取未暴露组数据
df_0$t_start <- 0  # 设置起始时间为0

# 3. 对每个结局类型进行迭代处理
for (i in 1:NROW(unique(df_0$outcome))) {
    df_outcome <- df_0[df_0$outcome == unique(df_0$outcome)[i], ]
    df_outcome$outcome <- 1L
    
    # 使用 survival::survSplit 函数将数据展开成多个时间区间
    df_0_long_outcome <- survival::survSplit(
        data = df_outcome,
        cut = event_times,  # 在每个事件时间点切分
        end = "fup_outcome",  # 结局随访时间
        start = "t_start",  # 开始时间
        event = "outcome"  # 结局事件
    )
}

# 4. 处理删失数据
df_0_long_censor <- survival::survSplit(
    df_0,
    cut = event_times,
    end = "fup_outcome",
    start = "t_start", 
    event = "censor"
)

# 5. 合并结局和删失数据
df_0_long_outcome$censor <- df_0_long_censor$censor
df_0_long_outcome$t_stop <- df_0_long_outcome$fup_outcome

# 6. 最后合并暴露组和未暴露组的数据
df_long <- rbind(
    df_1_long,  # 暴露组长格式数据
    df_0_long   # 未暴露组长格式数据
)

举个具体例子:

# 原始数据(宽格式)
df_original <- data.frame(
  id = 1,
  exposure = 1,
  time_to_event = 10,
  time_to_exposure = 5
)

# 转换后的数据(长格式)
df_long <- data.frame(
  id = rep(1, 4),
  clone = rep(c(0,1), each=2),  # 0=未暴露,1=暴露
  t_start = c(0,5, 0,5),  # 时间区间起点
  t_stop = c(5,10, 5,10), # 时间区间终点
  outcome = c(0,1, 0,1),  # 在每个时间区间的结局状态
  censor = c(0,0, 0,0)    # 在每个时间区间的删失状态
)

主要转换特点:

  1. 将每个病人的数据按时间点切分成多个区间

  2. 每个区间包含:

    • 起始时间(t_start)
    • 结束时间(t_stop)
    • 该区间内的结局状态
    • 该区间内的删失状态
  3. 这种转换允许我们:

    • 精确计算每个时间点的风险
    • 处理时依赖性协变量
    • 进行复杂的生存分析

这种长格式数据结构更适合进行生存分析,因为它能够捕捉到随时间变化的信息。

让我详细解释时间点区间的设定过程:

# 假设我们有这样的原始数据
df <- data.frame(
    id = c(1, 2),
    time_to_event = c(10, 15),      # 结局事件时间
    time_to_exposure = c(5, 8),     # 暴露事件时间
    ced_window = 3                  # 临床资格窗口时间
)

# 1. 首先收集所有关键时间点
event_times <- sort(unique(c(
    df$time_to_event,      # 结局事件时间: 10, 15
    df$time_to_exposure,   # 暴露事件时间: 5, 8
    df$ced_window          # 临床资格窗口: 3
)))
# event_times 结果为: c(3, 5, 8, 10, 15)

# 2. 对每个病人,根据这些时间点切分数据
# 以病人1为例,转换后的长格式数据:
df_long_patient1 <- data.frame(
    id = 1,
    # 时间区间
    t_start = c(0,  3,  5,  8, 10),    # 区间起点
    t_stop  = c(3,  5,  8, 10, 15),    # 区间终点
    
    # 在每个区间内的状态
    exposure = c(0,  0,  1,  1,  1),   # 0=未暴露, 1=暴露
    outcome  = c(0,  0,  0,  0,  1),   # 0=无事件, 1=有事件
    censor   = c(0,  0,  0,  0,  0)    # 0=未删失, 1=删失
)

时间区间设定的关键点:

  1. 关键时间点的来源:

    • time_to_event: 结局事件发生时间
    • time_to_exposure: 开始暴露的时间
    • ced_window: 临床资格窗口时间(通常是观察开始的一段时间)
  2. 区间划分原则:

    # 每个区间都是左闭右开的,例如:
    [0, 3)   # 观察开始到临床资格窗口
    [3, 5)   # 临床资格窗口到暴露时间
    [5, 8)   # 暴露后的第一个区间
    [8, 10)  # 暴露后到结局事件前
    [10, 15] # 结局事件发生的区间
    
  3. 状态记录规则:

    # exposure(暴露状态)
    - 在暴露时间点之前 = 0
    - 在暴露时间点之后 = 1
    
    # outcome(结局状态)
    - 在结局事件时间之前 = 0
    - 在结局事件发生时 = 1
    
    # censor(删失状态)
    - 未删失期间 = 0
    - 发生删失时 = 1
    
  4. 实际应用中的考虑:

    # 时间区间的特点
    - 每个区间的起点(t_start)是上一个区间的终点
    - 区间长度不需要相等
    - 区间数量取决于关键事件的发生次数
    
    # 状态变化的记录
    - 状态的改变只发生在区间的边界点
    - 每个区间内的状态保持不变
    - 可以精确捕捉状态改变的时间点
    

这种时间区间设定的优势:

  1. 可以精确追踪状态随时间的变化
  2. 便于计算时间依赖性的风险和概率
  3. 适合进行复杂的生存分析
  4. 能够处理时变协变量

这种设计使得我们可以:

  • 准确估计在任意时间点的风险
  • 计算累积风险和生存概率
  • 评估时间依赖性暴露的影响
  • 进行各种生存分析方法的建模

生成权重

现在,我们只需要生成权重。

clones_long_weights <- generate_ccw(clones_long, predvars = c("cov1", "cov2"))

head(clones_long_weights) |>
  DT::datatable(
    rownames = FALSE, 
    options = list(
      scrollX = TRUE,
      paging = FALSE
    )
  )

您还可以使用plot_ccw_over_time()可视化权重,使用plot_var_mean_over_time()可视化平均值。

clones_long_weights |>
  plot_ccw_over_time()
clones_long_weights |>
  plot_var_mean_over_time("cov1")

评估结果

我们现在拥有进行CCW分析所需的一切。例如,我们可以将东西组合在一起来评估手术与不手术的风险比:


  # 加载survival包,用于生存分析
  library(survival)

  # 创建分析数据集
  # 使用管道操作符依次处理数据
  df <- dummy_data |>
    # 使用create_clones()创建克隆数据集
    # id: 个体ID变量名
    # event: 事件变量名 
    # time_to_event: 到事件发生的时间变量名
    # exposure: 暴露变量名
    # time_to_exposure: 到暴露发生的时间变量名
    # ced_window: 设置时间窗口为100
    create_clones(
      id = 'id',
      event = 'event', 
      time_to_event = 'timetoevent',
      exposure = 'exposure',
      time_to_exposure = 'timetoexposure',
      ced_window = 100
    ) |>
    # 将克隆数据转换为长格式
    cast_clones_to_long() |>
    # 生成克隆权重,使用cov1和cov2作为预测变量
    generate_ccw(c('cov1', 'cov2'))

  # 使用Cox比例风险模型进行分析
  # Surv()创建生存对象,使用t_start和t_stop定义时间区间,outcome为事件状态
  # clone为主要预测变量
  # weights = weight_cox使用生成的克隆权重
  coxph(Surv(t_start, t_stop, outcome) ~ clone, data = df, weights = weight_cox)

请注意,我们在coxph()模型中使用了结果而不是事件。尽管如此,这种分析当然有问题,因为克隆过程会使方差无效。解决这个问题的最简单方法是引导方差。我还没有创建一个函数来做到这一点,但是把下面的例子作为如何做到这一点的例子。


# 加载boot包,用于进行Bootstrap分析
library(boot)

# 定义Bootstrap分析的主函数
# data: 输入的原始数据集
# indices: Bootstrap重抽样的索引
boot_cox <- function(data, indices) {
  
  # 使用Bootstrap样本创建带权重的长格式数据框
  # data[indices,]: 根据indices抽取Bootstrap样本
  ccw_df <- data[indices, ] |>
    # 创建克隆数据集
    create_clones(
      id = 'id',                    # 个体ID变量
      event = 'event',              # 事件状态变量
      time_to_event = 'timetoevent',# 到事件时间变量
      exposure = 'exposure',        # 暴露状态变量
      time_to_exposure = 'timetoexposure', # 到暴露时间变量
      ced_window = 100             # 时间窗口设为100
    ) |>
    # 转换为长格式数据
    cast_clones_to_long() |>
    # 生成克隆权重
    generate_ccw(c('cov1', 'cov2'))
    
  # 使用Cox比例风险模型计算风险比(HR)
  cox_ccw <- coxph(Surv(t_start, t_stop, outcome) ~ clone, 
                   data = ccw_df, 
                   weights = weight_cox) 
   
  # 提取并计算HR值
  hr <- cox_ccw |>
    coef() |>    # 提取系数
    exp()        # 取指数得到HR

  # 初始化输出向量
  out <- c("hr" = hr)

  # 分别为治疗组(clone=1)和对照组(clone=0)创建生存对象
  # 使用survfit函数拟合KM曲线
  surv_1 <- survfit(Surv(t_start, t_stop, outcome) ~ 1L, 
                    data = ccw_df[ccw_df$clone == 1, ], 
                    weights = weight_cox)

  surv_0 <- survfit(Surv(t_start, t_stop, outcome) ~ 1L, 
                    data = ccw_df[ccw_df$clone == 0, ], 
                    weights = weight_cox)

  # 计算限制平均生存时间(RMST)差异
  # 设定365天为截止时间点
  rmst_1 <- surv_1 |>
    summary(rmean = 365) |>           # 计算365天内的平均生存时间
    (\(summary) summary$table)() |>   # 提取结果表
    (\(table) table["rmean"])()       # 提取rmean值
  
  rmst_0 <- surv_0 |>
    summary(rmean = 365) |>
    (\(summary) summary$table)() |>
    (\(table) table["rmean"])()

  # 计算RMST差异
  rmst_diff <- rmst_1 - rmst_0

  # 将RMST差异添加到输出向量
  out <- c(out, "rmst_diff" = rmst_diff)

  # 计算1年生存率差异
  # 找到最接近365天的时间点索引
  index_1yr_1 <- which.min(abs(surv_1$time - 365))
  index_1yr_0 <- which.min(abs(surv_0$time - 365))

  # 获取1年生存概率
  surv_1_1yr <- surv_1$surv[index_1yr_1]  # 治疗组1年生存率
  surv_0_1yr <- surv_0$surv[index_1yr_0]  # 对照组1年生存率

  # 计算生存率差异
  surv_diff_1yr <- surv_1_1yr - surv_0_1yr

  # 将1年生存率差异添加到输出向量
  out <- c(out, "surv_diff_1yr" = surv_diff_1yr)

}

# 执行Bootstrap分析
# data: 使用dummy_data数据集
# statistic: 使用boot_cox函数
# R: Bootstrap重复次数为10次
boot_out <- boot(data = dummy_data, statistic = boot_cox, R = 10)

# 计算Cox回归系数的Bootstrap置信区间
boot.ci(boot_out,    # Bootstrap分析结果对象
        type = "norm",  # 使用正态分布方法计算置信区间
        index = 1)      # 提取第1个统计量(Cox回归系数)的置信区间

# 计算RMST差异的Bootstrap置信区间 
boot.ci(boot_out,    # Bootstrap分析结果对象
        type = "norm",  # 使用正态分布方法计算置信区间
        index = 2)      # 提取第2个统计量(RMST差异)的置信区间

# 计算1年生存率差异的Bootstrap置信区间
boot.ci(boot_out,    # Bootstrap分析结果对象
        type = "norm",  # 使用正态分布方法计算置信区间
        index = 3)      # 提取第3个统计量(1年生存率差异)的置信区间

竞争风险

将包功能扩展到竞争风险问题是微不足道的。我们将结果变量保留为整数,但添加了附加值。

dummy_data$competing <- sample(0:2, nrow(dummy_data), replace = TRUE)
head(dummy_data) |>
  DT::datatable(
    rownames = FALSE, 
    options = list(
      scrollX = TRUE,
      paging = FALSE
    )
  )
compete_ccw <- dummy_data |>
  create_clones(
    id = 'id',
    event = 'competing',
    time_to_event = 'timetoevent',
    exposure = 'exposure',
    time_to_exposure = 'timetoexposure',
    ced_window = 100
  ) |>
  cast_clones_to_long() |>
  generate_ccw(c('cov1', 'cov2'))

table(compete_ccw$outcome)

head(compete_ccw) |>
  DT::datatable(
    rownames = FALSE, 
    options = list(
      scrollX = TRUE,
      paging = FALSE
    )
  )

裁剪权重极端值

您可以使用winsorize_ccw_weights()函数修剪/取消排序极端权重:

compete_ccw_trim <- compete_ccw |>
  winsorize_ccw_weights(quantiles = c(0.10, 0.90))

max(compete_ccw$weight_cox)
max(compete_ccw_trim$weight_cox)

References

Hernán, Miguel A., and James M. Robins. “Using big data to emulate a target trial when a randomized trial is not available.” American journal of epidemiology 183.8 (2016): 758-764.

Gaber, Charles E., et al. “The Clone-Censor-Weight Method in Pharmacoepidemiologic Research: Foundations and Methodological Implementation.” Current Epidemiology Reports (2024): 1-11.

Maringe, Camille, et al. “Reflection on modern methods: trial emulation in the presence of immortal-time bias. Assessing the benefit of major surgery for elderly lung cancer patients using observational data.” International journal of epidemiology 49.5 (2020): 1719-1729.

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容