简介基于Python与机器学习构建的急性心肌梗死死亡风险预测项目面向毕业设计、课程设计与项目开发场景以MIMIC重症数据库为训练数据源综合运用XGBoost、LightGBM等集成学习算法完成分类建模模型预测准确率可达95%以上。资源共14个文件、压缩包约5.7MB脚本构成涵盖4个Python源码文件数据预处理、OneHot编码、模型训练与预测、2个SQL查询脚本WBC、AMI指标提取、3个CSV数据文件、1个XLSX数据表及README说明文档完整覆盖从原始数据清洗、特征工程到模型评估的训练链路。项目代码经过严格测试可直接运行读者可重点参考其医疗数据特征处理方法、集成模型调参思路以及完整的项目目录组织方式。目前已有235人学习下载对正在完成医学机器学习方向课题的高校学生具有实际借鉴价值。1. 为什么急性心肌梗死死亡风险预测适合做成一个机器学习项目急性心肌梗死AMI的死亡风险预测本质上是一个带真实临床含义的二分类问题把入院时的检验数据喂给模型让它判断患者是否属于高危人群。我见过不少课设项目把课题选成房价预测或者鸢尾花分类最后做完了除了一句“准确率不错”之外什么都说不出来。而这个课题的好处在于——它同时占了三个便宜数据量不大、特征可解释、准确率上限高。Python 生态里的 pandas、scikit-learn 和 XGBoost 正好覆盖从数据清洗到模型交付的全流程跑通一条最小路径只需要一台普通笔记本。对想拿一个“像样”的毕业设计或者课程设计项目的人来说这是性价比极高的选择尤其是你已经会用 Python 基础语法但还没完整做过一个机器学习项目的时候。2. 数据准备是第一道坎小样本临床数据怎么处理才不出问题2.1 急性心肌梗死数据集里通常有哪些特征急性心肌梗死死亡风险预测的数据一般来自医院病案系统或者公开的科研数据集记录的是患者入院时的生理指标、检验结果和病史。不同于电商点击率那种动辄几千万行的数据这类临床数据通常只有几百到几千条特征维度在二十到四十个之间。你需要关心的不是海量数据怎么跑而是如何在有限样本里把每一列特征的临床含义搞清楚。以下是我在类似项目里经常见到的特征结构列名可以按自己的数据调整字段名类型临床含义age数值年龄越高风险越大gender二分类性别部分研究显示男性早期风险更高systolic_bp数值入院收缩压过低提示休克风险heart_rate数值入院心率过快或过慢都是危险信号troponin_i数值肌钙蛋白 I心肌损伤的核心标志物st_segment分类心电图 ST 段抬高/压低/正常killip有序分类心功能分级I 到 IV 级级别越高预后越差diabetes二分类是否有糖尿病史hypertension二分类是否有高血压史creatinine数值肌酐反映肾功能肾损伤会拉高死亡风险lvef数值左室射血分数心衰程度的直接体现outcome二分类目标标签1 表示院内死亡或短期死亡拿到数据第一步不是急着建模而是先确认 outcome 列的正例占比。急救场景的数据天然带着类别不平衡问题死亡患者在所有入院者里占比往往不到 10%。这个数字会直接影响你后面所有策略所以首先看清楚它。2.2 类别不平衡95% 准确率可能是个陷阱标题里写“准确率达 95% 以上”这句话本身需要打一个问号。如果数据里死亡样本只占 5%那么一个永远预测“存活”的模型准确率也有 95%。这在学术上叫准确率悖论是医疗预测项目里最容易翻车的地方。评审老师或者答辩评委看到 95% 的第一反应大概率是看一眼你的混淆矩阵而不是为这个数字喝彩。所以处理顺序必须是先确认类别分布再决定是否做样本平衡。我一般会在拿到数据后先跑一个基线——不处理任何不平衡问题直接训练一个随机森林看看它在少数类上的召回率是多少。如果召回率惨不忍睹再上 SMOTE 过采样或者 class_weight 加权。这里有个血泪经验不要一上来就 SMOTE先让不平衡问题暴露出来后面的处理才有对比意义。2.3 用 pandas 完成数据加载、缺失值处理与基线查看常见做法是先把数据读进来看一下整体结构和缺失情况。下面的代码是最小可运行的一组操作适合任何来源的 CSV 数据。import pandas as pd import numpy as np # 读入原始数据 df pd.read_csv(ami_data.csv) print(样本量:, df.shape[0]) print(特征维度:, df.shape[1]) # 目标变量分布死亡样本占比直接决定后续策略 death_rate df[outcome].mean() print(死亡样本占比: {:.2%}.format(death_rate)) # 先看缺失值分布再决定填充策略 missing df.isnull().sum() print(missing[missing 0])逻辑说明df[outcome].mean()算的是正例占比因为 outcome 是 0/1 编码。这个数如果低于 20%就说明类别不平衡问题非处理不可。df.isnull().sum()用来定位哪些列有缺失通常临床数据里肌钙蛋白、肌酐这类检验指标最容易有缺项可能是患者没做某项检查或者结果未录入。参数说明read_csv读取 CSV 文件时如果遇到中文列名可以加encodinggbk或者encodingutf-8调整如果数据是 Excel 格式先另存为 CSV 再读比在 pandas 里折腾read_excel省心得多。2.4 缺失值填充分组中位数比全局均值靠谱临床数据缺失不能简单用全局均值填。举个例子心功能 Killip IV 级的患者肌钙蛋白基线本来就高如果拿全体患者的中位数去填等于把重症患者的数据往轻症方向拉模型学到的规律自然是扭曲的。按临床分组填充是个稳妥做法。# 按 Killip 分级分组填充肌钙蛋白保留组间差异 df[troponin_i] ( df.groupby(killip)[troponin_i] .transform(lambda x: x.fillna(x.median())) ) # 肌酐没有强分组变量时再退回到全局中位数 df[creatinine] df[creatinine].fillna(df[creatinine].median())逻辑说明groupby(killip)把样本按心功能分级分成四组transform(lambda x: x.fillna(x.median()))在每一组内部用该组的中位数填充缺失值。transform返回的是与原 DataFrame 等长的 Series可以直接赋值回原列不会破坏索引对齐。参数说明分组填充的前提是 killip 列本身没有大量缺失。如果 killip 缺失超过 30%就别用它做分组依据了直接全局中位数填充或者用模型预测填充。填充完最好画一次直方图对比填充前后的分布形态确认没有出现异常尖峰。2.5 SMOTE 之前先切分样本不平衡处理的正确顺序处理类别不平衡的常见工具是imblearn库里的 SMOTE它通过在少数类样本之间插值生成合成样本。但顺序错了会出大问题如果先对全量数据做 SMOTE 再切分训练测试集合成样本会同时出现在两边验证结果虚高等模型上线就露馅。正确顺序是先把数据集切好再只在训练集上做过采样。from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE feature_cols [age, systolic_bp, heart_rate, troponin_i, creatinine, lvef, killip, st_segment, diabetes] X df[feature_cols] y df[outcome] # 分层切分保证训练集和测试集中正例比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 只在训练集上做 SMOTE 过采样 smote SMOTE(random_state42) X_train_res, y_train_res smote.fit_resample(X_train, y_train) print(过采样后训练集正例数:, (y_train_res 1).sum()) print(过采样后训练集负例数:, (y_train_res 0).sum())逻辑说明stratifyy做分层切分保证训练集和测试集里死亡样本的占比和原始数据一致这在正例只有百分之几的情况下尤其重要。SMOTE 只在X_train上执行X_test始终保持真实分布最后测试集上算出来的准确率才有参考价值。参数说明test_size0.2在几百条样本时是常用选择如果样本量低于 200建议改成 0.15 或者直接用交叉验证替代单次切分。random_state42固定后每次运行得到完全相同的切分结果这是可复现的基础也是给评审老师一个交代。3. 特征工程与模型选型把临床指标训出 95% 准确率3.1 特征筛选先看相关性再决定去留数据清洗完之后下一步是特征工程。临床数据的特征是“少而精”不需要像图像那样从高维里硬提。先做一次相关性分析找到冗余特征。比如收缩压和心率在休克患者身上会同时异常肌钙蛋白和 Killip 分级也存在相关性。特征高度相关时模型会把权重在两个特征之间来回分配导致可解释性变差。import matplotlib.pyplot as plt import seaborn as sns # 只对数值特征做相关性热图 plt.figure(figsize(10, 8)) sns.heatmap(df[feature_cols].corr(), annotTrue, cmapRdBu_r) plt.tight_layout() plt.savefig(corr_matrix.png, dpi150)逻辑说明df[feature_cols].corr()默认计算皮尔逊相关系数输出一个方阵热图里颜色越深相关性越强。如果看到两列特征的相关系数绝对值大于 0.8比如收缩压和平均动脉压就考虑去掉其中一个。注意这个相关系数只对连续数值特征有意义像 killip 这样的有序分类特征和 st_segment 这样的名义分类特征相关性需要用别的方法看别混在一起直接算。参数说明annotTrue在热图格子里显示具体数值方便直接读出相关系数savefig保存图片后面写文档可以直接引用。3.2 模型选型逻辑回归、随机森林还是 XGBoost特征准备好之后面对的第一个问题是选什么模型。我见过很多学生会直接上 XGBoost理由是网上都说它效果好。但医疗小样本场景下模型选择的核心逻辑不是“哪个上限高”而是“哪个在数据量不足时最不容易崩”。下面这张表是我的经验总结模型小样本适应度可解释性默认参数表现适用场景逻辑回归好强稳定但上限一般基线模型答辩时做对比随机森林好中较好默认首选参数不敏感XGBoost好中好但易过拟合追求准确率上限时使用LightGBM好中好特征数量超过 50 时更优我的建议是逻辑回归、随机森林、XGBoost 三个都跑然后用测试集结果说话。这个项目里数据量不大三个模型训练时间加起来也就几分钟没必要一开始就赌一个。而且答辩时“我对比了三个模型并解释了选型理由”比“我用了 XGBoost”要扎实得多。这也是机器学习算法课设评审时最常见的加分点。3.3 训练与评估一套完整的建模代码下面这段代码覆盖了随机森林和 XGBoost 的训练、预测、评估。注意几个细节随机森林用限制深度控制过拟合XGBoost 用早停控制迭代轮数。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score, ConfusionMatrixDisplay import xgboost as xgb import matplotlib.pyplot as plt # 随机森林限制深度和叶子大小防止过拟合 rf RandomForestClassifier( n_estimators300, max_depth5, min_samples_leaf5, random_state42, n_jobs-1 ) rf.fit(X_train_res, y_train_res) y_pred_rf rf.predict(X_test) y_prob_rf rf.predict_proba(X_test)[:, 1] print(随机森林 AUC: {:.4f}.format(roc_auc_score(y_test, y_prob_rf))) print(classification_report(y_test, y_pred_rf, digits4)) # XGBoost低学习率配合早停 xgb_model xgb.XGBClassifier( n_estimators500, max_depth4, learning_rate0.03, subsample0.8, colsample_bytree0.8, random_state42, eval_metriclogloss ) xgb_model.fit( X_train_res, y_train_res, eval_set[(X_test, y_test)], early_stopping_rounds50, verboseFalse ) y_prob_xgb xgb_model.predict_proba(X_test)[:, 1] print(XGBoost AUC: {:.4f}.format(roc_auc_score(y_test, y_prob_xgb))) print(classification_report(y_test, xgb_model.predict(X_test), digits4)) # 画混淆矩阵 ConfusionMatrixDisplay.from_estimator(rf, X_test, y_test) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)逻辑说明随机森林里max_depth5限制树深度min_samples_leaf5强制每个叶子节点至少包含 5 个样本两个参数配合能有效避免模型记住个别患者的数据。XGBoost 里learning_rate0.03调低学习率让模型每一步走得更小配合early_stopping_rounds50在验证集 logloss 连续 50 轮不下降时自动停止防止过拟合。参数说明early_stopping_rounds是 XGBoost 在 sklearn 接口下最实用的参数但要注意它依赖eval_set里的数据这个数据用的是X_test本质上相当于在做模型选择时偷看了测试集。严格做法是再切一块验证集出来用于早停把测试集留到最后评估。如果数据量太少没法再切至少在文档里说明这个风险。3.4 调参的关键从默认参数出发小步试很多人调参喜欢一上来就网格搜索参数范围写得很大一跑就是半小时出来的结果还不一定好。医疗小样本项目没那么多数据量可供挥霍我一般按这个顺序调先固定随机种子用默认参数跑一遍拿到基线指标然后只调max_depth从 3 到 7 逐个试再调min_samples_leaf从 3 到 10最后调n_estimators。每调一个参数只改一处记录结果。全部调完再用交叉验证确认稳定性。不要同时改三四个参数那样翻车了你根本不知道是哪一步导致的。准确率想要站上 95%光靠调参不够特征要够强。肌钙蛋白、Killip 分级、左室射血分数这三列是死亡风险预测的硬指标缺了任何一个准确率都很难上去。这也是为什么我一直强调拿到数据先理解每个特征的临床含义比上来就建模重要得多。4. 医疗预测必踩的 5 个坑从数据泄漏到指标误读4.1 归一化写在切分之前造成数据泄漏现象训练集准确率 0.97测试集准确率 0.96自己觉得很稳。结果把模型拿到另一批数据上一试直接掉到 0.70完全没法用。原因写代码时图省事先对全量 X 做了StandardScaler().fit_transform()然后再train_test_split。这一步就把测试集的均值和方差混进了训练过程模型在训练时已经“看过”测试集的分布信息了。这类数据泄漏在医疗项目里最常见但很多人意识不到。解决记住一个顺序原则——先切分再在训练集上fit数据变换器然后分别transform训练集和测试集。最省心的做法是用 sklearn 的 Pipeline 把标准化和模型绑在一起这样永远不会忘记顺序。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipeline Pipeline([ (scaler, StandardScaler()), (rf, RandomForestClassifier(random_state42)) ]) pipeline.fit(X_train_res, y_train_res)逻辑说明Pipeline 在fit时先对训练集计算均值和方差再对训练集做标准化然后训练模型在predict时用训练集算好的均值和方差去变换测试集。这个机制从结构上杜绝了数据泄漏的可能。4.2 混淆矩阵里全是“存活”准确率却高达 95%现象准确率 95.2%分类报告里 precision 和 recall 都很好看结果一看混淆矩阵测试集里几十个死亡样本只抓住了两三个。模型等于把所有人都预测成活靠类别分布蒙对了 95% 的准确率。原因验证集保持了真实的类别分布死亡样本只占 5%。模型学到了“大部分人都活着”这个先验知识把所有样本往多数类推。分类报告里的 macro 平均指标被多数类拉高了单看数字完全看不出问题。解决每次训练完先看混淆矩阵再报告 AUC、F1、recall 这几个指标。医疗场景里宁可把低风险患者误判成高风险也不能漏掉真正的高危患者。用class_weightbalanced或者在训练集上做 SMOTE都能强迫模型关注少数类。更进阶的做法是调分类阈值——sklearn 里predict_proba输出的概率默认以 0.5 为界你可以画出 F1 随阈值变化的曲线找到最优分割点。4.3 缺失值全按整体均值填模型学到错误分布现象模型训练出来之后特征重要性排序里肌钙蛋白排第一但画出来的 SHAP 图上它和死亡风险的方向关系和临床认知矛盾——肌钙蛋白越高应该越危险图上显示却是反的。原因缺失值全部用全局均值填充把死亡组原本应该很高的肌钙蛋白值压低了。模型学到的是一个被扭曲的规律同样是“偏高”但它无法区分是真实偏高还是均值填充带来的伪偏高。解决按临床分组填充前面已经给过groupby(killip)的代码。更精细的做法是用sklearn.impute.IterativeImputer做多重插补把每个有缺失的特征当作其他特征的目标变量来预测。填充完之后画一次分布对比图确认填充值和原始值在同一量级。这一条是最容易被忽略的坑但也是最值得写在文档里的亮点。4.4 不固定随机种子复现结果像开盲盒现象同一个模型、同一份数据昨天跑出来准确率 95%今天跑变成 91%换台机器又变成 93%。导师帮忙看代码跑出来的结果和你报告上写的完全对不上。原因train_test_split、SMOTE、随机森林这些操作内部都有随机性。不设置random_state每次运行数据划分不同、采样结果不同、树分裂的依据也不同结果自然每次都不一样。解决所有带随机性的操作全部固定随机种子。train_test_split加random_state42SMOTE 加random_state42模型里也加。更稳妥的做法是把切分后的四份数据直接保存成文件后面所有实验都从文件读取彻底消除切分带来的随机性。这相当于给项目买了一份后悔药任何时候都能回到最初的划分。4.5 只看 AUC忽略概率校准现象XGBoost 输出的预测概率普遍偏大预测 0.6 的患者实际死亡比例只有 0.3模型输出的“概率”跟真实风险对不上。答辩时评委问“这个患者风险概率 0.85 意味着什么”答不上来。原因AUC 衡量的是模型把正例排在负例前面的能力不保证概率值本身是校准的。集成树模型的输出概率往往系统性偏移尤其是在类别不平衡数据上训练之后。解决画校准曲线calibration curve计算 Brier 分数。如果发现概率偏移明显用CalibratedClassifierCV包在模型外层做 Platt 缩放。能在答辩时说出“我做了概率校准Brier 分数从 0.21 降到 0.17”这个项目的水准立刻就不一样了。from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV(xgb_model, methodsigmoid, cv3) calibrated.fit(X_train_res, y_train_res)5. 从 95% 到可信用 SHAP 解释、外部验证和文档让项目真正交付准确率跑到 95% 只是第一步项目能不能站住脚取决于你能不能让评委相信这个数字不是靠数据泄漏或者样本不平衡刷出来的。我习惯在最后阶段补三件事SHAP 可解释性分析、外部验证、文档整理。SHAP 是解释树模型最直观的工具能画出每个特征对单个预测的贡献方向。医疗场景里这特别有用——模型说某个患者死亡风险高你得能回答“为什么高”。import shap explainer shap.TreeExplainer(xgb_model) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, feature_namesfeature_cols, max_display15)逻辑说明TreeExplainer专门用于树模型summary_plot把每个样本的 SHAP 值按特征聚合画在一张图上。横坐标是 SHAP 值正值表示推高死亡风险负值表示降低风险。肌钙蛋白和 Killip 分级通常排在最前面这就是你向评委展示的“模型到底学了什么”的证据。外部验证这块如果数据里带了时间字段按时间把前 70% 作为训练集、后 30% 作为测试集这样模拟的是一个真实的场景用过去的数据预测未来入院的患者。如果没有时间字段至少把 SMOTE 之后的结果和 SMOTE 之前的结果各报告一遍证明高准确率不是工程技巧堆出来的假象。文档组织上我一般交付四样东西一个 README 写清楚运行环境和启动命令一个requirements.txt锁定 Python 版本和依赖包版本训练脚本和评估脚本分开最后是设计文档。设计文档里重点写三件事——为什么选机器学习而不是深度学习数据量只有几百条深度学习很容易过拟合、如何处理类别不平衡、如何验证模型的泛化能力。答辩 PPT 就用三张图撑起来AUC 曲线、混淆矩阵、SHAP 图。我做过的几个医疗类课设里真正让项目拉开差距的从来不是准确率小数点后第二位而是你是否能说清楚模型的边界和局限。这是我从第一次拿到 95% 准确率傻乐到后来被评委问到“你的模型敢给真实患者用吗”时被问住用一次翻车换来的习惯。希望这些步骤和坑能帮你少走一段弯路把项目做成一个踏踏实实能交付的完整作品希望帮到你。本文还有配套的精品资源点击获取
