每次拿到诊断试验的数据,我几乎不加思考地打开R,敲三行代码:
library(pROC) roc1 <- roc(data$disease, data$marker) plot(roc1)这就是pROC包的核心用法。搞了这些年生物统计和临床预测模型,ROC曲线始终是跟审稿人、临床医生打交道时最有说服力的图表之一,而R里的pROC包,凭借它的AUC计算、阈值筛选、置信区间和图形定制能力,已经成为我日常分析中绕不开的工具。
pROC能做的不只是把曲线画出来,它还悄悄解决了一个新手最头疼的问题:到底哪些参数控制着什么?为什么别人画的图横坐标是1-特异性,我画出来的却直接是特异性?AUC怎么标到图上?置信区间怎么画成那条经典的“带子”?这篇文章就把pROC绘制ROC曲线涉及的各类参数掰开揉碎,讲清楚每个参数的作用、背后的计算逻辑,以及实际使用中容易被忽略的细节。不管你是刚接触ROC曲线的新手,还是已经能熟练跑通基础代码、想进一步调整图形细节的进阶用户,看完应该都能找到对应的答案。
1. 为什么是pROC:不是唯一选项,但最顺手
1.1 pROC包的两个核心优势
R语言里能画ROC曲线的包其实不少,ROCR、plotROC、cutpointr这些我都试过。但pROC真正打动我的点有两个:一是它把“计算指标”和“绘图”这件事解耦得很干净,同一个roc对象既可以用来提取AUC、置信区间、约登指数,又可以反复加工成不同风格的图;二是它对阈值处理非常细,能够输出每个点对应的灵敏度、特异度、阳性预测值、阴性预测值等完整信息,这在做诊断试验报告时太重要了。
另一个很实际的原因是pROC的底层计算经过优化,跑大数据集也不费劲。之前我处理过一个上万样本的体检数据,用ROCR算AUC几乎秒出,但想同时输出一组置信区间就得手动写循环;换成pROC,一个ci函数全搞定。这节省的时间是实打实的。
1.2 先建立两个基本认知
在用参数之前,先理解pROC包的基本结构。其实所有操作都围绕两个层面:第一个层面是创建roc对象,也就是你调用roc()函数去拟合一条ROC曲线;第二个层面是对这个对象做后续操作,包括plot()绘图、ci()计算置信区间、coords()提取阈值坐标、roc.test()做AUC比较等。
理解这一点很重要,因为“查参数”时要分清是查哪一层。很多人拿着网络上的代码,看到plot(roc1, print.auc=TRUE)能跑通,就以为print.auc是roc函数的参数,其实不是。print.auc是plot.roc方法识别的绘图参数。同样的名字在不同函数里可能含义不同,这点后面会反复提到。
2. roc()函数参数详解:先把曲线“算”对
2.1 数据输入三件套
roc()最基础的调用方式是直接传两个向量,一个是二分类结果变量response,另一个是连续或有序的预测变量predictor。比如:
result <- roc(response = mydata$status, predictor = mydata$score)这里有两个容易被忽略的点。response不一定要是0/1格式,它可以是因子、字符,甚至逻辑值。pROC会自动识别其中两个类别作为正负类。但如果你传入的response有3个及以上水平,函数会直接报错,它会很直白地告诉你需要“仅两类”的数据。
再来说predictor,这个通常是连续值,比如某蛋白浓度、临床评分、风险预测概率。它的数值大小本身没有限制,pROC会根据方向参数自动判断。
当数据量比较大的时候,我更喜欢用带data参数的写法,尤其是配合公式接口:
result <- roc(outcome ~ marker, data = dat, levels = c("control", "case"), direction = ">")formula写法让你的代码更可读,也免去了重复写数据集名称的麻烦。如果你是从Excel或者CSV读进来的数据,变量名跟数据框直接绑定,用这种方式最顺手。
2.2 levels与direction:最容易搞错的两个参数
先看levels。它的作用是明确告诉pROC哪个类别是“阳性组”,哪个是“阴性组”。默认情况下,pROC会把两个类别按因子水平或字符串排序,第一个当作阴性对照,第二个当作阳性病例。
但实际数据里顺序经常不是你想要的样子。比如你的数据里"disease"排在"healthy"前面,不指定levels,AUC就可能变成小于0.5的倒挂结果。这时候要么在原始数据里设置因子顺序,要么直接在这里指定:
result <- roc(data$status, data$value, levels = c("healthy", "disease"))levels的顺序是“先阴性,后阳性”。
direction参数同样关键。它表示预测变量取值较大时,是否更倾向于判定为阳性。默认是"auto",让pROC自己选一个方向让AUC大于0.5。你可以手动指定为">"或者"<":
result <- roc(data$status, data$value, direction = ">")这里的逻辑是:如果某个指标越大越提示患病,就用">"。比如肿瘤标志物CEA、PSA这类,浓度越高越可疑,就用">"。反过来,如果某个指标越小越危险,比如某些保护性因子,就用"<"。
提示:方向设反了,AUC会变成1减去真实AUC。比如真实AUC是0.85,方向反了你会算出0.15。所以画图前如果发现AUC离谱到小于0.5,第一反应就是检查direction。
2.3 smooth参数:什么时候需要平滑曲线
smooth是pROC里一个让我又爱又恨的参数。默认smooth=FALSE,此时曲线是“经验ROC”,也就是直接用真实数据算出的灵敏度-特异度折线。如果数据量足够大,比如上千例,经验曲线已经很光滑,不需要平滑。
但当样本量小,比如只有三五十例时,曲线会呈现明显的阶梯状,看起来很不专业。这时可以设置:
result_smooth <- roc(data$status, data$value, smooth = TRUE)smooth=TRUE会利用核密度估计对原始数据拟合平滑曲线。它的优点是图形好看,AUC也会相应变化;缺点是这条曲线不再严格对应真实观测数据,相当于做了一次非参数拟合。在做正式统计报告时,我会同时报告两条AUC,一条是经验AUC,一条是平滑AUC,让审稿人知道结果稳健。
要特别提醒的是,smooth=TRUE之后,某些后续函数会有所不同。比如coords()提取最佳阈值时,阈值坐标和原始指标刻度可能不再一一对应;ci()置信区间也可能表现出更平滑但更宽的区间。
2.4 计算相关参数:auc、ci、percent、quiet
roc()里还提供了一组控制计算行为的参数。auc是一个逻辑值或数字,默认TRUE,即计算AUC。你可以直接传一个数字进去强制指定AUC,比如auc=0.85,但这种情况基本只在画模拟曲线或重整别人数据时才用。
ci参数用于在拟合时直接计算AUC的置信区间:
result_ci <- roc(data$status, data$value, ci = TRUE)这会用DeLong方法快速计算置信区间。注意,ci=TRUE只带来自AUC的置信区间,不带灵敏度、特异度逐点置信区间,后者需要单独用ci.se或者ci.sp函数处理。
percent参数不太常用但很好玩。设成TRUE后,AUC、灵敏度、特异度等指标全部以百分比形式显示:
result_pct <- roc(data$status, data$value, percent = TRUE)这时AUC不再是0.879,而是87.9。部分外文期刊要求ROC报告中用百分比格式,这个参数就可以直接满足。
quiet是一个容易被忽略的细节。当你的数据方向是自动判断的,或者类别顺序有些模糊,pROC会在控制台输出一大堆提示信息,包括“Setting levels: control = ..., case = ...”之类的说明。这些提示在交互式分析时有帮助,但当你写脚本批量处理几十个指标时就很烦人。设成quiet=TRUE,统统闭嘴。
3. plot相关参数详解:把图画到能发表的水平
3.1 基础绘图参数:col、lwd、lty
创建roc对象之后,绘图的核心函数是plot()。因为pROC给这个对象定义了专门的plot方法,所以直接调用plot(roc1)就会触发pROC的绘图逻辑。
颜色、线宽、线型是控制视觉风格的三大参数,几乎每张图都要用:
plot(roc1, col = "steelblue", lwd = 2, lty = 1)col接受R内置颜色,也可以传十六进制色值,比如"#2E86AB"。lwd控制线条粗细,数值越大越粗,一般2到3比较合适,太粗会遮住置信区间带。lty是线型,1是实线,2是虚线,3是点线,多曲线对比时可以用来区分。
我在实际操作中的习惯是:主曲线用实线加粗,如果有多条曲线,其余曲线用不同颜色加不同线型,比如——主曲线col="darkred", lwd=2.5, lty=1;第二条col="navy", lwd=2, lty=2;第三条col="darkgreen", lwd=2, lty=3。这样黑白打印时也能区分。
3.2 坐标轴方向与对角线:legacy.axes和identity
我第一次用pROC绘图时,最困惑的就是横坐标。默认情况下,pROC把横坐标画成“特异性”,从左往右是1到0。而更为流传的ROC图样式是横坐标为“1-特异性”,从左往右是0到1,也就是传说中sensitivity against 1-specificity形式。这两种都是对的,只是历史传统不同。
要统一成更常见的样式,就用:
plot(roc1, legacy.axes = TRUE)设成TRUE后,横坐标自动换成1-特异性,方向从左往右递增。如果你的报告需要与传统诊断试验文献一致,强烈建议加上这个参数。另外,对角线用identity参数控制:
plot(roc1, legacy.axes = TRUE, identity = TRUE)identity=TRUE会画出一条从(0,0)到(1,1)的45度参考线,对应的就是AUC=0.5的无区分能力线。identity.lty和identity.col可以进一步控制这条线的样式,默认是虚线。
3.3 在图上展示AUC与置信区间:print.auc、print.ci
AUC是ROC曲线的灵魂,画图时我通常直接在图上标注:
plot(roc1, print.auc = TRUE)默认情况下它会把AUC值放在图内右下角。如果想要放在图例区域,或者调整位置,可以用print.auc.x和print.auc.y手动指定坐标:
plot(roc1, print.auc = TRUE, print.auc.x = 0.4, print.auc.y = 0.5)print.auc还支持格式化格式,比如:
plot(roc1, print.auc = TRUE, print.auc.adj = c(-0.5, 1), print.auc.col = "black")print.ci是另一个常用参数。当roc对象已经带有置信区间信息,比如用ci=TRUE创建的,加上print.ci=TRUE就能在图上把区间标出来:
plot(roc1, print.auc = TRUE, print.ci = TRUE)比如图上会显示“AUC: 0.879 (95% CI 0.821-0.937)”这样的信息。
3.4 阈值点标注:print.thres与print.thres.pch
有时你想在曲线上标出某个特定阈值对应的点,尤其在寻找最佳切点时非常有用:
plot(roc1, print.thres = TRUE)print.thres=TRUE会把所有计算过的阈值点都标注出来,对于数据量大的情况会比较杂乱。更优雅的做法是指定最佳阈值:
thr <- coords(roc1, "best", ret = "threshold") plot(roc1, print.thres = thr, print.thres.pch = 20, print.thres.cex = 1.2)print.thres.pch控制点的形状,19或20是实心圆点,视觉上最清晰。print.thres.cex控制点的大小。print.thres.col和print.thres.adj可以分别控制标注文字的颜色和位置偏移。这个组合在做“找最佳cutoff”的项目里几乎是标配。
4. 实操示例:从模拟数据到完整成图
4.1 准备模拟数据
为了把前面的参数串起来,我用模拟数据走一遍完整流程。这里生成100例观察数据,其中50例为阴性组,50例为阳性组。预测变量marker服从正态分布,两组均值设成有明显的差异:
set.seed(2024) dat <- data.frame( status = factor(c(rep("healthy", 50), rep("disease", 50))), marker = c(rnorm(50, mean = 2.0, sd = 1), rnorm(50, mean = 3.2, sd = 1)) ) str(dat)这里我故意把healthy放在前面,disease放在后面,为的是演示levels参数的必要性。先看一下数据分布,确认marker在两组间确实有区分度。
4.2 拟合roc对象并计算关键指标
先用显式方式创建roc对象:
roc1 <- roc(dat$status, dat$marker, levels = c("healthy", "disease"), direction = ">", smooth = FALSE, ci = TRUE, quiet = TRUE)看一下这个对象输出了什么:
roc1屏幕上会显示Area under the curve: 0.842,以及置信区间。初步判断,marker有一定的诊断价值。
接下来提取最佳阈值,这里用约登指数为准则,也就是让灵敏度与特异度之和最大的阈值:
best_thr <- coords(roc1, "best", ret = c("threshold", "sensitivity", "specificity", "ppv", "npv")) print(best_thr)coords里的"best"会自动基于约登指数取阈值,同时返回对应的灵敏度、特异度、阳性预测值和阴性预测值。输出看到的阈值对应的marker值,就是临床实践中可以采用的切点。
4.3 绘制并美化ROC曲线
现在把这张图按可发表的规格画出来:
plot(roc1, col = "#2E86AB", lwd = 2.5, legacy.axes = TRUE, identity = TRUE, grid = TRUE, print.auc = TRUE, print.ci = TRUE, print.thres = best_thr[["threshold"]], print.thres.pch = 20, print.thres.cex = 1.2)这张图里包含了:
- 曲线本身,深蓝色,线宽2.5,视觉上很清晰
- 横坐标采用1-特异性,符合主流文献习惯
- 带对角线作为参考
- AUC和95%置信区间直接标注在图上
- 用实心圆点标出了最佳阈值位置
如果你要进一步调整坐标轴标签,可以用xlab和ylab参数:
plot(roc1, xlab = "1 - Specificity", ylab = "Sensitivity")觉得默认字体小,也可以加cex.lab、cex.axis、cex.main分别控制轴标签和轴刻度文字大小。
4.4 多条ROC曲线叠加对比
实际项目里很少只评估一个指标。更多情形是:比较两个标记物,或者比较一个新模型跟一个旧模型。pROC做多条曲线叠加非常简单:
roc1 <- roc(dat$status, dat$marker, levels = c("healthy", "disease"), direction = ">", quiet = TRUE) dat$marker2 <- c(rnorm(50, mean = 2.5, sd = 1), rnorm(50, mean = 3.5, sd = 1)) roc2 <- roc(dat$status, dat$marker2, levels = c("healthy", "disease"), direction = ">", quiet = TRUE) plot(roc1, col = "#D7263D", lwd = 2, legacy.axes = TRUE) plot(roc2, col = "#1B998B", lwd = 2, legacy.axes = TRUE, add = TRUE)关键是第二条曲线要加上add=TRUE,否则会覆盖掉第一张图。之后可以自己加legend说明:
legend("bottomright", legend = c("Marker 1 (AUC 0.842)", "Marker 2 (AUC 0.913)"), col = c("#D7263D", "#1B998B"), lwd = 2)legend里我习惯直接注明AUC,这样一眼就能看出哪个指标区分能力更强。
4.5 用置信区间带展示不确定性
除了在角落标注CI,pROC还支持把灵敏度和特异度的置信区间画成一条带状区域。这是一张高质量ROC曲线常见的加分项:
plot(roc1, legacy.axes = TRUE, col = "#2E86AB", lwd = 2) ci_obj <- ci.se(roc1, specificities = seq(0, 1, l = 50)) plot(ci_obj, type = "shape", col = "#2E86AB")这里ci.se计算的是每个特异性水平下灵敏度的置信区间。plot(ci_obj, type="shape")会把CI区域以半透明色带画到已有图上,效果类似误差带。也可以用type="bars"在每个点上画竖线,但图形会更杂乱。
这个细节在样本量较小的研究里特别有价值。因为仅看一条线,很容易高估临床标志物的稳定性;带上区间带,读者对估计的不确定性就一目了然。
5. 高频问题与排查口诀
5.1 AUC小于0.5怎么办
这是出现次数最多的问题。AUC算出来0.3、0.35,排除代码错误后,绝大多数是方向反了。检查顺序:
- 检查levels是否把健康的和患病的顺序倒置了
- 检查direction是否正确
- 查一下两组marker的中位数和均值,确认到底哪组更高
注意顺序:先检查levels,再检查direction。如果数据里disease水平排在前面,而roc函数按字母序把healthy当成了阳性组,方向再对也是反的。只需要把levels正确指定后,AUC通常就会翻转为大于0.5的值。
5.2 置信区间画不出来
如果你在plot里写了print.ci=TRUE,但图上没有出现置信区间,多半是roc对象本身没有携带CI信息。前面讲过,只有在roc()时设了ci=TRUE,或者单独调用ci(roc1)计算并赋值,对象里才有置信区间数据。
ci_roc <- ci(roc1) print(ci_roc) plot(ci_roc)记住,plot里的print.ci只是个“开关”,真正的内容在对象里。没有数据,开关开着也没用。
5.3 smooth后阈值坐标对不上
设了smooth=T后,coords(roc1, "best")返回的阈值可能跟你原始标记物数据里的任何一个数值都对不上。这是因为平滑过程做了核密度估计,阈值刻度发生了偏移。这种情况下我通常建议:先在原始数据上计算最佳阈值,再基于平滑曲线画图;或者干脆报告时以经验曲线的阈值为准,平滑曲线只作为视觉呈现。
5.4 图上文字重叠、AUC标注位置不好看
print.auc默认放在图内右下区域,但如果你有多条曲线或者图例占地方,就会挤在一起。手动调整print.auc.x和print.auc.y是很快的解决方案。比如:
plot(roc1, print.auc = TRUE, print.auc.x = 0.6, print.auc.y = 0.3)AUC文字就会出现在横轴0.6、纵轴0.3的位置,基本不会跟其他元素冲突。
5.5 批量画多个指标时输出太多提示信息
同时分析几十个指标时,pROC默认会在控制台不停输出“Setting levels”等说明。统一加上quiet=TRUE,或者在外层包一个suppressMessages(),输出就会清爽很多。这是我日常跑批量筛选时必做的操作。
6. 进阶参数与个人经验总结
6.1 自定义阈值点筛选:coords的三种取法
coords函数是pROC里容易被低估的工具。它有几种用法:
# 指定某个具体阈值 coords(roc1, x = 2.5) # 指定某个特异性水平,反查灵敏度 coords(roc1, x = 0.8, input = "specificity") # 自动取最佳阈值 coords(roc1, "best", best.method = "youden")x参数可以传阈值本身,也可以传一个特异性值然后通过input参数指定输入类型。best.method默认是"youden",也就是约登指数。还可以传"closest.topleft",选取距离左上角最近的阈值点,两种准则下得到的切点可能略有不同,实际项目里最好根据临床场景选择。
6.2 用roc.test比较两条曲线
在诊断标志物对比研究中,光看AUC高一点不够,还得做假设检验。pROC内置了roc.test函数:
test_result <- roc.test(roc1, roc2, method = "delong") print(test_result)输出会给出DeLong检验的P值。P值小于0.05就意味着两条ROC曲线对应的AUC差异有统计学意义,这比单纯画图对比有说服力得多。还有"bootstrap"方法,样本量小或者数据分布特殊时更稳健,但会稍微慢一些。
6.3 一条重要的实战体会
很多人在跑ROC分析时,把大部分精力放在调整图形参数,却忽略了一个更本质的东西:你画ROC之前,是否确认了数据里没有缺失值、是否确认了marker在两组的方向一致、是否理解了levels里的顺序。图形参数随时可以调,画一百张图都不费劲,但如果你把levels传反了、把方向设反了,再漂亮的图也是一张错误的图。
所以我实际工作的流程通常是:先不看图,先把roc对象算出来,看一眼AUC数值合不合理,再用coords去看阈值对应的原始指标刻度是否符合经验判断,最后才进入绘图阶段。绘图的参数再多,也是这一步之后的事。
pROC包学习曲线并不陡峭,核心的roc()、plot()、coords()、ci()几个函数掌握好,已经能覆盖绝大多数诊断试验和预测模型评估的需求。真正值得花时间研究的,反而是那些藏在参数背后的统计语义——levels代表的是你怎么定义病与非病,direction代表的是你指标升高的临床含义,smooth代表的是你在经验与拟合之间的取舍。把这些想明白了,画出来的图才经得起同行审阅,也算真正把这个包用得顺手了。