简介:基于Python机器学习的急性心肌梗死死亡风险预测项目,面向毕业设计、课程设计及项目开发场景,适合需要快速搭建二分类预测模型或学习医学数据挖掘的学生与开发者。项目依托MIMIC数据库中的离散检查指标,完成从SQL数据抽取、数据清洗、OneHot特征编码到XGBoost、LightGBM模型训练与评估的完整流程,报告准确率达95%以上,能直观展示机器学习在医疗风险预测中的应用路径与关键步骤。资源压缩为zip包,共14个文件,大小约5.7MB,核心为4个Python脚本,分别对应数据预处理、OneHot编码和不同模型训练,同时附带3个CSV数据文件、2个SQL查询脚本,以及Excel、TXT、Markdown说明文档和license文件,文件类型覆盖从数据获取到模型评估的各个环节,便于按功能模块对照阅读。目前已有237人学习/下载。除核心建模代码外,还提供白细胞计数提取等数据库查询语句和项目说明文档,可帮助读者理解MIMIC数据结构、窗口函数用法及特征构建思路;源码经过严格测试,可放心参考并在此基础上做模型调优、新特征尝试或换用其他数据验证,适合作为毕业设计或课程设计的完整基础框架,具有较强的扩展性和学习价值。
1. 急性心肌梗死死亡风险预测:为什么95%准确率反而要警惕
急性心肌梗死死亡风险预测,是典型的“小样本、高维度、强类别不平衡”医疗机器学习问题。你拿到的训练数据可能就几百到几千条,死亡事件占比不到20%,但涉及几十个临床字段——这种组合让入门者最容易栽在“准确率”上。搜到这个标题的人,多半是在做毕业设计或课程设计,需要一个能跑通、能写进论文、能答辩的项目。基于Python和机器学习的急性心肌梗死死亡风险预测,流程上并不复杂:数据清洗、特征筛选、训练分类器、输出评估;复杂的是,标题里“准确率达95%以上”这个数字,在医疗预测场景里往往不是好消息,而可能是数据泄露、类别失衡或评估方式出错的信号。这篇文章会把从数据准备到模型落地的完整路径拆开讲。
2. 数据准备:心梗数据集长什么样、缺失值怎么处理
2.1 心肌梗死数据集的常见字段与标签定义:院内死亡、30天死亡区别很大
做这个项目,第一步要搞清楚你预测的到底是什么。同样是“死亡风险”,标签可以定义为院内死亡、出院后30天内死亡,或者随访期内全因死亡。三种标签对应完全不同的数据收集成本和临床意义。毕业设计最常见的选择是院内死亡——它只需要从住院记录里取最终结局,标签干净,不会牵扯出院后失访的问题。
特征侧的字段,一份可用的心梗数据集通常分为四组。人口学与病史组:年龄、性别、高血压史、糖尿病史、既往心衰史、卒中史。生命体征组:入院收缩压、舒张压、心率、呼吸频率、体温。实验室检查组:肌钙蛋白I/T、CK-MB、肌酐、BNP或NT-proBNP、白细胞计数、血红蛋白、血小板、血糖。心电图相关组:ST段抬高与否、是否新发左束支传导阻滞、心梗部位(前壁/下壁/非ST段抬高型)。
这四组字段不是拍脑袋凑的。GRACE评分和TIMI评分这两个临床公认的风险评估工具,用的核心变量就是年龄、心率、收缩压、肌酐、ST段偏移和心衰标志物。你的机器学习模型本质上是在拟合临床评分已经验证过的这些关系,所以特征工程阶段不要丢掉这些先验知识。如果你用的是公开的心血管数据集,先确认它包含的字段能映射到上面至少两组,否则预测效果和答辩说服力都会大打折扣。
这里的机器学习中的数据处理是什么,对大多数入门者来说没有直观概念。放到心梗数据上你就明白了:它不只是删空值、改格式,而是要把“缺失”“未检测”“正常范围”这些医疗语义翻译成模型能吃的结构化输入。这一步做得好不好,直接决定后面模型是学到真实风险信号,还是学到数据收集流程的噪音。
2.2 缺失值处理:均值填充之外还要补一列“是否检测过”
医疗数据的缺失机制比普通业务数据复杂得多。一份常见的心梗记录里,肌钙蛋白可能只有60%到70%的样本有数值——没测的原因往往是入院时症状不典型、基层医院没有检测条件、或者在转院前已经测过。这里的“没测”本身就携带临床信息,把它当作普通缺失用均值抹掉,等于主动扔掉一部分预测信号。
常见做法是双轨处理:对关键变量,要么加一列“是否检测过”的二值标志,要么把缺失单独编码为一类;连续特征本身用中位数填充。以下代码是我通常会用的处理方式:
import pandas as pd import numpy as np # 读取脱敏后的电子病历导出表 df = pd.read_csv('ami_data.csv') # 1. 先看缺失概况,缺失率超过80%的列直接考虑删除 missing_ratio = df.isnull().mean().sort_values(ascending=False) print("各列缺失率:") print(missing_ratio[missing_ratio > 0.05]) # 2. 给高缺失临床变量加“是否检测过”标志 # 标志列把缺失机制变成显式特征,让模型自己学“没测”的含义 for col in ['troponin_i', 'bnp', 'ck_mb']: df[col + '_missing_flag'] = df[col].isna().astype(int) # 3. 连续变量用中位数填充,避免极端值被均值拉扯 for col in ['troponin_i', 'bnp', 'ck_mb']: df[col] = df[col].fillna(df[col].median()) # 4. 类别变量统一填充为“unknown”,保留缺失类别而非删行 cat_cols = df.select_dtypes(include=['object']).columns for col in cat_cols: df[col] = df[col].fillna('unknown') # 5. 缺失率超过80%的列整列删除,保留有效信息密度 df = df.drop(columns=[col for col in df.columns if df[col].isnull().mean() > 0.8])这段代码的用意分三层。第一层,col + '_missing_flag'这步是把缺失机制本身变成特征,模型能学到“没测肌钙蛋白”和“肌钙蛋白正常”是两种不同的临床状态。第二层,中位数填充的选择是因为肌钙蛋白、BNP这类指标分布长尾严重,均值会被少数超高值带偏,中位数更稳健。第三层,类别变量不删行也不填特殊值,而是直接用'unknown'占位,保证后续建模时分类编码不会报错,也不会因为删行丢失样本。
有一个容易忽略的细节:填充一定要放在特征工程之前,但missing_flag列要保留到训练阶段一起喂给模型。如果你在特征筛选时把标志列删了,等于又把“未检测”这个信息扔回去了。
2.3 训练集划分:按patient_id分组,别让同一病人两头露面
在医疗数据上做随机划分是高风险动作,原因有二。第一,同一病人可能多次住院,随机划分会把同一个人的两次住院分别扔进训练集和测试集,模型等于提前“见过”这个人——验证指标会虚高。第二,死亡事件占比低,不控制比例的话,测试集里可能几乎没有死亡样本,评估没有任何意义。常见做法是使用GroupShuffleSplit或按时间切分,保证同一个人只在一边出现:
from sklearn.model_selection import GroupShuffleSplit # df 中必须有 patient_id 列,标识每次住院属于哪个病人 # y 是标签列:in_hospital_death,1为院内死亡,0为存活出院 y = df['in_hospital_death'] gss = GroupShuffleSplit( n_splits=1, test_size=0.2, random_state=42 ) train_idx, test_idx = next( gss.split(df, y=y, groups=df['patient_id']) ) train_df = df.iloc[train_idx].reset_index(drop=True) test_df = df.iloc[test_idx].reset_index(drop=True) # 验证两个集合里的死亡比例,避免出现测试集全是存活样本的情况 print("训练集死亡比例: %.3f" % train_df['in_hospital_death'].mean()) print("测试集死亡比例: %.3f" % test_df['in_hospital_death'].mean())GroupShuffleSplit与标准train_test_split的最大区别在groups参数。它允许你在切分时设置一个“分组约束”,同一个patient_id的所有行必须同时进入训练集或测试集。test_size=0.2控制测试集占20%,random_state=42固定随机种子,保证每次运行切分结果一致。切分后顺手打印两边的死亡比例,如果测试集比例明显偏离训练集,建议换一个随机种子或改用分层与分组结合的方式。
需要注意一点:这里没有用stratify参数。GroupShuffleSplit本身不支持直接的stratify,但分组约束往往比分层更重要。如果数据量足够,你也可以先用train_test_split按y分层切出20%当测试集,再在剩下的80%里用KFold做交叉验证——不过前提仍然是patient_id已经在上一层分好了,不能混。
3. 特征工程与模型选型:95%准确率是怎么来的、含多少水分
3.1 特征筛选的第一原则:只用入院24小时内能拿到的变量
特征筛选在这个项目里有一个铁律:只能使用入院24小时内可获取的变量做预测。为什么这条如此重要?因为心梗后的干预手段——急诊PCI、溶栓、冠脉搭桥、血管活性药物——都发生在住院期间,而这些治疗决策本身与死亡风险强相关。如果模型拿“是否接受急诊PCI”当特征,它学到的不是死亡风险,而是治疗方案选择的映射关系。这在临床上是致命的逻辑错误:你的模型看起来准确率极高,实际部署时这些治疗特征根本不存在于入院时刻。
具体做法是建立一个特征白名单。入院时必得的变量有:年龄、性别、心率、收缩压、舒张压、ST段偏移情况、肌钙蛋白初值、肌酐、既往病史。住院后才出现的变量有:是否PCI、是否用主动脉内球囊反搏、是否使用机械通气、24小时后复查的肌钙蛋白峰值。后者无论如何都不能进特征集。很多公开数据集在导出时不做这个区分,你要对照数据字典手动剔除。
3.2 模型选型:小样本表格数据不该一上来就上深度学习
很多课程设计爱好者第一反应是上神经网络,这是一个代价不小的错误。心梗死亡预测数据集通常只有几百到几千条样本、几十个特征,深度学习在这种规模下极易过拟合,且训练慢、调参复杂、解释性差。答辩时老师问“为什么某个病人被预测为高风险”,深度学习给不出合理的答案。
在这个场景下,我的经验排序是:逻辑回归做基线,随机森林做稳健对照,LightGBM或XGBoost做最终模型。逻辑回归的价值不在于效果,而在于给整个项目一个“分数”——如果集成模型比逻辑回归提升不明显,说明特征工程没做好;反过来,如果集成模型提升很大,也要小心是不是过拟合。关于“机器学习算法哪家强”的问题,在小样本表格数据上,树模型和梯度提升通常稳定压过神经网络。
| 模型 | 适合场景 | 主要优点 | 主要风险 |
|---|---|---|---|
| Logistic Regression | 基线对照 | 稳定、可解释、不容易翻车 | 欠拟合非线性关系 |
| RandomForest | 中等数据量 | 抗过拟合能力好 | 对噪声敏感 |
| LightGBM | 表格数据竞赛标配 | 速度快、能吃到特征交互 | 小样本下需要强正则 |
| MLP / 深度学习 | 大规模数据 | 能学到复杂表征 | 小样本严重过拟合 |
3.3 跑通基线模型:先看AUC,别把准确率当主指标
准确率95%听起来很美,但在心梗死亡预测里几乎必然含水。假设你的数据集里有12%的死亡样本,一个什么都不学、把所有病人预测为存活的“模型”,准确率就有88%。在此基础上稍微学一点点存活模式,就能轻易到92%到95%。这就是为什么机器学习项目里准确率在类别不平衡时是“安慰剂指标”——毕业设计答辩时,主审老师一眼就能识破。
正确的主指标是ROC曲线的AUC值,辅助指标是敏感度和特异度组合。AUC衡量的是模型把死亡病人排在存活病人前面的能力,不受类别比例影响。以下代码用于快速验证一个特征白名单是否有效:
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score # X 只保留白名单内的基线特征列 feature_cols = ['age', 'heart_rate', 'sbp', 'dbp', 'troponin_i', 'creatinine', 'st_elevation'] X = train_df[feature_cols] y = train_df['in_hospital_death'] # 用随机森林做快速验证,5折交叉验证看AUC rf = RandomForestClassifier( n_estimators=300, max_depth=4, min_samples_leaf=10, random_state=42 ) scores = cross_val_score(rf, X, y, cv=5, scoring='roc_auc') print("5折AUC: %.3f ± %.3f" % (scores.mean(), scores.std()))交叉验证的每个fold都会重新训练并评估一次模型,输出的AUC均值反映了特征组合的稳定预测能力。如果5折AUC在0.75到0.85之间,说明特征集是有效的;如果AUC只有0.5到0.6,说明特征白名单选得不对或预处理有问题,先去检查缺失值处理。如果AUC高达0.99,第一反应不是高兴,而是回头检查有没有把治疗特征或标签本身的衍生变量混进来了。
4. 训练与评估的源码落地:LightGBM五折交叉验证与关键参数
4.1 源码结构:训练、评估、预测一条线
毕业设计不仅看模型效果,更看工程组织能力。常见做法是把项目拆成下面几个文件而不是一个脚本写完:
| 文件 | 职责 | 关键输出 |
|---|---|---|
data_preprocess.py | 读取原始数据、缺失值处理、分组划分 | 清洗后的train_df.csv |
feature_engineering.py | 特征白名单筛选、标志列生成 | 最终特征矩阵 |
train.py | LightGBM五折训练、早停、保存模型 | model.txt、折数AUC |
evaluate.py | 测试集评估、ROC曲线、阈值选择 | 评估报告与图表 |
predict.py | 加载模型、对单条新样本输出风险概率 | 死亡概率与风险等级 |
requirements.txt | 固定依赖版本 | 环境可复现 |
这个结构的核心思想是“数据和模型分离”。答辩时老师会问“换一份数据能不能直接跑”,如果你所有处理都在一个脚本里且硬编码了路径,这个问题就把你问住了。数据预处理和特征工程代码中所有路径都用参数传入,训练程序只接收文件路径与模型路径,就能做到换数据不换代码。
4.2 LightGBM五折训练代码:关键参数与控制过拟合
LightGBM是心梗死亡预测这类任务最常用的模型。五折交叉验证的目的是用全部训练数据而不是只用一个验证集来评估,同时每折的模型独立训练,避免单次划分带来的偶然性。以下代码是可以直接抄的骨架:
import lightgbm as lgb import numpy as np from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score params = { 'objective': 'binary', 'metric': 'auc', 'learning_rate': 0.03, 'num_leaves': 15, 'max_depth': 4, 'min_child_samples': 20, 'feature_fraction': 0.7, 'bagging_fraction': 0.7, 'bagging_freq': 1, 'lambda_l2': 1.0, 'verbosity': -1, } # X 是特征矩阵,y 是标签列 X = train_df[feature_cols] y = train_df['in_hospital_death'] skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof_pred = np.zeros(len(X)) auc_scores = [] for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx] y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx] dtr = lgb.Dataset(X_tr, label=y_tr) dva = lgb.Dataset(X_val, label=y_val, reference=dtr) model = lgb.train( params, dtr, num_boost_round=1000, valid_sets=[dva], callbacks=[lgb.early_stopping(50)] ) # 保存每个fold的模型,最后评估时可以选最优折 model.save_model(f'lightgbm_fold{fold}.txt') val_pred = model.predict(X_val, num_iteration=model.best_iteration) oof_pred[val_idx] = val_pred auc_scores.append(roc_auc_score(y_val, val_pred)) print(f"Fold {fold + 1} AUC: {auc_scores[-1]:.4f}") print("平均AUC: %.4f ± %.4f" % (np.mean(auc_scores), np.std(auc_scores)))params里的每一项都值得在答辩时展开讲。num_leaves=15配合max_depth=4限制了树模型的复杂度,这是小样本场景下最重要的防过拟合手段——树分裂太深会把训练集的噪声学进去。min_child_samples=20强制每个叶子节点至少包含20个样本,防止个别离群样本独占一个叶子。feature_fraction=0.7让每棵树只能随机使用70%的特征,这能有效防止模型反复依赖同一组强相关特征导致的多重共线性问题。
early_stopping(50)的作用是在验证集AUC连续50轮不提升时提前终止训练,同时保存最佳迭代数。num_boost_round=1000是最大迭代次数的上限,实际训练中通常几百轮就会触发早停。StratifiedKFold的shuffle=True在每折划分前打乱顺序,配合固定random_state=42保证实验可复现。
4.3 死亡风险预测的阈值:默认0.5会漏掉八成危重病人
训练完成后,模型输出的是一个死亡概率值,默认把大于0.5判断为“会死亡”是最粗糙的做法。在心梗死亡这种死亡比例只有10%到15%的数据里,模型输出的概率普遍偏低,0.5阈值会导致几乎没有人被预测为死亡,敏感度惨不忍睹。常见做法是用约登指数(Youden's J)在ROC曲线上找最优切点:
from sklearn.metrics import roc_curve # oof_pred 是交叉验证时留一折预测的概率 fpr, tpr, thresholds = roc_curve(y, oof_pred) youden = tpr - fpr best_idx = np.argmax(youden) best_threshold = thresholds[best_idx] print("最优阈值: %.4f" % best_threshold) # 在这个阈值下的敏感度与特异度 print("敏感度: %.4f" % tpr[best_idx]) print("特异度: %.4f" % (1 - fpr[best_idx]))约登指数最大化“真阳性率减假阳性率”,本质是在敏感度和特异度之间找平衡点。在心梗死亡预测中,宁可牺牲一点特异度也要把高危病人捞出来,所以实际选择时可以在约登指数基础上手动向高敏感度方向再偏移0.02到0.05。阈值是评估报告里的重要参数,不要只在代码里写死,要在文档里说明“为什么是0.25而不是0.5”。
5. 医疗预测的五个典型坑:从数据泄露到指标错配
做心梗死亡预测的人,很多前几次都会在同样的地方翻车。以下五条是把这类项目最常见的问题按现象到原因到解决方式整理出来的踩坑记录,属于血泪经验。
5.1 现象:AUC冲到0.99,原因:治疗特征造成时间窗泄露
这是心梗预测里最隐蔽也最致命的错误。特征列表里混入了“是否接受PCI”“是否使用血管活性药物”“是否机械通气”这类治疗变量,模型AUC瞬间冲到0.95以上。原因完全违背了预测的方向性:治疗方案本身是医生根据死亡风险制定的,死亡风险高的病人更容易接受有创干预——模型学到的是“治疗与结局的关联”,而不是“入院特征对结局的预测”。解决方法是回归特征白名单,只保留入院24小时内必得的基线特征。建议在特征表里加一列“可获取时点”,标注每个特征入院当天能否拿到,这一列直接写入项目文档,答辩时能证明你理解数据语义。
5.2 现象:准确率92%但AUC只有0.61,原因:指标错配
训练完成后输出一行“准确率: 92%”,看起来很棒,但交叉验证AUC只有0.61。原因在于数据集死亡比例只有12%左右,模型把所有样本都判为存活就能拿到88%的准确率,再学会少量存活模式就能到92%。准确率在这个场景下不反映任何临床价值。解决方式是以AUC作为主指标,辅助看敏感度和特异度。在评估报告里同时列出三个数字:AUC、敏感度、特异度,并在论文里解释准确率为何不是首选指标。已经有数据显示,预测模型在类别不平衡数据集上使用准确率评估,会产生严重的乐观偏差。
5.3 现象:同一个病人的两次住院分别进了训练集和测试集
验证集AUC明显高于同样本量应有的水平,排查时发现同一patient_id的多次住院记录被随机分到了不同集合。原因是用普通train_test_split切分,没有按病人分组。模型在训练时见过这个病人的其他住院记录,测试时对这个病人近期的入院记录做预测,等于开卷考试。解决方式改用GroupShuffleSplit按patient_id分组,代码在2.3节已经给出。如果数据集没有病人ID字段,至少也要按入院时间切分,前70%作为训练集、后30%作为测试集,避免时间穿越。
5.4 现象:补上“肌钙蛋白未检测”标志列后AUC显著提升
第一次建模时把肌钙蛋白缺失值用中位数填掉,然后删掉了原始缺失信息,AUC只有0.72。后来在特征里保留troponin_i_missing_flag这一列,AUC升到0.82。原因在于临床语义:没测肌钙蛋白不代表正常,可能代表症状不典型、错过了检测窗口、或基层医院检测能力不足——缺失本身是一个有区分度的临床状态。解决方式是严格按2.2节的双轨处理,所有关键实验室指标都保留缺失标志列,哪怕填充前后数值不变,标志列也一定要进特征集。
5.5 现象:SMOTE之后验证集虚高、独立测试集崩盘
为了缓解类别不平衡,对训练数据做了SMOTE过采样,之后交叉验证AUC涨到0.98,但拿一份彻底没见过的独立测试集评估只有0.70。原因在于过采样的时机放错了:先对整个数据集过采样再做交叉验证,SMOTE合成的样本会同时出现在训练折和验证折中,让模型在验证时“看到”训练样本的合成近邻。解决方式是把过采样放进每一折交叉验证内部,即每一折先用训练折做SMOTE,验证折保持原始分布。除此之外,对心梗死亡预测我通常不建议用SMOTE,改用class_weight='balanced'或直接在LightGBM中设置is_unbalance=True效果更稳。
6. 最后一步:校准曲线和SHAP解释,让模型不再是个黑匣子
6.1 校准曲线:预测0.9就要有90%的人真的死亡
AUC只回答“排序对不对”的问题,不回答“概率准不准”的问题。临床场景里,给病人报一个风险概率,这个概率本身要有意义——预测死亡概率0.9的一组病人,真实死亡比例应该接近90%。评价格式是校准曲线和Brier分数,Brier分数是真实结局与预测概率的均方误差,越低越好,0.2以下基本可用。绘制校准曲线在Python里只需要sklearn.calibration模块配合matplotlib,把测试集样本按预测概率分箱,对比每个箱内的平均预测概率和实际死亡比例,曲线越贴近对角线越好。这个图放在答辩PPT里比罗列AUC有说服力得多。
6.2 用SHAP给答辩加分的做法
模型选用了LightGBM这类树模型,最大优势是能配合SHAP做个体解释。给每个病人的预测结果输出一张里水图,展示年龄、肌钙蛋白、心率各自对死亡概率的贡献方向。以下是一段可以直接跑的代码:
import shap # 加载最优折的LightGBM模型 model = lgb.Booster(model_file='lightgbm_fold0.txt') # 用测试集计算SHAP值,background是特征均值样本集 explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(test_df[feature_cols]) # 对第一个测试样本画个体解释图 shap.initjs() shap.force_plot( explainer.expected_value, shap_values[0, :], test_df[feature_cols].iloc[0, :] )SHAP在这里不是锦上添花,而是直接回应“黑匣子”质疑的答案。某位病人被判定死亡风险高,图里能清楚看到“年龄78岁贡献+0.12、肌钙蛋白超高贡献+0.18、收缩压低贡献+0.07”,这比一个裸概率可靠得多。我做这类项目时养成的习惯是,每个病人的预测都必须附带一张SHAP图,确保任何一条高风险预测都能在特征层面找到对应解释。这个习惯让项目在盲审和答辩时的可信度高一个档次。
部署或写论文时,也别忘了给评估报告附上阈值选择依据和交叉验证完整结果。回到准确率95%这个话题——如果有一天你在报告里看到这个数字,先别高兴,去检查训练集和测试集有没有重叠、特征里有没有治疗后变量、评估指标用的是不是AUC。这三点都干净了,95%才可能真实可信。希望帮到你。
本文还有配套的精品资源,点击获取