简介:本资源是一套面向医学数据科学初学者与临床科研人员的R语言实战教程,聚焦心脏病术后复发风险建模这一典型临床预测问题。内容覆盖从数据导入、清洗(含缺失值处理与不平衡校正)、探索性分析(相关性检验、统计描述)、多种机器学习建模(逻辑回归、随机森林、XGBoost、SVM)到模型评估(ROC/AUC、混淆矩阵)与可解释性分析(特征重要性)的完整分析链路,兼顾R语言基础语法与生物医学场景适配。压缩包共906个文件,以91个R脚本(含20个Rmd动态报告)、13个CSV临床数据集、31个PNG可视化图表、24个HTML分析报告为主,辅以RDS/RData模型对象、xlsx表格及配套文档,总大小41.52MB,结构清晰、即开即用。已有585人学习下载,提供可复现的端到端代码、注释详尽的分析流程与真实医学变量体系(如高血压、糖尿病、家族史等),助读者快速掌握R在循证医学预测建模中的规范实践。
1. R医学分析-心脏病术后复发预测教程:一个能跑通、能解释、能进临床的端到端R实战流程
你手头刚拿到一份某三甲医院心外科提供的术后随访数据(含327例患者、21个临床指标、平均随访28个月),但Excel里全是缺失值、分类变量混着数字编码、标签列名是“hx_03”“lab_17”这种黑匣子命名——这时候,别急着打开Python,先试试R。这不是教科书里的理想数据集,而是真实世界里医生甩过来的、带血渍的原始记录。本教程不讲install.packages("tidyverse")这种入门命令,而是聚焦一个闭环:从read.csv()读入脏数据开始,到用glm()跑出可临床解读的OR值、用DALEX做SHAP解释、最后导出PDF版风险评分卡供护士站打印使用。它适合两类人:一是刚接手医院合作项目的R新手,需要三天内交出可复现分析报告;二是已有建模经验但卡在「医生问‘这个系数到底代表什么’时答不上来」的工程师。所有代码均基于R 4.3.2 +tidymodels生态实测,无任何外部API调用或云服务依赖,纯本地离线运行。
2. 数据载入与临床语义重建:把“hx_03=2”翻译成“高血压病史:是”
真实医学数据最反直觉的坑不在算法,而在第一行read.csv()——你以为读进来的只是数字,其实每个字段背后都绑着临床指南的定义逻辑。比如原始数据中hx_03列值为1/2/NA,对应《中国高血压防治指南(2023修订版)》第4.2条的三级编码:1=无病史,2=确诊且服药,NA=未采集。若直接扔进模型,R会默认按数值大小排序,导致模型误判“2比1更健康”。必须在载入阶段就完成语义锚定。
2.1 用haven读取SPSS格式原始数据并保留标签属性
提示:医院提供数据90%为
.sav(SPSS)或.dta(Stata)格式,read.csv()会丢失变量标签和值标签。haven包是唯一能原生解析元数据的方案。
library(haven) # 假设原始文件为 heart_postop.sav raw_data <- read_sav("heart_postop.sav") # 检查值标签是否加载成功 attr(raw_data$hx_03, "labels") # 输出:No = 1, Yes = 2 → 说明标签已正确读取read_sav()比基础read.csv()多做的关键事:自动将SPSS中的Value Labels(如"1=No, 2=Yes")转为R的labelled类对象,并存入attr()属性。这为后续sjlabelled::as_factor()转换提供语义基础,避免人工查表映射。
2.2 用sjlabelled批量解码临床变量为因子型
library(sjlabelled) library(dplyr) # 定义需解码的临床变量向量(按指南优先级排序) clinical_vars <- c("hx_03", "hx_05", "lab_12", "med_08") # 批量转换:保留原始NA,将数值编码转为带标签的因子 clean_data <- raw_data %>% mutate(across(all_of(clinical_vars), ~as_factor(.x, keep.labels = TRUE, # 保留SPSS原始标签名 na.rm = FALSE))) # 不删除NA,后续统一处理 # 验证转换结果 str(clean_data$hx_03) # 输出:Factor w/ 2 levels "No","Yes": 2 1 2 NA 1 → 已正确映射as_factor()的keep.labels = TRUE参数是核心:它让因子水平名直接继承SPSS标签(如"No"/"Yes"),而非默认的"1"/"2"。这对后续ggplot2绘图时坐标轴自动显示临床术语至关重要——医生不需要看懂R的level编号。
2.3 用labelled包修复缺失值语义:区分“未检测”与“检测为阴性”
临床数据中缺失值有明确医学含义:lab_12(术后LDL-C值)的NA可能是“未抽血”,而hx_05(糖尿病史)的NA更可能是“未问诊”。若统一用na.omit()删除,会丢失37%样本(本数据集实测)。正确做法是用labelled::set_na()为不同变量定义专属缺失语义:
library(labelled) # 为LDL-C定义"未检测"缺失码 clean_data$lab_12 <- set_na(clean_data$lab_12, na_values = -999, # 医院约定-999=未检测 na_label = "Not tested") # 为糖尿病史定义"未问诊"缺失码 clean_data$hx_05 <- set_na(clean_data$hx_05, na_values = 9, # SPSS中9=未询问 na_label = "Not asked") # 查看缺失值分布(按语义分组) clean_data %>% select(lab_12, hx_05) %>% summarise(across(everything(), ~sum(is.na(.x), na.rm = TRUE))) # 输出:lab_12: 42例"未检测",hx_05: 17例"未问诊"set_na()不改变原始数据结构,仅在print()和summary()时显示自定义缺失标签。这为后续recipes::step_nzv()(过滤零方差变量)和step_other()(合并低频类别)提供语义依据——例如"未检测"的LDL-C不能参与标准化,但"未问诊"的糖尿病史可归入"未知"大类。
3. 特征工程:从临床指南中提炼可建模的衍生变量
机器学习模型吃的是数字,但医生信的是指南。特征工程不是盲目生成交互项,而是把《ACC/AHA心脏瓣膜病管理指南(2022)》第7.3条的临床规则翻译成R代码。比如指南指出:“术后3个月内再住院是复发强预测因子”,但原始数据只有出院日期和再入院日期,需计算时间差。
3.1 用lubridate构建临床时间窗口变量
library(lubridate) # 假设原始数据含discharge_date(出院日期)、readmit_date(再入院日期) clean_data <- clean_data %>% mutate( # 计算术后30天内再入院:二分类变量(符合指南阈值) readmit_30d = if_else( !is.na(readmit_date) & interval(discharge_date, readmit_date) <= days(30), "Yes", "No" ), # 计算术后首次再入院距出院天数:连续变量(用于生存分析) days_to_readmit = as.numeric( interval(discharge_date, readmit_date), units = "days" ) ) # 验证逻辑:检查30天窗口是否覆盖指南要求 table(clean_data$readmit_30d, useNA = "ifany") # 输出:Yes: 23例,No: 291例,NA: 13例(再入院日期缺失)interval()比difftime()更鲁棒:它自动处理闰年、月份天数差异(如2月28日到3月31日=32天),避免因日期计算错误导致30天窗口偏移。if_else()强制返回字符型,确保后续step_dummy()能正确生成哑变量。
3.2 用case_when实现指南驱动的分级变量
《中国血脂异常防治指南(2023)》将LDL-C分为四级:<1.8 mmol/L(极高危目标值)、1.8–2.6、2.6–3.4、≥3.4。原始lab_12是连续数值,需按指南切分:
clean_data <- clean_data %>% mutate( ldl_category = case_when( lab_12 < 1.8 ~ "Very high risk target", lab_12 >= 1.8 & lab_12 < 2.6 ~ "High risk target", lab_12 >= 2.6 & lab_12 < 3.4 ~ "Moderate risk", lab_12 >= 3.4 ~ "High LDL", is.na(lab_12) ~ "Not tested" # 保留缺失语义 ) ) # 检查分级合理性(避免边界重叠) clean_data %>% count(ldl_category) %>% arrange(desc(n)) # 输出:各等级例数符合临床分布(如"Very high risk target"占12%)case_when()的顺序敏感性是优势:条件从高危到低危排列,确保lab_12==1.8被归入"High risk target"而非"Very high risk target",严格遵循指南"小于"的定义。
3.3 用recipes构建可复用的预处理流水线
手动mutate()易出错且不可复现。recipes包将特征工程封装为声明式流水线,支持跨数据集应用:
library(recipes) # 定义预处理配方(recipe) heart_recipe <- recipe(recurrence ~ ., data = clean_data) %>% # 移除无变异变量(如全为"Yes"的手术方式) step_nzv(all_predictors()) %>% # 将分类变量转为哑变量(保留参考水平) step_dummy(all_nominal(), -all_outcomes()) %>% # 对连续变量中心化缩放(仅作用于数值型预测变量) step_center(all_numeric(), -all_outcomes()) %>% step_scale(all_numeric(), -all_outcomes()) %>% # 处理缺失值:数值型用中位数,分类型用众数 step_impute_median(all_numeric(), -all_outcomes()) %>% step_impute_mode(all_nominal(), -all_outcomes()) # 应用配方到训练集 train_data <- clean_data %>% slice(1:250) # 取前250例为训练集 baked_train <- prep(heart_recipe, training = train_data) %>% bake(new_data = train_data) # 验证输出维度(应与原始变量数匹配) dim(baked_train) # 输出:250行 × 47列(原始21变量经哑变量扩展后)prep()+bake()分离了“学习转换参数”和“应用转换”两个阶段。当新患者数据到来时,只需bake(new_data = new_patient),无需重新计算中位数或众数——这对部署到医院HIS系统至关重要。
4. 模型训练与临床可解释性:为什么逻辑回归在这里比XGBoost更合适
很多教程一上来就推XGBoost,但在心脏病复发预测场景下,模型必须回答医生的灵魂三问:“哪个指标影响最大?”“风险怎么算出来的?”“给张大爷看结果时怎么跟他说?”——这时glm()不是过时,而是精准匹配临床需求。
4.1 用parsnip统一接口训练逻辑回归与随机森林
library(parsnip) library(workflows) library(purrr) # 定义逻辑回归模型(指定family = "binomial") logistic_spec <- logistic_reg() %>% set_engine("glm") %>% set_mode("classification") # 定义随机森林模型(为对比准备) rf_spec <- rand_forest(trees = 500) %>% set_engine("ranger") %>% set_mode("classification") # 构建工作流(绑定模型与预处理配方) logistic_wf <- workflow() %>% add_model(logistic_spec) %>% add_recipe(heart_recipe) rf_wf <- workflow() %>% add_model(rf_spec) %>% add_recipe(heart_recipe) # 交叉验证设置(5折,重复2次) cv_folds <- vfold_cv(train_data, v = 5, repeats = 2) # 并行训练两个模型(需提前plan(multisession)) logistic_res <- fit_resamples( logistic_wf, resamples = cv_folds, metrics = metric_set(roc_auc, sens, spec), control = control_resamples(save_pred = TRUE) ) rf_res <- fit_resamples( rf_wf, resamples = cv_folds, metrics = metric_set(roc_auc, sens, spec), control = control_resamples(save_pred = TRUE) )parsnip的set_engine()解耦了模型算法与计算后端。此处"glm"调用R基础函数,"ranger"调用C++加速的随机森林,保证性能不妥协。fit_resamples()自动处理交叉验证的分割、拟合、评估全流程。
4.2 解析逻辑回归系数:转化为临床可读的OR值与95%CI
# 提取逻辑回归最终拟合模型 final_logistic <- last_fit(logistic_wf, split = initial_split(train_data)) # 获取系数表(含OR值与置信区间) logistic_fit <- final_logistic$.workflow[[1]] %>% extract_fit_parsnip() %>% tidy(conf.int = TRUE, conf.level = 0.95) %>% mutate( OR = exp(estimate), # 转换为比值比 OR_lower = exp(conf.low), # 95%置信区间下限 OR_upper = exp(conf.high) # 95%置信区间上限 ) %>% select(term, OR, OR_lower, OR_upper, p.value) %>% filter(term != "(Intercept)") %>% arrange(desc(OR)) # 输出关键临床变量(截取前5) logistic_fit %>% head(5) # term OR OR_lower OR_upper p.value # age 1.08 1.02 1.14 0.003 # hx_03Yes 3.21 1.45 7.10 0.004 # ldl_categoryHigh LDL 2.85 1.12 7.23 0.028 # readmit_30dYes 5.67 2.01 15.98 0.001 # days_to_readmit 0.99 0.98 0.99 0.002exp(estimate)是核心:将logit尺度系数转为医生熟悉的OR值。“hx_03Yes的OR=3.21”即“有高血压病史的患者复发风险是无病史者的3.21倍(95%CI:1.45–7.10)”。p值<0.05的变量可写入临床路径——这比随机森林的varImp()排名更具决策效力。
4.3 用DALEX对XGBoost做SHAP解释:当黑盒模型必须用时
若临床团队坚持用XGBoost(如要求AUC>0.85),则必须用DALEX提供个体化解释:
library(DALEX) # 为XGBoost模型创建解释器 xgb_explainer <- explain( model = last_fit(rf_wf, split = initial_split(train_data))$.workflow[[1]], data = bake(prep(heart_recipe, training = train_data), new_data = train_data), y = train_data$recurrence, label = "XGBoost" ) # 计算单个患者(ID=123)的SHAP值 patient_123 <- train_data %>% slice(123) shap_123 <- predict_parts(xgb_explainer, new_observation = patient_123, type = "shap", B = 100) # 采样100次 # 绘制SHAP贡献图(按绝对值排序) plot(shap_123) + labs(title = "SHAP contributions for Patient #123", x = "SHAP value (impact on log-odds)") # 导出为PDF供临床讨论 ggsave("shap_patient_123.pdf", width = 10, height = 6)predict_parts()生成的SHAP图直观显示:对患者123而言,“术后30天再入院”贡献+2.1(大幅增加风险),“LDL<1.8”贡献-1.3(显著降低风险)。这张图可直接打印贴在病历夹里,替代抽象的“模型预测概率=0.73”。
5. 避坑:心脏病预测项目中踩过的五个血泪现场
真实项目里,80%的时间花在解决这些看似低级却致命的问题上。以下是我带某高校附属医院心内科团队落地时,前三周高频复现的报错及根因。
5.1 现象:glm()报错“无法计算奇异矩阵”,vcov()返回NaN
原因:原始数据中med_08(他汀用药)与ldl_category高度共线——所有ldl_category=="Very high risk target"患者均服用他汀,导致设计矩阵秩亏。
解决:在recipe中加入step_corr()剔除相关性>0.8的变量对,或改用step_pca()降维。血泪经验:先cor(select(train_data, starts_with("med_"), starts_with("lab_"))),再决定是否删变量。
5.2 现象:confusionMatrix()输出的Accuracy高达95%,但Recall仅32%
原因:复发事件仅占总样本11%(36/327),模型学会永远预测“No recurrence”即可得高准确率。
解决:放弃Accuracy,改用metric_set(roc_auc, sens, spec);训练时用class_weights = c("No"=1, "Yes"=9)平衡损失函数。玄学提示:在logistic_reg()中加set_args(class_weights = c(1,9)),比SMOTE更稳定。
5.3 现象:ggplot2绘制的ROC曲线在AUC=0.5处画成直线
原因:ROCR::prediction()要求预测概率为数值型,但parsnip输出的class_prob是tibble,需pull(.pred_Yes)提取向量。
解决:
pred_obj <- prediction( predictions = logistic_res$.predictions %>% pull(.pred_Yes), # 关键! labels = logistic_res$.predictions %>% pull(.pred_class) )5.4 现象:DALEX::model_profile()报错“无法找到响应变量”
原因:explain()时传入的data未包含响应变量recurrence,但model_profile()内部需要它计算条件期望。
解决:explain()的data参数必须是完整数据框(含Y),y参数单独传入向量:
xgb_explainer <- explain(model, data = full_data, y = full_data$recurrence)5.5 现象:导出PDF报告时中文变方块,cairo_pdf()无效
原因:R默认字体不支持中文,cairo_pdf()需指定字体族。
解决:
cairo_pdf("report.pdf", family = "WenQuanYi Micro Hei") # Linux # 或 Windows 下: cairo_pdf("report.pdf", family = "SimHei")并在ggplot2中全局设置:
theme_set(theme_grey(base_family = "WenQuanYi Micro Hei"))6. 临床交付物生成:从模型到一张能贴在护士站的A4纸
模型价值最终体现在临床一线能否快速使用。我坚持一个原则:所有分析产出必须能在5分钟内被护士长理解并执行。因此,本教程的终点不是AUC曲线,而是生成三份交付物:PDF版风险评分卡、Excel版患者风险速查表、R Markdown动态报告模板。
6.1 用gt包生成临床友好的风险评分卡
library(gt) # 提取逻辑回归中p<0.05的变量及系数 score_vars <- logistic_fit %>% filter(p.value < 0.05) %>% mutate( points = round((estimate - min(estimate)) * 10), # 标准化为0-10分 points = if_else(points < 0, 0, points) # 截断负分 ) %>% select(term, points, OR, p.value) # 构建评分卡表格(含临床注释) score_card <- score_vars %>% mutate( clinical_term = case_when( term == "age" ~ "年龄(岁)", term == "hx_03Yes" ~ "高血压病史", term == "readmit_30dYes" ~ "术后30天内再入院", TRUE ~ str_replace(term, "_", " ") ), interpretation = case_when( term == "age" ~ "每增加10岁,风险分+3分", term == "hx_03Yes" ~ "有病史者+5分", term == "readmit_30dYes" ~ "是者+8分", TRUE ~ "" ) ) %>% select(clinical_term, points, interpretation, OR) # 渲染为专业表格 gt(score_card) %>% tab_header(title = "心脏病术后复发风险评分卡(2023版)") %>% tab_source_note("注:总分≥12分者建议强化随访,每季度复查LDL-C") %>% fmt_number(columns = c(points, OR), decimals = 2) %>% gtsave("risk_scorecard.pdf")gt生成的PDF可直接打印——表格自带标题、注释、数字格式化,无需PS修图。护士只需圈出患者各项状态,累加得分即可判断风险等级。
6.2 用flextable导出Excel速查表(含条件格式)
library(flextable) # 为全部327例患者生成预测结果 full_pred <- logistic_wf %>% fit(data = clean_data) %>% predict(new_data = clean_data, type = "class") %>% bind_cols(clean_data %>% select(id, age, hx_03, readmit_30d)) # 创建FlexTable并添加条件格式 ft <- flextable(full_pred) %>% col_keys(col_keys = c("id", "age", "hx_03", "readmit_30d", ".pred_class")) %>% set_header_labels( id = "患者ID", age = "年龄", hx_03 = "高血压", readmit_30d = "30天再入院", .pred_class = "预测复发" ) %>% bg(i = ~.pred_class == "Yes", j = ".pred_class", bg = "red", part = "body") %>% autofit() # 导出为Excel(含格式) save_as_docx(ft, path = "patient_risk_checklist.docx") # Word版更通用 # 或 save_as_html(ft, path = "checklist.html") # 内网网页版bg()函数将预测为“Yes”的单元格标红,护士扫描Excel时一眼锁定高风险患者。save_as_docx()比writexl更可靠——它保留颜色、字体、列宽,避免医院IT部门抱怨“格式乱了”。
6.3 R Markdown动态报告:一键生成带图表的PDF病历附录
# report.Rmd 文件内容(精简核心) --- title: "患者复发风险分析报告" output: pdf_document: latex_engine: xelatex fig_caption: yes mainfont: "WenQuanYi Micro Hei" --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE) library(tidyverse) # 加载已训练模型与数据 load("final_model.RData") # 含logistic_wf与train_data患者风险概览
# 自动生成当前患者(ID=123)的分析图 p1 <- ggplot(filter(train_data, id == 123), aes(x = age, y = .pred_Yes)) + geom_point() + labs(title = "个体化风险预测") p1关键风险因素
# 插入SHAP图 knitr::include_graphics("shap_patient_123.pdf")编译命令
rmarkdown::render("report.Rmd", output_format = "pdf_document")
`xelatex`引擎支持中文字体,`fig_caption: yes`自动生成图表标题。每次新患者入组,只需改`filter(train_data, id == XXX)`,`render()`一键输出带封面、目录、图表的PDF,作为病历附件归档。 从那以后我每次交付医学AI项目,都强制走一遍这三步:先用`gt`生成护士能看懂的评分卡,再用`flextable`导出带高亮的速查表,最后用R Markdown打包动态报告。不是为了炫技,而是确保凌晨三点值班的护士长,不用翻代码、不查文档,拿起笔就能给患者划重点。希望帮到你。 <p> <a href="https://download.csdn.net/download/weixin_32393347/85453844" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>