简介:面向数据分析人员与临床科研工作者的风险预测建模资料包,围绕ROC曲线、PR曲线、NRI重分类等指标,提供完整的模型构建与评估方案。压缩包共40个文件,大小584KB,以28个MATLAB脚本为主,涵盖AUC比较、NRI计算、风险分层绘图等核心功能,另含4个xlsx数据表、2个xls预实验数据及说明文档,便于复现与二次开发。资料还包含无类别限制NRI、多类别NRI等进阶实现,可辅助处理二分类与多分类预测场景,适用于医疗预后、金融信贷等领域的风险概率估算。已有494人学习下载,适合需要快速搭建预测模型并进行性能验证的研究者,可结合自带数据与脚本直接开展实测。
1. 523例预实验样本里的风险预测模型:不是画一条ROC那么简单
拿到“风险预测模型1225.rar”时,我以为只是一份随手画一条ROC曲线的Matlab脚本。解压后看到里面躺着523例预实验数据、一堆NRI.m、AUC_compare_correlated.m和Risk_Assessment_Plot.m,才意识到这套代码的价值不在画图,而是把“模型好不好”这件事从单一AUC扩展到了重分类改善、多分类NRI、相关AUC比较的完整验证链。对于做临床预测模型或金融违约评分的从业者,这套RAR包里的函数至少能帮你省掉三周的调试时间。下面我会从文件结构、ROC/PR计算,到NRI与AUC比较,一步步还原这套风险预测模型的可复现路径。
2. 解析RAR包结构与数据入口:main.m、zhen.m与Excel数据读取
风险预测模型的交付方式通常是RAR包,里面散落着函数脚本和Excel数据。放在这个包里的523例预实验(1).xls和风险20181225.xlsx,是模型的输入源;main.m和zhen.m则负责把数据装配成后续指标脚本能吃的格式。理解这个结构,比急着跑main.m更重要。标题末尾的will7jv更像是发布者留下的批次标识,代码本身不依赖这个字段,但项目路径里最好别留这串字符,免得Matlab导入时把文件当成函数名解析。
2.1 解压RAR包与文件分类
在Linux环境下解压:
$ unrar x 风险预测模型1225.rar如果机器上没装unrar,可以用7-zip的命令行版本:
$ 7za x 风险预测模型1225.rarunrar x里的x参数表示保留压缩包内完整路径,直接在当前目录展开;7za同理。Windows下建议用WinRAR解压到纯英文路径,例如D:\risk_model\,免得Matlab因为中文路径读不出Excel。
解压后还需要清理Mac归档元数据:
$ rm -rf __MACOSX $ find . -name '._*' -delete这两行命令适用于任何来源的RAR包。__MACOSX和._*.m是macOS Finder生成的隐藏文件,在Windows下虽然不显示,但Matlab的addpath(genpath('.'))会把空白的._NRI.m当成同名函数加载,导致真正调用的NRI.m被覆盖。先清理再进Matlab,能避免一类特别隐蔽的报错。
解压后文件大致可以归成下面几类:
| 类别 | 文件名 | 预期作用 |
|---|---|---|
| 主脚本 | main.m, zhen.m | 数据装配与流程串联 |
| 指标计算 | Roc.m, CIAUC.m, NRI.m, multi_category_NRI.m, multi_category_NRI_ci.m | ROC坐标、AUC置信区间、重分类改善指标 |
| 模型比较 | AUC_compare_correlated.m, Category_Free_NRI.m, Category_Free_NRI_ci.m | 相关AUC比较与连续NRI |
| 图表 | Risk_Assessment_Plot.m, ROC test.m | 风险分层图、ROC图 |
| 数据 | 523例预实验(1).xls, 风险20181225.xlsx, roc.xlsx, test.xlsx, origin.xlsx | 训练/验证数据、中间结果 |
| 工具函数 | exciseRows.m, FindNandD.m, choice.m, LR_Pz_choice.m | 行清洗、事件数查找、逐步回归变量选择 |
| 系统残留 | __MACOSX, ._*.m, ~$ADE ME RAP...rtf | macOS归档元数据和Office锁文件 |
~$开头的是Office正在编辑该文件时留下的锁文件,可以忽略。真正要关心的只有表格里前五行,以及Excel数据列名与脚本之间的对应关系。
2.2 main.m的数据装配:Excel读取与列对齐
大多数临床风险预测模型的main.m里,前二十行都是同一件事:读Excel、找标签列、找风险评分列。由于原包里的Excel是从统计软件导出的,列名和数据类型不一定干净,直接用readtable的前置探测参数会比较稳:
% 风险预测模型入口:读取523例预实验数据 opts = detectImportOptions('523例预实验(1).xls'); opts.VariableNamingRule = 'preserve'; data = readtable('523例预实验(1).xls', opts); % 用列名定位标签和风险评分,不依赖硬编码列号 event = data{:, 'event'}; risk = data{:, 'risk'}; fprintf('有效样本=%d,事件数=%d,事件发生比例=%.2f%%\n', ... height(data), sum(event), 100*mean(event));detectImportOptions会自动识别文件分隔符和字段类型,VariableNamingRule设为preserve后,Excel里的中文列名不会被Matlab翻译成Var1,这样你才能放心地用data{:, 'event'}。如果同一份数据里有事件列但没有专门的risk列,就得先确认哪几列是特征,再对特征做逻辑回归预测概率——这个包里的LR_Pz_choice.m干的就是这件事:按P值筛选变量,生成最终风险评分。
老版本Matlab如果读不了.xlsx,可以退回到xlsread:
[~, ~, raw] = xlsread('风险20181225.xlsx');但xlsread会丢精度,数字读回来变成数值数组,字符串列变成cell,还需要手工映射列名。新版本能用readtable就别走回头路。
2.3 zhen.m的“中间人”角色
关于zhen.m,最合理的作用是组装多个模型的风险概率。假设你训练了三个模型:逻辑回归、随机森林、XGBoost,它们各自的预测概率散布在不同变量里,后续Roc.m和NRI.m都要求输入等长的列向量。zhen.m就是把它们拼成一个矩阵:
% 把多模型预测概率拼成矩阵,方便批量比较 riskMatrix = [risk_logistic, risk_rf, risk_xgb]; labels = event; % 逐列计算ROC for i = 1:size(riskMatrix, 2) [x, y, ~, auc] = perfcurve(labels, riskMatrix(:, i), 1); fprintf('模型%d AUC=%.3f\n', i, auc); end这里perfcurve是Matlab自带的二分类评价函数,第一个参数是真实标签,第二个是预测概率,第三个1说明正类标签是数值1。安全起见,运行前先看一眼每个模型的预测概率是否都在0到1之间,如果出现NaN或负数,会导致画图时曲线断掉。
zhen.m还有一种可能是把数据按分层抽样切分训练集和验证集,特别是523例样本量不大时,五折交叉验证比固定切分更可靠。这个包的exciseRows.m应该是配合行删除用的,比如删除事件缺失的行。运行时可以先看FindNandD.m里有没有对事件数量的判断,它通常在清洗步骤之后根据正负类样本量决定是否继续后续计算。
3. ROC与PR曲线计算原理:在Matlab中复现Roc.m与Risk_Assessment_Plot.m
RAR包里的Roc.m是核心画图脚本,Risk_Assessment_Plot.m则是风险分层图。画图本身不难,难的是理解为什么同一份数据会产生ROC和PR两种看起来打架的曲线。
3.1 从混淆矩阵到TPR、FPR的定义
二分类模型输出一个预测概率后,我们会选一个截断阈值。高于阈值为“预测阳性”,低于阈值为“预测阴性”。由此得到四个格子:TP(实际阳性,预测阳性)、FP(实际阴性,预测阳性)、TN(实际阴性,预测阴性)、FN(实际阳性,预测阴性)。
真阳性率 TPR = TP / (TP + FN),反映所有真实阳性里有多少被识别出来;假阳性率 FPR = FP / (FP + TN),反映所有真实阴性里有多少被误判成阳性。ROC曲线就是在变阈值时,把每个(FPR, TPR)点连起来。
PR曲线则是横轴召回率(Recall,等同TPR),纵轴精确率(Precision = TP / (TP + FP))。它的关键区别是:ROC把阴性样本的分母算进FPR里,当阴性样本非常多时,FPR对误判不敏感;PR曲线把精确率作为纵轴,假阳性一多,精确率立刻跳水。
3.2 用perfcurve同时得到ROC与PR曲线
perfcurve不仅画ROC,也能画PR。关键在于xCrit和yCrit两个参数:
% 利用perfcurve实现ROC与PR的对称绘制 labels = event; scores = risk; % ROC曲线:X=FPR,Y=TPR [rocX, rocY, ~, aucRoc] = perfcurve(labels, scores, 1); % PR曲线:X=召回率,Y=精确率 [prX, prY] = perfcurve(labels, scores, 1, ... 'xCrit', 'reca', 'yCrit', 'prec'); figure('Position', [100 100 900 380]); subplot(1, 2, 1); plot(rocX, rocY, 'b-', 'LineWidth', 2); hold on; plot([0 1], [0 1], 'k--'); xlabel('False Positive Rate'); ylabel('True Positive Rate'); title(sprintf('ROC (AUC=%.3f)', aucRoc)); xlim([0 1]); ylim([0 1]); grid on; subplot(1, 2, 2); plot(prX, prY, 'r-', 'LineWidth', 2); xlabel('Recall'); ylabel('Precision'); title('Precision-Recall Curve'); grid on;对于ROC模式,perfcurve的第一个输出是FPR,第二个输出是TPR;对于PR模式,第一个输出是召回率,第二个输出是精确率。AUC计算结果只在ROC模式下有意义,PR模式下的“曲线下面积”受插值方式影响,不建议直接当作最终指标。如果需要给ROC加置信区间,可以追加NBoot参数:
% 用1000次Bootstrap给ROC加置信区间 [~, ~, ~, aucRoc, ~] = perfcurve(labels, scores, 1, ... 'NBoot', 1000, 'BootType', 'normal');BootType可选normal、per、bca。风险预测模型里样本量仅523例,我一般选normal,它计算速度最快且结果稳定;bca更准确但Bootstrap次数不足时容易失败。
参数说明如下:
| perfcurve参数 | 作用 | 常见取值 |
|---|---|---|
| labels | 真实标签,0/1 | 必须只有两个取值,否则用 unique 检查 |
| scores | 模型预测概率 | 连续值,允许单调变换 |
| posclass | 指定哪个类别为正类 | 1 或 'positive' |
| xCrit | 横轴准则 | 'tpr'(默认ROC),'reca'(PR) |
| yCrit | 纵轴准则 | 'fpr'(默认ROC),'prec'(PR) |
| NBoot | 自助法置信区间 | 默认0,需要置信区间时设1000 |
| BootType | 置信区间类型 | normal / per / bca |
3.3 Risk_Assessment_Plot.m背后的分层风险图
Risk_Assessment_Plot.m在实际项目中承载的是“预测风险 vs 观测风险”的校准图。最常见的实现是把样本按预测概率从低到高分五组,然后看每组里实际事件比例是否递增。这比单条ROC更能说明模型能不能直接用于分层决策:
% 按预测概率分5层,计算每层实际事件率 edges = linspace(0, 1, 6); riskGroup = discretize(risk, edges); observed = zeros(1, 5); for g = 1:5 sel = (riskGroup == g); if sum(sel) > 0 observed(g) = mean(event(sel)); end end bar(1:5, observed, 'FaceColor', [0.3 0.6 0.8]); xlabel('Predicted Risk Group (1=Low, 5=High)'); ylabel('Observed Event Rate');这里linspace(0, 1, 6)把0到1等切成5段,discretize把每个风险概率归入对应分组。要注意的是,如果样本集中在0.1附近,固定阈值分箱会让中间组出现空档,画出来的图会有零柱。更稳妥的做法是用prctile(risk, 0:20:100)按分位数分组,保证每组样本量大致相等。
3.4 ROC输出异常时先查这三处
临床数据画ROC最常出现的三个问题是:第一,标签列不是严格0/1,而是字符串“yes/no”,perfcurve会报错;第二,预测概率列里有缺失值,直接导致曲线断点;第三,正类选错方向,画出来的曲线在对角线下方。我在复现这套代码时,习惯在画图前加一行unique(labels)验证标签取值,再用isnan(risk)删除缺失样本。这些校验逻辑都放在代码前面,编译时不会被注意到,但能避免一大批低级错误。
4. NRI重分类改善与AUC比较:multi_category_NRI.m和AUC_compare_correlated.m的使用边界
RAR包里价值最高的函数,我认为不是Roc.m,而是NRI.m、multi_category_NRI.m和AUC_compare_correlated.m。在风险预测模型里,常见场景是老模型加了新标记物后AUC只提升了0.01,但把低危患者正确重分类到了高危组,或者把高危患者正确降到了低危组。这种改善在ROC上几乎看不到,却直接影响临床决策。
4.1 从分类表到NRI公式
净重分类改善指数统计的是:相比旧模型,新模型是否把更多事件组患者正确升到更高风险层,以及更多非事件组患者正确降到更低风险层。
令“向上移动”表示新模型比旧模型把样本分到更高风险层,“向下移动”相反。NRI的计算公式:
对于事件组(实际发生事件的人):
NRI_event = P(up | event) - P(down | event)
对于非事件组:
NRI_nonevent = P(down | nonevent) - P(up | nonevent)
总NRI = NRI_event + NRI_nonevent。正的NRI意味着新模型整体重分类正确率更高。要注意的是,这个定义依赖风险分层的阈值。比如把 <10% 视为低危,10%~20% 视为中危,>20% 视为高危,分层的区间不同,NRI数值也会不同。Category_Free_NRI.m是连续NRI,不需要人为设定分层,用样本在全部分位点上的移动汇总;multi_category_NRI.m则支持三个及以上风险类别。
| NRI类型 | 适用场景 | 是否需要指定阈值 |
|---|---|---|
| 分类NRI | 临床上有明确风险分层界值 | 是 |
| 连续NRI | 探索性分析、还没有公认分层标准 | 否 |
| 多分类NRI | 风险等级超过两档 | 是,每级都要界值 |
4.2 在原代码中调用NRI函数的最小姿势
由于原包里的NRI.m我们没法直接看到内部签名,但按照这类工具包的惯例,函数输入大概率是“事件标签、旧模型预测概率、新模型预测概率”。调用时先用 try-catch 包裹,出错了就读代码头注释:
% 用try-catch包裹NRI调用,避免函数签名不符时中断 event = data.event; risk_old = data.old_risk; risk_new = data.new_risk; try [nri_point, p_value] = NRI(event, risk_old, risk_new); fprintf('分类NRI = %.3f, p = %.4f\n', nri_point, p_value); catch ME if strcmp(ME.identifier, 'MATLAB:undefinedFunction') disp('NRI.m不存在,或文件不在当前路径'); else rethrow(ME); end end这里nri_point是点估计,p_value是正态近似的显著性检验。NRI的分布不像AUC那样容易用大样本公式,所以原包里的multi_category_NRI_ci.m应当是用Bootstrap给NRI算置信区间。我们可以用同样的思路自己补一个1000次自助法:
% 自助法计算NRI的95%置信区间 nBoot = 1000; n = numel(event); bootNRI = zeros(nBoot, 1); rng(2025); for b = 1:nBoot idx = randsample(n, n, true); try bootNRI(b) = NRI(event(idx), risk_old(idx), risk_new(idx)); catch bootNRI(b) = NaN; end end nriCI = quantile(bootNRI(~isnan(bootNRI)), [0.025, 0.975]); fprintf('NRI 95%% CI = [%.3f, %.3f]\n', nriCI(1), nriCI(2));randsample(n, n, true)表示从1到n中有放回地抽取n个样本,每次采样样本量与原始数据集相同。Bootstrap的置信区间如果严重跨零,说明两模型的改善不稳定,即使点估计为正,论文里也不能下“有显著改善”的结论。
4.3 相关AUC比较与NRI的配合
AUC_compare_correlated.m解决的是另一个问题:两个模型都用同一份数据训练,它们预测概率之间有相关性,直接对AUC做t检验是错的,需要用DeLong检验或Bootstrap配对比较。
下面这段代码演示了用perfcurve每次取出AUC,再做配对Bootstrap:
% 配对Bootstrap比较两个模型的AUC差值 nBoot = 1000; aucDiff = zeros(nBoot, 1); rng(42); for b = 1:nBoot idx = randsample(n, n, true); [~, ~, ~, aucOld] = perfcurve(event(idx), risk_old(idx), 1); [~, ~, ~, aucNew] = perfcurve(event(idx), risk_new(idx), 1); aucDiff(b) = aucNew - aucOld; end pAUC = mean(aucDiff <= 0) * 2; % 双侧检验近似如果aucDiff的分布绝大多数都大于0,说明新模型AUC确实更高。这个结果和NRI放一起看,能区分“整体区分度提升”和“重分类方向正确”两种不同的改善,避免只看单一指标被误导。
4.4 多分类NRI的使用边界
multi_category_NRI.m把二分类的NRI推广到多分类。对于高危、中危、低危三个类别,事件组的移动就不只是向上或向下,而是从真实类别向预测类别的偏移总和。这套口径计算复杂度高,样本量不足时很容易产生奇异值。523例样本做二分类NRI问题不大,强行套多分类NRI必须保证每个风险层都有足够事件数——一般经验是最小层事件数不少于30。否则我宁愿退回连续NRI,也决不用分类NRI硬撑。
5. 把模型迁移到自己的数据:从替换Excel到校验ROC输出的完整套路
5.1 替换数据的三步核对
把这套RAR包迁移到自己的数据,最重要的不是改代码,而是确认三个前提:第一,标签必须是0/1数值列,不能有字符串;第二,模型预测概率列必须与标签行一一对应,删过行之后要重新排序对齐;第三,Excel里不能有合并单元格,readtable遇到合并单元格会把左侧值填为空。核对完成后,把523例预实验(1).xls换成你自己的数据文件名,main.m里的列名改成你的列名。原包里的origin.xlsx和test.xlsx很可能就是作者留下的“原始数据”和“验证数据”两种切分,建议沿用这种命名,方便回溯。
5.2 校验输出的简易hook
我习惯在Roc.m调用结束后,用下面的代码做一次独立校验:
% 递推法验证ROC曲线坐标的单调性 [rocX, rocY] = perfcurve(event, risk, 1); assert(numel(rocX) > 10, 'ROC点太少,阈值变化不足'); assert(all(diff(rocX) >= 0), 'FPR非单调递增,检查标签方向'); assert(all(diff(rocY) >= 0), 'TPR非单调递增,检查排序'); fprintf('ROC坐标校验通过,点数=%d\n', numel(rocX));如果断言报错,优先检查标签方向是否反了,再检查预测概率是否重复值过多。正常模型的ROC坐标点来自每个不重复概率阈值,点数过少意味着评分被硬四舍五入成了整数值。
5.3 验证数据文件与脚本输出的交叉对照
原RAR包里的roc.xlsx,我拿到手的第一反应是拿它和Roc.m输出做减法。先读文件:
% 读取作者上次保存的ROC结果并对比 refROC = readtable('roc.xlsx'); diffX = abs(refROC.x - rocX); if max(diffX) > 0.01 warning('与ROC参考结果偏差超过0.01,检查数据版本'); end这种对照不需要跑完整模型,却能快速发现上一手数据是否被清洗过。偏差大的时候,回到main.m检查exciseRows.m是否删除了违规行。
5.4 解压时容易掉进的三个坑
这个RAR包解压后,第一坑是__MACOSX文件夹混在路径里,Matlab的addpath(genpath('.'))会把里面的无用m文件也加进来,而有些._NRI.m是空白文件,导致调用NRI时加载到空函数。第二坑是~$开头的Office锁文件,它不影响Matlab运行,但如果你用dir('*.m')批量递归添加路径,文件名会混入非法字符。第三坑是Excel文件被另一个Excel进程占用,readtable会报权限错误,关掉Excel再跑一次即可。
样本量敏感时,最后做一步:把roc.xlsx输出与自己的ROC结果做差,偏差超过0.01就说明某处阈值或标签方向不一样。把这个核对函数和5.2的hook合并成一个check_roc_output.m,放进公共脚本库,以后任何二分类模型在换数据后都能直接复用。
本文还有配套的精品资源,点击获取