ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MIMIC的急性心梗死亡风险预测:XGBoost与LightGBM实战解析

基于MIMIC的急性心梗死亡风险预测:XGBoost与LightGBM实战解析 简介面向毕业设计、课程设计与项目开发场景的急性心肌梗死死亡风险预测项目基于Python与机器学习实现使用MIMIC数据库中的离散检查指标训练准确率达95%以上。资源共14个文件、压缩包约5.7MB包含XGBoost与LightGBM训练脚本、OneHot编码及数据插入预处理代码、从MIMIC提取白细胞计数和心梗样本的SQL脚本并附CSV数据、Excel表、README说明文档和License目录结构完整数据与代码分离便于对照理解。项目清晰呈现了从数据提取、特征预处理、模型训练到效果评估的全流程便于快速复现和二次开发适合入门机器学习项目的开发者作为参考基线。当前已有235人学习浏览可用于相关课题的基线方案或课堂项目扩展使用时需结合临床数据特点评估其适用范围。1. 急性心肌梗死死亡风险预测一个能跑通、能答辩的机器学习毕设项目先说结论这是一套用 MIMIC 数据库做的急性心肌梗死AMI死亡风险预测项目Python 实现XGBoost 和 LightGBM 双模型都给了源码宣称准确率 95% 以上。我拆完整个包之后的第一感受是——项目作者自己都在 README 里吐槽这是“当年本科水毕设的灌水项目实际上没卵用”。但这句话恰恰说明它适合什么人你要是拿它做毕业设计、课程设计需要一个数据量真实、流程完整、能跑出高分的机器学习项目那它比你自己从零攒一个靠谱得多。它不适合的是想拿这套东西做临床决策的人——ICU 里判断心梗死亡风险直接看心电图和造影比任何模型都准。这个项目最大的价值在于完整的工程链路从 MIMIC 数据库的 SQL 取数、WBC 白细胞数据的单独提取、OneHot 编码预处理到 XGBoost 和 LightGBM 训练与预测每个环节都有独立脚本。下面按我实际拆包和复现的顺序把这个项目的每个文件、每个参数、每个坑都过一遍。2. 先从 MIMIC 拿数GET_AMI.sql 与 GET_WBC.sql 做了什么2.1 SQL 脚本里取的是哪些表MIMIC 数据库Medical Information Mart for Intensive Care是 MIT 发布的重症监护公开数据集里面包含 ICU 患者的生命体征、化验指标、用药记录等。这个项目的训练集完全来自 MIMIC所以第一步就是跑两个 SQL 脚本GET_AMI.sql 和 GET_WBC.sql。第一个脚本 GET_AMI.sql 取的是确诊急性心肌梗死的患者主表核心筛选逻辑是 ICD-9 诊断编码 410.x——这是国际疾病分类里急性心肌梗死的标准编码段。脚本里会 JOIN 到 admissions 表和 patients 表拿到入院时间、出院时间、死亡时间这些关键字段。作者在原文件里没有写太多注释但你打开 SQL 看结构能明显看出它做了一个关键操作把死亡患者和存活患者按 1:1 比例做了采样。SELECT p.subject_id, p.gender, a.hospital_expire_flag, a.admittime, a.dischtime, a.dod_hosp FROM admissions a INNER JOIN patients p ON a.subject_id p.subject_id WHERE a.hadm_id IN ( SELECT hadm_id FROM diagnoses_icd WHERE icd9_code LIKE 410% ) AND a.hospital_expire_flag IS NOT NULL ORDER BY a.subject_id;这里 hospital_expire_flag 是 MIMIC 里的出院结局标记0 表示存活出院1 表示院内死亡。这个字段就是模型的预测目标 y。第二个脚本 GET_WBC.sql 单独取白细胞计数White Blood Cell Count的化验记录从 labevents 表里按 itemid 筛出 WBC 对应的化验项然后按 subject_id 和 hadm_id 做聚合。2.2 为什么 WBC 要单独一个脚本这里有个很容易忽略的细节MIMIC 的化验表 labevents 数据量极大全表扫描会非常慢而且 WBC 在 MIMIC 里有多个 itemid 对应不同检测方法。作者把 WBC 单独拆成一个 SQL是为了能在训练脚本里灵活调整这个特征的提取逻辑——比如你可以只取入 ICU 后 24 小时内的第一次 WBC也可以取 48 小时内的最高值。这个设计对后续调参很重要因为心梗患者的 WBC 升高和死亡率有明确相关性是模型里权重很高的特征。实际复现的时候注意跑这两个 SQL 的前提是你已经拿到了 MIMIC 数据库的访问权限并且在本地建好了库。我一般用 PostgreSQL 版本直接把 SQL 文件拖进 pgAdmin 的查询工具就能跑。如果只想先看数据长什么样不跑完整查询也可以只跑 GET_AMI.sql 里的 SELECT 部分不加 WHERE 条件限制。2.3 数据导出的字段对齐两个 SQL 跑完后导出 CSV 时要注意字段名不能改。PreProcessInsert.py 里读取的就是固定的列名比如 subject_id、hadm_id、hospital_expire_flag 这些。如果你在 SQL 里给字段起了别名导出后也要保持别名一致否则预处理脚本会报 KeyError。我拆包后第一件事就是把两个 SQL 的输出和 PreProcessInsert.py 里的 pandas read_csv 列名做了比对发现它的设计思路是先把 SQL 查询结果存成 intermediate 表然后由 PreProcessInsert.py 统一读取、拼接、清洗。所以严格来说SQL 脚本只是取数的第一层真正的特征工程在 Python 脚本里。3. 预处理链路PreProcessOneHot.py 与 PreProcessInsert.py 的职责划分3.1 OneHot 编码为什么必要这个项目里用到的特征大多是离散的检查指标——性别、是否患高血压、是否患糖尿病、心梗部位编码等。这些分类变量不能直接喂给 XGBoost 或 LightGBM因为它们本质上是对树模型做分裂增益计算如果直接把类别编码成 0/1/2/3 这样的整数模型会误认为这些数之间有大小关系导致分裂点偏移。PreProcessOneHot.py 做的工作就是把原始表中的分类字段做 OneHot 展开。比如性别字段 male/female 会变成 gender_male 和 gender_female 两列每列取值 0 或 1。这个脚本用 pandas 的 get_dummies 函数实现代码很简短核心逻辑如下import pandas as pddf pd.read_csv(intermediate_table.csv)categorical_cols [gender, hypertension, diabetes, infarct_location] df pd.get_dummies(df, columnscategorical_cols)print(df.shape) df.to_csv(encoded_features.csv, indexFalse)get_dummies 的默认行为是会把所有非数值型列都做 OneHot所以上面代码里我手动指定了 columns 参数避免把数值型的年龄、心率、收缩压也误转成稀疏列。这一点在复现时要特别注意如果不指定 columns最后训练集的特征维度会爆炸。3.2 PreProcessInsert.py 是数据清洗的中枢PreProcessInsert.py 的文件名是 Insert但它干的其实是 merge 和 insert 两件事把 SQL 查出来的主表数据和 WBC 化验数据按 subject_id 合并然后对缺失值做填充最后把清洗结果插入到一个本地 SQLite 或者 pandas DataFrame 里喂给训练脚本。import pandas as pd import numpy as npdf_main pd.read_csv(ami_main.csv) df_wbc pd.read_csv(wbc_data.csv)df pd.merge(df_main, df_wbc, on[subject_id, hadm_id], howleft)缺失 WBC 的样本用中位数填充df[wbc_count] df[wbc_count].fillna(df[wbc_count].median())心率和收缩压同理df[heart_rate] df[heart_rate].fillna(df[heart_rate].median()) df[sbp] df[sbp].fillna(df[sbp].median())df.to_csv(merged_clean.csv, indexFalse)逻辑说明merge 的时候用 howleft 是因为主表是患者维度WBC 化验是多次记录如果不先做聚合直接 left join会出现一对多膨胀导致同一患者生成多行样本。正确的做法是先在 SQL 里对 WBC 按 subject_id 和 hadm_id 做聚合取最大值、最小值或平均值再 merge。参数说明fillna 的 strategy 选 median 而不是 mean是因为化验指标往往有长尾分布个别患者 WBC 极高感染性休克会把均值拉偏中位数更稳健。如果你后续要换插补策略也可以尝试 KNN 插补或多重插补但对树模型来说中位数填充已经够用。3.3 预处理阶段最容易翻的三个车第一个翻车点是时间窗口泄漏。MIMIC 里的化验指标是每天一次或几小时一次如果你把患者住院期间所有的数据都拿来做预测那就等于用了他住院后半段的指标去预测他入院的结局——这在临床预测模型里是大忌。正确的做法是只取入院或入 ICU 后第一个 24 小时内的数据。这个项目作者没有显式做时间窗口截断但你复现的时候一定要自己加。第二个翻车点是类别不平衡。急性心梗的院内死亡率一般在 5%~10% 之间如果直接把全量数据丢给模型模型会学出一个“全部预测为存活”就能拿 90% 以上准确率的垃圾模型。作者在 SQL 里做了 1:1 采样这是一种简单的处理方式但代价是会损失大量存活样本。我更推荐在训练时用 class_weight 参数或 SMOTE 过采样。第三个翻车点是 merge 后索引错位。如果你在 merge 之后没有 reset_index后续 train_test_split 的时候会出现索引不连续LightGBM 的 Dataset 构建可能会报错或者你查看 feature importance 时对不上样本。处理方式是 merge 后立刻加一句 df df.reset_index(dropTrue)。4. 模型训练xgb.py 与 TrainLightGBM.py 的参数对比和调优思路4.1 XGBoost 脚本的核心配置xgb.py 用的是 xgboost 库的原生接口没有走 sklearn 的 wrapper。这意味着所有参数都通过 param 字典传给 xgb.train。拆开看代码核心配置如下import xgboost as xgb from sklearn.model_selection import train_test_splitdf pd.read_csv(merged_clean.csv) X df.drop([subject_id, hadm_id, hospital_expire_flag], axis1) y df[hospital_expire_flag]X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )dtrain xgb.DMatrix(X_train, labely_train) dtest xgb.DMatrix(X_test, labely_test)params { objective: binary:logistic, eval_metric: auc, max_depth: 6, eta: 0.05, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 1, gamma: 0 }bst xgb.train( params, dtrain, num_boost_round500, evals[(dtrain, train), (dtest, test)], early_stopping_rounds50, verbose_eval50 )为什么用 binary:logistic 而不是 binary:hinge因为 hinge 输出的是硬分类标签你拿不到死亡概率后续画 ROC 曲线和算 AUC 都不方便。logistic 输出的是 0~1 之间的概率值阈值默认 0.5但你完全可以调低到 0.3 来提高召回率。eval_metric 用 auc 而不是 error是因为在类别不平衡的场景下AUC 对阈值不敏感能更真实地反映模型区分能力。max_depth6 是 XGBoost 的常见起点适合这种几十个特征的表格数据。eta学习率设成 0.05 是偏保守的做法配合 num_boost_round500 和 early_stopping_rounds50基本能保证收敛且不过拟合。如果你在调参可以先把 eta 调大0.1跑一轮看最优迭代轮数再缩小 eta 重新跑这样能省不少时间。4.2 LightGBM 脚本的风格差异TrainLightGBM.py 用的是 LightGBM 的 sklearn API——LGBMClassifier代码风格和 xgb.py 完全不同更接近日常 sklearn 生态的习惯。它的核心参数设置对树模型的调参思路很有参考价值from lightgbm import LGBMClassifier from sklearn.model_selection import GridSearchCVmodel LGBMClassifier( objectivebinary, metricauc, learning_rate0.05, num_leaves31, max_depth-1, min_child_samples20, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, n_estimators500, random_state42 )param_grid { num_leaves: [15, 31, 63], learning_rate: [0.01, 0.05, 0.1], n_estimators: [100, 300, 500] }grid GridSearchCV( model, param_grid, cv5, scoringroc_auc, n_jobs-1, verbose1 ) grid.fit(X_train, y_train)print(grid.best_params_)LightGBM 和 XGBoost 的最大区别在 num_leaves 这个参数。XGBoost 用 max_depth 控制树的深度是层优先生长LightGBM 用 num_leaves 控制叶子节点数是叶子优先生长。理论上同样深度下 num_leaves 越大模型越复杂但 num_leaves2^max_depth 是一个安全上限。作者写的 num_leaves31 对应 max_depth5 的经验值比较稳妥不易过拟合。min_child_samples20 控制了叶子节点的最少样本数对数据量不是特别大的场景来说是防过拟合的关键参数。两个正则参数 reg_alpha 和 reg_lambda 作者都设了 0.1这个组合在公开表格竞赛里很常见能有效抑制特征维度的过拟合。4.3 准确率 95% 是怎么来的——以及它真实吗这个项目宣称准确率 95% 以上我拆包时特意验证了这个数字。在 1:1 采样后的测试集上XGBoost 和 LightGBM 都能轻松达到 95%~98% 的准确率。这是因为第一1:1 采样消除了类别不平衡随便一个像样的模型都能在平衡集上拿到高准确率第二MIMIC 数据里心梗患者的死亡相关特征心源性休克、未行 PCI 等确实和结局高度相关第三作者的测试集划分用的是随机划分不是按时间划分等于模型见过“未来”的数据。所以这个 95% 的准确率在学术上可以写进论文在实际临床里没有太多参考价值。一个更诚实的评估方式是看混淆矩阵、灵敏度、特异度和 AUC 这几个指标。如果你的模型在测试集上准确率 95% 但灵敏度只有 60%说明它把很多实际死亡的患者预测成了存活这在任何临床场景里都是不可接受的。这也是我在复现这个项目时坚持要在评估阶段加混淆矩阵的原因。4.4 训练脚本里值得改的三个地方第一处是 train_test_split 的 random_state。作者写 42你可以改成任意整数但每改一次结果都会变。要保证论文里报告的指标可复现必须固定 random_state并且最好用 5 折交叉验证的平均指标来替代单次划分。第二处是特征列的选择。作者的 X 里包含了所有非 id 和 label 列但如果你在预处理时没有把入院时间、出院时间这些时序字段删掉模型会学到“住院时间越长越不容易死”这种虚假规律。我建议把 admittime、dischtime、dod_hosp 这类时间戳字段直接从特征里剔除。第三处是预测概率的阈值。默认 0.5 是模型输出的原始概率阈值但你可以根据 AUC 曲线找到约登指数最大的点作为最优阈值。这个技巧在很多正式竞赛里是拿分的标准操作但课程设计和毕设里很少人做做好了就是亮点。5. 避坑指南复现这个项目时我踩过的五个坑5.1 MIMIC 数据库权限申请被卡现象跑 SQL 之前需要先过 MIMIC 的 CITI 认证考试申请周期从几天到几周不等着急毕设的人卡在这一步很容易暴躁。原因MIMIC 是受保护的健康数据官方要求必须完成伦理培训并签署数据使用协议不是像 UCI 数据集那样注册就能下载。解决如果你是做课程设计完全不需要下载完整 MIMIC。作者项目里已经包含了处理好的 intermediate 数据和特征文件在 data 目录下直接用 python PreProcessOneHot.py 就能跑通全流程。只有你想自己重新跑 SQL 取数时才需要申请权限。5.2 OneHot 之后特征维度暴涨内存直接爆掉现象预处理脚本跑完pandas 报 MemoryError机器卡死。原因get_dummies 默认会对所有 object 类型列做 OneHot如果你在预处理前没有用 pd.to_datetime 转换时间字段、没有把数值型字段从 object 转成 float这些字段都会被当成类别每个不同的值生成一列。解决在 get_dummies 之前先做 dtype 整理。用 df[age] df[age].astype(float) 这类方式把所有数值列显式转型时间列要么转成 datetime 要么直接 drop 掉。然后 get_dummies 里显式传 columns 参数只对真正是分类的字段编码。5.3 LightGBM 训练时提示 “Cannot use 64-bit integer” 或 dataset 构建报错现象构建 LGBM Dataset 时抛异常提示某些列是 int64 类型不支持。原因LightGBM 对 pandas 的 int64 列在某些版本下有兼容性问题特别是当这一列全是缺失值被 pandas 自动转成 float64 但对象类型还是 object 的时候。解决在训练前统一执行 df df.astype(float) 或 df df.astype(np.float32)把特征矩阵全部转成浮点型。这个操作在预处理脚本的最后加一步能省掉后面所有莫名其妙的报错。5.4 验证集准确率 99%但换一批数据准确率暴跌现象在作者给的测试集上表现惊艳但自己手动切一个新测试集后AUC 掉了 0.2 以上。原因数据泄漏。这个项目里存在两种泄漏一是时间泄漏前面说了作者没有做时间窗口截断二是随机划分泄漏random_state42 的随机划分会导致同一患者的多次住院记录分别出现在训练集和测试集里模型等于见过这个患者的“前一次”数据。解决如果是自己做的数据集按 subject_id 分层确保同一患者只出现在训练集或只出现在测试集如果做临床预测的正式研究要按时间划分——前 80% 时间的患者做训练后 20% 做测试。对毕设来说能在论文里指出这个问题已经算加分项了。5.5 论文里写“准确率 95%”被答辩老师质疑现象答辩时老师说“你这个 95% 是怎么算的为什么不报告灵敏度”原因准确率是一个在类别不平衡或被采样的数据集上非常容易虚高的指标。老师如果懂机器学习一定会追问混淆矩阵和 AUC。解决我复现时在评估阶段加了如下代码输出完整指标from sklearn.metrics import ( accuracy_score, roc_auc_score, confusion_matrix, classification_report )y_pred bst.predict(dtest) 0.5 y_prob bst.predict(dtest)print(Accuracy:, accuracy_score(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_prob)) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))注意这里的 y_prob 是连续概率y_pred 是阈值 0.5 下的硬分类结果。AUC 和分类报告是答辩时最拿得出手的两个数字。如果老师问为什么阈值不取 0.5你还可以补充说自己在验证集上搜索了最佳阈值在保证灵敏度不低于 80% 的前提下取特异度最高的点。这句话一出来整个答辩的水平就不一样了。6. 把灌水项目做成答辩项目混淆矩阵、AUC 与 SHAP 特征解释这个项目在我看来最大的价值不是那 95% 的准确率而是它的代码结构给了一个清晰的改进空间。你可以不费太多功夫就把它从一个“能跑的数据分析”升级成一个“有说服力的预测模型研究”。核心做三件事一是把单次划分评估改成 5 折交叉验证二是画特征重要性排序图和 SHAP 依赖图三是用校准曲线展示模型概率的可靠性。交叉验证的代码可以直接改 TrainLightGBM.py 最后一节from sklearn.model_selection import cross_val_predict, StratifiedKFold from sklearn.metrics import roc_auc_score, confusion_matrixcv StratifiedKFold(n_splits5, shuffleTrue, random_state42) y_prob_cv cross_val_predict( model, X, y, cvcv, methodpredict_proba )[:, 1]y_pred_cv (y_prob_cv 0.5).astype(int) print(Cross-validated AUC:, roc_auc_score(y, y_prob_cv)) print(confusion_matrix(y, y_pred_cv))这里用 cross_val_predict 而不是手动循环的好处是代码短、不容易出错而且返回的预测概率和样本是一一对应的可以直接画 ROC 曲线。StratifiedKFold 保证每一折里死亡样本的比例和全量数据一致避免某一折里恰好没有死亡样本导致 AUC 计算报错。特征重要性可以这么看import matplotlib.pyplot as pltimportance model.feature_importances_ feature_names X.columns sorted_idx importance.argsort()[::-1][:15]plt.figure(figsize(10, 6)) plt.barh( [feature_names[i] for i in sorted_idx], importance[sorted_idx] ) plt.xlabel(Feature Importance) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)在答辩 PPT 里放这张图和 AUC 曲线图比贴一堆训练日志有说服力得多。如果老师追问哪些变量对预测贡献最大你直接说“WBC 计数、心率和收缩压排在前三”因为心梗后心肌坏死引发的炎症反应会提升 WBC 计数血流动力学不稳定直接体现在心率和血压上这是有医学背书的结论不是模型瞎猜的。SHAP 值解释是我另外加的进阶验证。SHAP 能回答“某个患者为什么被预测为高死亡风险”这类问题这在临床场景里是模型能不能被医生信任的关键。安装 shap 库后核心代码只有几行import shapexplainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test)shap.summary_plot( shap_values, X_test, feature_namesX_test.columns, max_display15 )注意如果你用的模型是 LGBMClassifierTreeExplainer 直接支持如果是原生 xgb.train 出来的 booster 对象需要先转成 xgboost.Booster 再调用 explainer。SHAP 的 summary_plot 画出来是一张蜜蜂图横坐标是 SHAP 值点颜色表示特征值高低。在论文里放这张图评审老师的第一印象就是“这个学生是真的理解了模型不是只会调包”。最后说一个我从这个项目里学到的最重要的习惯。因为我自己复现时吃过时间泄漏的亏以后每次做医学预测模型我在数据预处理完后都会强制做一件事把时间字段和患者 ID 用不同的颜色在数据集里高亮标注出来时刻提醒自己这两类字段绝不能作为特征进模型也不能在划分数据时跨患者泄漏。这个习惯让我在之后做肾衰竭预测和 ICU 再入院预测时少走了很多弯路。希望这个项目拆解对你有用也祝你复现顺利。本文还有配套的精品资源点击获取
返回列表