☰
多时间点生存ROC曲线R实现:timeROC包绘制SCI级AUC图表
2026/10/12 18:16:22 网站建设 项目流程

简介:面向医学、生物统计与临床试验中的生存分析场景,这份R语言源代码包用于绘制多时间点生存ROC曲线,解决传统ROC仅能评估单一时间点的局限,适合已有R基础并希望量化生存预测模型性能的研究者使用。压缩包体积仅13KB,包含3个文件:R脚本负责数据读入、AUC计算和曲线绘制,txt文件提供示例数据结构,PDF为输出效果参考,便于核对运行结果。当前已有351人学习下载。代码以pROC和survival包为基础,涵盖数据整理、不同随访时刻ROC曲线叠加、图例标注及AUC对比等关键环节,研究者只需准备包含生存时间、事件状态和风险评分的数据,即可替换示例文件直接运行,快速评估模型在多时间点的区分能力,为SCI论文中的预后分析图表提供规范、可复现的实现方案。该工具能显著减少重复编码工作量,帮助研究者将精力集中在模型解读与科研结论上。

1. 多时间点生存ROC曲线:这份R源代码能直接改改就用

手头有个临床预测模型的课题,模型训练完,审稿人一句话让补多时间点生存ROC曲线。常规做法是拿pROC在每个时间点单独画一条ROC,画完发现问题很大:删失数据根本绕不过去,每个时间点能用的样本量都不一样,画出来的AUC没法横向比。后来拆到这份R语言源代码资源包,里面就三个文件:input.txt(示例数据)、bioR43.multiTimeROC.R(主脚本)、ROC.pdf(效果参照)。脚本核心是调用timeROC包,一条命令算出多个时间点的AUC,再叠画到同一张图上,直接输出SCI能用的矢量图。这份资源适合已经有R基础、正在做生存分析模型验证的研究者,特别是要应付审稿人“补分析”场景的。拿到手不是让你从头写代码,而是把数据套进它的格式,改三行参数就能出图。

2. 从单时间ROC到多时间生存ROC:为什么必须用timeROC而不是pROC

2.1 生存ROC和普通ROC的本质区别:删失数据怎么处理

普通ROC曲线评估的是二分类问题,每个样本都有一个确定标签,0或1,样本之间独立,不存在“时间”维度。但生存ROC面对的是随访数据,每个样本只有两列关键信息:生存时间time和结局状态status。这里有个坑:随访结束时还没发生事件的样本,状态是删失(censored),它不代表“永远不发生”,只是“在观察窗口内没发生”。如果你硬把删失样本当成0类扔进pROC,等于默认这些人在随访期内绝对安全,结果会系统性低估模型性能。

多时间点生存ROC的解决思路是加权估计。在某个特定时间点t,把所有样本划分成两组:在t之前发生事件的人是一组,在t时刻仍然存活的人是另一组。但对删失样本来说,问题来了——有些人在t之前就失访了,我们不知道他到底什么时候发生事件。timeROC包的处理方法是IPCW(逆删失概率加权),给每个样本根据删失分布算一个权重,用加权后的伪样本去估计灵敏度(sensitivity)和特异度(specificity),从而得到t时刻的ROC曲线和AUC。这套逻辑在timeROC包的文档里写得很清楚,核心函数是timeROC()。

用pROC做多时间点ROC,等于把每个时间点单独拎出来做一次二分类,每个时间点的样本集还不一样,曲线之间没有一致性。timeROC则是在同一个队列、同一种删失结构下,用统一的加权框架估计每个时间点的AUC,这才具有横向可比性。这份R源代码选择timeROC方案,从方法学上是站得住的。如果你投的期刊有统计审稿人,用pROC逐点画图很容易被追问“删失样本怎么处理的”,用timeROC自带IPCW权重,至少在方法描述上无懈可击。

2.2 数据格式是第一道坎:先把手里的表改造成生存数据框

解压打开input.txt,内容是一张典型的纵向随访表格。我按经验整理一下这个格式里最关键的列,对应关系如下表。

列名类型格式要求说明
time数值单位统一为天/月/年生存时间,从入组到事件发生或随访截止
status数值0或1,必须数值型1=事件发生(死亡/复发/转移),0=删失或随访结束未事件
marker数值连续变量模型预测值,比如风险评分、蛋白表达值、signature得分
ID字符/数值不与他人重复非必需,但建议保留以便核查

实际操作里,大多数人的原始数据长这样:OS_time(总生存时间)、OS_event(是否死亡事件)、risk_score(模型输出的预测风险)。拿到这份资源后,第一步不是跑脚本,而是把这三列改名为time、status、marker,再检查status是不是0/1编码——很多人用"Dead"/"Alive"字符串,这个后面会报错。

# 读取临床数据,假设你的文件叫 clin.csv clin <- read.csv("clin.csv", stringsAsFactors = FALSE) # 检查核心三列的缺失情况和类型 summary(clin$time) table(clin$status) # 确认只有0和1,且1代表事件发生 # 如果status是字符串,手动转成0/1 clin$status <- ifelse(clin$status == "Dead", 1, 0) # 按资源包的格式约定列名 clin <- clin[, c("time", "status", "marker")] clin <- clin[complete.cases(clin), ] # 去掉缺失行 str(clin)

逻辑说明:这段代码本质是把你的临床原始表标准化成input.txt那种三列结构。summary、table、str三个检查是为了避免后面timeROC函数在运行中途报错。complete.cases这一步很重要——timeROC对缺失值默认是直接报错或者整行剔除,与其让它自动丢,不如自己在读数据时把处理逻辑写明白,知道哪些样本被排除了。

参数说明:status里事件发生必须记为1,删失记为0。如果你的数据反着来,AUC会和预期完全相反——所有曲线都跑到对角线下方,那是因为事件和删失的编码颠倒了。时间是数值单位,混入日期格式(如"2023-01-01")会直接解析失败,建议入组前先统一换算成月或天。

3. 把脚本跑起来:从timeROC核心参数到SCI级别的ROC.pdf

3.1 主脚本解读:bioR43.multiTimeROC.R做了哪四件事

解压看主脚本,逻辑很清晰,拆开就是四步:读取数据、调用timeROC计算多时间点AUC、把多条ROC曲线叠画到一张图、输出PDF。下面这段代码是我按原资源包的功能结构重写的可运行版本,保留了核心调用方式,你拿到原脚本后对着看,结构是一一对应的。

# ===== bioR43.multiTimeROC.R 核心逻辑 ===== # 加载依赖包 library(survival) library(timeROC) # 1. 读取示例数据 dat <- read.table("input.txt", header = TRUE, sep = "\t") # 确认数据结构 head(dat) # 2. 定义需要评估的时间点,单位跟time列一致 # 常见做法:取随访时间的四分位数,或者根据临床意义选择 eval_times <- c(12, 24, 36) # 12/24/36个月 # 3. 计算多时间点AUC res <- timeROC( T = dat$time, # 生存时间 delta = dat$status, # 事件状态:1=事件,0=删失 marker = dat$marker, # 模型预测值 cause = 1, # 竞争风险下指定关注的事件类型 times = eval_times, # 评估时间点 iid = TRUE, # 是否计算iid分解,用于置信区间 weighted = TRUE # 是否做IPCW加权,生存ROC必须TRUE ) # 4. 查看各时间点AUC和置信区间 print(res$AUC) print(confint(res, level = 0.95)$CI_vect) # 5. 生成PDF,同图画多条ROC曲线 pdf("ROC.pdf", width = 7, height = 6) # 用第一个时间点建画布,后续时间点叠加lines plot(res, time = eval_times[1], col = "#C00000", lwd = 2, title = "Multi-time ROC Curve") for (i in 2:length(eval_times)) { lines(res, time = eval_times[i], col = i + 1, lwd = 2) } # 添加对角线参考 abline(a = 0, b = 1, lty = 2, col = "gray50") # 添加图例,位置根据曲线分布选"bottomright"或"topleft" legend("bottomright", legend = paste0("AUC at ", eval_times, " months = ", round(res$AUC, 3)), col = c("#C00000", 3:(length(eval_times) + 1)), lwd = 2, bty = "n") dev.off()

逻辑说明:脚本的骨架是timeROC()函数这一次调用,它同时算出了times向量里所有时间点的AUC、标准误、置信区间,并保存了画ROC曲线所需的全部坐标数据。iid=TRUE是计算渐近分布的标准做法,没有它confint()算不出区间估计。weighted=TRUE是生存ROC生效的关键,等于启用IPCW加权,这是和普通ROC最本质的区别。可视化部分先plot()第一个时间点的ROC作为底,再用lines()叠加其余时间点的曲线,不同颜色区分。

参数说明:cause=1在存在竞争风险事件时指定“关注哪个事件”,比如复发和死亡都是终点事件,你想单独评估“复发”这个终点就设cause=1;如果只有单一终点,保持默认1即可。eval_times的选择有讲究——不能等距拍脑袋设,一般取随访时间的25%、50%、75%分位数,或者按临床意义定(比如肿瘤研究常取1年、3年、5年,对应12/36/60个月)。注意times最大值不能超过最大随访时间,否则那个时间点的AUC会报错或者返回NA,后面避坑章节会细说。

3.2 输出物检查:怎么判断ROC.pdf是靠谱的还是翻车了

跑完代码之后,别急着关RStudio,先看两个东西。第一是控制台打印的res$AUC,第二个是confint输出的置信区间。

# 输出多时间点AUC汇总表 auc_summary <- data.frame( time_point = eval_times, AUC = round(res$AUC, 3), lower_95 = round(confint(res, level = 0.95)$CI_vect[, 1], 3), upper_95 = round(confint(res, level = 0.95)$CI_vect[, 2], 3) ) print(auc_summary) write.csv(auc_summary, "multi-time_ROC_AUC_summary.csv", row.names = FALSE)

逻辑说明:这段是论文方法的补充材料常客——AUC_summary.csv会作为补充表格放进SCI论文附件。confint()返回的是一个矩阵,CI_vect是置信区间矩阵,行列对应每个时间点,取第1列为下限,第2列为上限。

除了数字,还要打开ROC.pdf看图。正常的图应该满足三个条件:每条曲线都从左上角(高灵敏度高特异度区域)出发、整体在对角线上方、不同时间点的曲线能明显区分开且AUC大小关系和临床预期一致。如果看到某条曲线贴着对角线走,说明该时间点模型预测能力接近随机猜,这是正常的分析结果,如实报告就好;如果曲线大面积在对角线下方,那不是模型差,而是代码里status编码反了,回到第2章改回来重跑。

判断完了,再做一步进阶验证:画时间- AUC曲线。这是评估模型随时间变化稳定性的常用补充图,timeROC包也直接支持。

# 连续评估多个整月时间点,观察AUC的时间趋势 continuous_times <- seq(from = 6, to = max(eval_times) * 1.2, by = 6) res_con <- timeROC( T = dat$time, delta = dat$status, marker = dat$marker, cause = 1, times = continuous_times, iid = TRUE, weighted = TRUE ) pdf("AUC_over_time.pdf", width = 7, height = 5) plot(res_con$times, res_con$AUC, type = "b", pch = 19, col = "#C00000", xlab = "Time (months)", ylab = "AUC", main = "AUC over Time", ylim = c(0.4, 1)) dev.off()

逻辑说明:这段把离散的几个时间点扩展成每6个月一个的连续序列,得到一条“AUC随时间变化”的折线,能看出模型在早期、中期、晚期的区分能力是上升、下降还是平稳。临床上如果AUC随随访时间延长而上升,说明模型对远期事件的预测更好;如果早期高后期掉得厉害,说明模型对近期高风险人群敏感,远期能力弱。这个图可以作为正文图2或补充材料提交。

参数说明:seq(from=6, to=max(eval_times)*1.2, by=6)里的by=6代表每隔6个月计算一次,时间单位是月;如果你的数据单位是天,要改成by=180这类对应值。ylim范围视实际情况调整,别硬套c(0.4, 1)。

4. 避坑指南:多时间点生存ROC的六个常见问题

4.1 报错“times必须小于最大随访时间”或AUC显示NA

现象:代码运行到timeROC()时报错,提示时间点向量里有值大于最大观察时间;或者没报错,但某个时间点的AUC返回NaN/NA。

原因:timeROC要求评估时间点必须落在随访时间范围内。随访时间最长的样本是60个月,你设了72个月作为评估点,在72个月这个时点,全队列里没有任何人达到“观察满72个月且未删失”的状态,权重估计失去支撑,结果自然无法计算。

解决:先跑summary(dat$time)看最大随访时间,以最大随访时间tmax为上限。时间点不要正好卡在最大值上,留出一点余量。

# 自查最大随访时间,以此为上限设置评估点 t_max <- max(dat$time, na.rm = TRUE) # 合理做法:取t_max的70%-90%作为最晚评估点 eval_times <- quantile(dat$time, probs = c(0.25, 0.5, 0.75), na.rm = TRUE) eval_times <- round(eval_times, 1) # 取整方便报告 eval_times <- eval_times[eval_times < t_max * 0.9] # 剔除接近最大值的点 print(eval_times)

逻辑说明:以分位数作为评估点是多个R包教程里的通行做法,能避免人为拍脑袋选的随意性,也保证每个时间点都有足够样本支撑。剔除接近tmax的点是为了防止删失比例过高时AUC估计方差过大——时间点越靠后,剩余风险集越小,置信区间越宽,画出来的图说服力越弱。

4.2 报错“delta必须为数值向量”或曲线全部在对角线下方

现象:timeROC()报错提示delta类型不对,或者图能出来但曲线都在对角线下面。

原因:status列在R里被读成了字符型或因子型,比如"1"、"0"带引号,或者用了"Dead"/"Alive"。字符被强制转数值时会出错。曲线翻转到对角线下方这事,原因基本只有一种:status编码记反了——事件=0,删失=1。

解决:读数据时用stringsAsFactors=FALSE,再做一步显式转换加检查。

# 显式把status转成数值型0/1 dat$status <- as.numeric(as.character(dat$status)) stopifnot(all(dat$status %in% c(0, 1))) # 不是0/1组合直接中断 # 交叉验证:事件样本的time应该更短才算合理 aggregate(time ~ status, data = dat, FUN = median)

逻辑说明:stopifnot是安全的强制检查口,条件不满足直接抛出异常,避免带着脏数据往下跑。aggregate那行是验证编码方向是否符合临床直觉——按常理,事件组的生存时间中位数应该显著短于删失组,如果反了,八成是status定义反了。

4.3 加载timeROC包报错“程辑包不存在”或依赖缺失

现象:library(timeROC)提示不存在这个包,或者install.packages("timeROC")报了“无法连接到CRAN仓库”“依赖包不存在”之类的问题。

原因:要么是R版本太老或镜像源没配置好,要么是缺少底层依赖包(它依赖survival和一些基础工具包)。R包的安装报错信息经常不直接说明缺的是谁,只给一个像“不存在叫‘xx程辑包’这个名字”的提示,排查起来确实玄学。

解决:先设CRAN镜像,再装依赖,最后装timeROC。别直接裸敲install.packages("timeROC")。

# 设置国内镜像,兼容部分网络环境下CRAN连接不稳定的情况 options(repos = c(CRAN = "https://mirrors.tuna.tsinghua.edu.cn/CRAN/")) # 先确保依赖包就位 install.packages(c("survival", "pROC")) # pROC非必需但画图时常用 # 再装timeROC install.packages("timeROC") # 装完验证能否正常加载 library(timeROC)

逻辑说明:R包安装报错90%是镜像源问题和依赖缺失,先设镜像再装依赖是通用解法。pROC顺手装上是储备,后续如果要画传统二分类ROC对比,不用再装一次。

4.4 图例把ROC曲线压住了,或不显示AUC数值

现象:图明显比例失衡,图例框太大,把右下角的ROC曲线遮住;或者图例的AUC数字远到看不见。

原因:legend的位置参数设成了固定的"bottomright",但实际曲线恰好也聚集在右下角。生存ROC曲线常见形态是陡峭的左上角加平缓的右下拐,右下角通常有曲线穿过,图例一放就压线。

解决:看图例位置,改成"topleft";如果还挡,手动用inset微调,配合segments加AUC标签。

# 动态判断图例放哪边,以第一个时间点的曲线坐标为参考 library(prodlim) # timeROC依赖包,内含plot方法 legend_pos <- ifelse(mean(res$TP[1, ]) > 0.5, "bottomright", "topleft") legend(legend_pos, legend = paste0("t = ", eval_times, "m, AUC = ", round(res$AUC, 2)), col = c("#C00000", 3:(length(eval_times) + 1)), lwd = 2, bty = "n", cex = 0.9)

逻辑说明:res$TP是timeROC结果里的灵敏度矩阵,第一行对应第一个时间点。用它在图上的平均高度判断曲线偏向图的上半区还是下半区,决定图例落在哪个角。cex=0.9缩小字号也能腾出空间。bty="n"去掉图例边框,整体更干净,这在SCI图上更常见。

4.5 多时间点曲线颜色太接近,打印后分不清

现象:颜色是默认的2,3,4号色,在屏幕上能看清,但论文打印成黑白稿后,3号绿色和4号蓝色几乎无法区分。

解决:自主定义一组色彩对比度高、色盲友好的配色,并在画图时用col_vec统一管理。

# 色盲友好的配色方案,按时间点顺序排列 col_vec <- c("#C00000", "#0072B2", "#E69F00", "#009E73", "#999999") # 使用时截取前n个颜色 plot_cols <- col_vec[1:length(eval_times)]

逻辑说明:#C00000是深红、#0072B2是中蓝、#E69F00是橙色,这组方案在多本可视化书籍里被验证过在色盲人群中和黑白打印下都能区分。

4.6 变量列名带空格或特殊字符,脚本运行到一半报错

现象:自己导入Excel导出的CSV,列名是"OS time(月)"、"Event(死亡=1)"这种带空格、中文、括号的格式,脚本里用dat$OS time取列报语法错误,或者timeROC()提示找不到列。

原因:R中列名含空格必须用反引号包裹访问,脚本是按input.txt的简洁列名写的,拿到带复杂列名的表直接套用必然翻车。

解决:读取后统一改列名。

dat <- read.csv("your_data.csv", check.names = FALSE) # 保留原列名以防中文乱码 names(dat) <- c("time", "status", "marker") # 按位置重命名前三列

逻辑说明:check.names=FALSE防止R自动把列名里的空格改成语点,但用完后立刻手动重命名,一举两得:既避免原列名读取时的错位,又让后续代码能直接访问dat$time。

5. 进阶玩法:把这份源代码改造成批量分析工具

5.1 批量处理多个模型:ROC全家桶一键生成

你手里大概率不止一个模型要验证。比如你做了COX回归风险评分、Lasso筛选后的signature、还有单个biomarker表达量,理论上每组都要出一套ROC图。目标是把bioR43.multiTimeROC.R从“单个模型专用”改造成“多模型通吃”。

# 封装成函数,一个模型一套结果 run_multitime_roc <- function(data, marker_col, time_col = "time", status_col = "status", eval_times = c(12, 24, 36), pdf_prefix = "ROC") { res <- timeROC( T = data[[time_col]], delta = data[[status_col]], marker = data[[marker_col]], cause = 1, times = eval_times, iid = TRUE, weighted = TRUE ) # 输出单模型AUC表 ci <- confint(res, level = 0.95)$CI_vect out <- data.frame( marker = marker_col, time = eval_times, auc = round(res$AUC, 3), lower = round(ci[, 1], 3), upper = round(ci[, 2], 3) ) # 画图 pdf(paste0(pdf_prefix, "_", marker_col, ".pdf"), width = 6.5, height = 5.5) plot(res, time = eval_times[1], col = "#C00000", lwd = 2, title = paste0("ROC: ", marker_col)) for (i in 2:length(eval_times)) { lines(res, time = eval_times[i], col = c("#0072B2", "#E69F00")[i - 1], lwd = 2) } abline(a = 0, b = 1, lty = 2, col = "gray50") legend("bottomright", legend = paste0(eval_times, "m AUC=", round(res$AUC, 2)), col = c("#C00000", "#0072B2", "#E69F00")[1:length(eval_times)], lwd = 2, bty = "n") dev.off() return(out) } # 对三个候选模型批量执行 models <- c("risk_score", "lncRNA_sig", "TNMB_stage") results <- do.call(rbind, lapply(models, function(m) { run_multitime_roc(dat, marker_col = m) })) print(results)

逻辑说明:函数里data[[marker_col]]用双中括号取动态列名,这是批量处理的关键写法。do.call(rbind, lapply(...))是R里批量处理返回数据框的标准组合拳,能把多个模型的AUC汇总成一张表。pdf_prefix参数控制输出文件名,避免多个模型覆盖同一个PDF。

参数说明:marker_col可以传入任意列名,包括基因表达量、风险评分、分期分组(只要转成数值)。组别数据如TNM分期,可以转成有序数值1/2/3/4当marker用,逻辑上是把分期当作等级变量评估其预测能力。

5.2 把AUC对比结果写进论文:参数和方法描述的参考写法

做完分析总要写到论文里,方法学部分和结果部分的写法是有套路的。我拆过的项目里,评审对ROC分析最看重的就是三点:时间点怎么选的、删失怎么处理的、置信区间有没有。下面是一个可以直接改的模板。

多时间点ROC曲线用于评估各模型在不同随访时间点的预测区分度。评估时间点设定为随访时间的25%(12个月)、50%(24个月)和75%(36个月)分位数。采用timeROC包基于逆删失概率加权(IPCW)估计各时间点的AUC及其95%置信区间。结果显示,风险评分在12、24、36个月的AUC分别为0.82(95%CI: 0.75-0.89)、0.78(95%CI: 0.71-0.85)和0.74(95%CI: 0.66-0.82),区分度随时间略有下降。

这里AUC数值是我编的示意,你替换成自己的真实结果即可。注意写法要点:时间点的选择依据必须写清楚(分位数?临床节点?);一句话点出IPCW加权;AUC结果按时间顺序报,同时给置信区间;最后加一句时间趋势描述。这个模板能应对大多数临床SCI期刊的方法学描述要求。

再补充一个技巧:如果三个模型在同一队列上比较,光看各模型的AUC不够,最好把三条曲线画在同一张图上做视觉比较。做法是改一下第3章的plot部分,多循环一层模型。

# 多模型在同一图上的比较 pdf("multi_model_compare.pdf", width = 7, height = 6) plot(res_1, time = 24, col = "#C00000", lwd = 2, title = "Model Comparison at 24 Months") lines(res_2, time = 24, col = "#0072B2", lwd = 2) lines(res_3, time = 24, col = "#E69F00", lwd = 2) legend("bottomright", legend = c("Risk score (AUC=0.78)", "lncRNA sig (AUC=0.83)", "TNM stage (AUC=0.65)"), col = c("#C00000", "#0072B2", "#E69F00"), lwd = 2, bty = "n") dev.off()

逻辑说明:这个图的信息密度比多时间点曲线更高——同一时间点下,多模型的区分能力一目了然,审稿人不需要自己去对比三个子图。实际操作中,做多模型比较时固定时间点为中位随访时间或临床最关注的节点(如肿瘤研究的3年或5年生存率节点),更有说服力。

参数说明:比较图里res_1、res_2、res_3是分别用timeROC()对三个模型在同一个24个月时间点算出的结果对象。运行前务必确保三个模型的marker列和time/status列都对齐,数据框排序一致,避免错配。

5.3 一个经验教训:AUC是点估计,置信区间才是真相

拆这个资源包的过程中,我用模拟数据做过一次测试:把样本量砍到100例,分组做多时间点ROC,某个时间点的AUC点估计是0.72,但95%置信区间是0.51到0.93——一个区间横跨了0.5这条“随机猜测”线。如果不看置信区间,单报0.72,论文写出来是有隐患的。从那以后我每次跑多时间点ROC,都会强制走一遍:先summary确认时间范围,再用confint核对各时间点的置信区间,最后才画图写结果。这份源代码帮你省掉了从零写包的工作量,但统计报告的底线仍然在自己手里,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询