☰
基于Python与机器学习的急性心肌梗死死亡风险预测实战
2026/10/1 18:47:34 网站建设 项目流程

简介:本资源面向高校学生与开发者,提供一套基于Python与机器学习的急性心肌梗死死亡风险预测完整项目,适用于毕业设计、课程设计及项目开发练手。项目以MIMIC数据库数据为训练集,围绕重症监护场景下的死亡风险建模,涵盖数据预处理、特征工程与多种集成学习模型的训练流程,可作为医疗数据挖掘方向的入门参考。压缩包共14个文件,约5.7MB,包含4个Python脚本、3个CSV数据文件、2个SQL查询脚本,以及xlsx、txt、md说明文档与开源协议文件,脚本与数据分离,便于按模块阅读和二次开发。目前已有236人学习下载。读者可获得可运行的源码、数据读取与预处理脚本、模型训练代码及配套说明文档,并在此基础上替换数据集或调整特征,延伸出肾衰竭等其他疾病的风险预测实验,积累从数据清洗到模型评估的完整实践思路。

1. 急性心肌梗死死亡风险预测:从一份临床数据到能跑通的模型

急性心肌梗死的院内死亡风险预测,是机器学习在临床场景里少有的「特征清晰、标签明确、样本量够用」的落地方向。拿到一份包含年龄、肌钙蛋白、Killip 分级、射血分数、心率、血压等字段的表格数据,用 Python 加机器学习做二分类,把死亡风险分成高危和低危,这件事本身并不玄学。真正难的是:数据里缺失值怎么填、类别不平衡怎么处理、95% 的准确率到底是怎么来的、换一批数据还能不能稳住。这篇笔记就围绕「基于 Python + 机器学习的急性心肌梗死死亡风险预测」这个题目,把数据预处理、特征工程、模型选型、训练调参、评估验证、踩坑排查整条链路讲清楚,适合做毕业设计、课程设计或者想快速搭一个可复现基线的人。读完你能自己跑出一套完整流程,也能判断别人报的 95% 到底靠不靠谱。

2. 数据准备与特征工程:把临床表格变成模型能吃的矩阵

2.1 先搞清楚字段含义再动手

拿到数据第一件事不是pd.read_csv然后model.fit,而是逐列确认含义。急性心肌梗死预测里常见的字段大致分几类:人口学(年龄、性别)、生命体征(收缩压、舒张压、心率、呼吸频率)、实验室指标(肌钙蛋白、肌酸激酶、血糖、肌酐、白细胞)、心功能(左室射血分数 LVEF、Killip 分级)、病史(高血压、糖尿病、吸烟、既往心梗)、以及结局标签(院内死亡 0/1)。

这里有个血泪经验:Killip 分级是 I 到 IV 的有序类别,直接当连续数值喂进去,模型会误以为 IV 是 I 的 4 倍,实际它只是等级。正确做法是做 one-hot 或者保留序关系用有序编码。同理,性别、吸烟这类二值字段保持 0/1 即可,不要做标准化。

import pandas as pd import numpy as np df = pd.read_csv("ami_data.csv") # 确认标签分布,先看正负样本比例 print(df["death"].value_counts(normalize=True)) # 有序类别:Killip 分级保留顺序,用映射而不是 one-hot killip_map = {"I": 1, "II": 2, "III": 3, "IV": 4} df["killip"] = df["killip"].map(killip_map) # 无序类别:性别、吸烟等做 one-hot df = pd.get_dummies(df, columns=["sex", "smoke"], drop_first=True)

逻辑说明:value_counts(normalize=True)先看标签比例,这一步决定了后面要不要做重采样。Killip 用映射保留序关系,是因为它本身有轻重之分,one-hot 会丢掉这个信息。drop_first=True避免虚拟变量陷阱,防止多重共线性。

参数说明:normalize=True输出比例而非计数;drop_first=True在二分类字段上其实只保留一列,对性别这种二值字段效果就是 0/1 编码。

2.2 缺失值处理:别一上来就均值填充

临床数据缺失是常态,肌钙蛋白、LVEF 这些关键指标缺失率可能到 10% 到 30%。直接fillna(df.mean())是最省事也最容易翻车的做法,因为它会抹掉缺失本身的含义——很多时候「没测某项指标」本身就是病情轻或重的信号。

我一般分三步走:缺失率超过 40% 的字段直接考虑丢弃,除非它是核心指标;缺失率 5% 到 40% 的用多重插补或者 KNN 插补;缺失率低于 5% 的用中位数填充并加一个缺失指示列。

from sklearn.impute import KNNImputer # 先标记缺失,保留缺失模式信息 for col in ["troponin", "lvef", "creatinine"]: df[f"{col}_missing"] = df[col].isna().astype(int) # 对数值列做 KNN 插补,n_neighbors 取 5 是常用起点 num_cols = ["age", "sbp", "dbp", "heart_rate", "troponin", "lvef", "creatinine"] imputer = KNNImputer(n_neighbors=5) df[num_cols] = imputer.fit_transform(df[num_cols])

逻辑说明:先加_missing指示列,让模型知道这个值原本是缺失的,这在临床预测里经常能提升效果。KNN 插补用样本间相似度找邻居,比均值填充更贴近个体情况。

参数说明:n_neighbors=5是经验值,样本量小可以降到 3,样本量大可以升到 7 到 10。注意 KNNImputer 必须在训练集上 fit,再 transform 测试集,否则会数据泄漏。

2.3 类别不平衡:95% 准确率的陷阱就在这里

急性心肌梗死院内死亡率通常不高,正样本(死亡)可能只占 5% 到 15%。如果数据里 90% 是存活,模型全预测存活就能拿 90% 准确率,这种「准确率」毫无意义。所以看到别人报 95% 准确率,第一反应应该是问:正负样本比例多少?用的什么评估指标?

处理不平衡常见三种做法:SMOTE 过采样、类别权重调整、阈值移动。我一般优先用class_weight="balanced",因为它不改数据分布,只是让损失函数对少数类更敏感,工程上最稳。

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split X = df.drop(columns=["death"]) y = df["death"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) clf = RandomForestClassifier( n_estimators=300, max_depth=8, class_weight="balanced", random_state=42 ) clf.fit(X_train, y_train)

逻辑说明:stratify=y保证训练集和测试集的正负比例一致,避免随机切分导致某一侧正样本过少。class_weight="balanced"自动按类别频率反比给权重,少数类权重更高。

参数说明:n_estimators=300是树数量,太少欠拟合,太多训练慢且收益递减;max_depth=8控制树深,防止过拟合,临床数据特征不多时 6 到 10 比较合适。

3. 模型选型与训练:为什么树模型是这类表格数据的首选

3.1 逻辑回归、随机森林、XGBoost 怎么选

表格型临床数据,特征维度通常几十到上百,样本量几千到几万,这种场景下梯度提升树(XGBoost、LightGBM)和随机森林往往比深度学习更稳。逻辑回归可解释性最强,系数直接对应优势比,适合写论文时讲清楚每个因素的方向,但拟合非线性关系能力弱。

我的选型顺序是:先跑逻辑回归做基线,看 AUC 大概什么水平;再上随机森林看特征重要性;最后用 XGBoost 或 LightGBM 冲性能。如果三者差距不大,就选可解释性最好的那个,临床场景里医生更信能解释的模型。

from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.metrics import roc_auc_score # 逻辑回归必须标准化,树模型不需要 lr_pipe = Pipeline([ ("scaler", StandardScaler()), ("clf", LogisticRegression(class_weight="balanced", max_iter=1000)) ]) lr_pipe.fit(X_train, y_train) lr_prob = lr_pipe.predict_proba(X_test)[:, 1] print("LR AUC:", roc_auc_score(y_test, lr_prob))

逻辑说明:逻辑回归对特征尺度敏感,所以用 Pipeline 把标准化和模型绑在一起,避免测试集泄漏。predict_proba取第 1 列是正类概率,AUC 用概率算而不是硬标签。

参数说明:max_iter=1000防止不收敛警告;class_weight="balanced"同样处理不平衡。AUC 是评估不平衡数据最常用的指标,0.5 是随机,0.8 以上算不错,0.9 以上要警惕泄漏。

3.2 XGBoost 训练与早停

XGBoost 在表格数据上通常能比随机森林再高一点,但参数多,容易过拟合。关键是设好eval_set和early_stopping_rounds,让它在验证集 AUC 不再提升时自动停。

import xgboost as xgb dtrain = xgb.DMatrix(X_train, label=y_train) dtest = xgb.DMatrix(X_test, label=y_test) params = { "objective": "binary:logistic", "eval_metric": "auc", "max_depth": 5, "eta": 0.05, "subsample": 0.8, "colsample_bytree": 0.8, "scale_pos_weight": (y_train == 0).sum() / (y_train == 1).sum() } model = xgb.train( params, dtrain, num_boost_round=1000, evals=[(dtest, "test")], early_stopping_rounds=50, verbose_eval=100 )

逻辑说明:scale_pos_weight是不平衡数据的 XGBoost 版处理方式,等于负样本数除以正样本数。早停用测试集做验证在这里只是演示,严格做法应该从训练集再切一个验证集,测试集只在最后用一次。

参数说明:max_depth=5比随机森林浅,因为 boosting 是串行叠加,深树容易过拟合;eta=0.05学习率小一点配合更多轮数更稳;subsample和colsample_bytree都是 0.8,增加随机性防过拟合。

3.3 交叉验证:单次切分的 95% 不可信

单次 train_test_split 得到的准确率波动很大,换一个 random_state 可能从 95% 掉到 88%。要报可信的数字,必须做交叉验证,最好分层 K 折。

from sklearn.model_selection import StratifiedKFold, cross_val_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X, y, cv=cv, scoring="roc_auc") print("AUC mean: %.4f, std: %.4f" % (scores.mean(), scores.std()))

逻辑说明:StratifiedKFold保证每折正负比例一致,scoring="roc_auc"用 AUC 而不是准确率。输出均值和标准差,标准差大说明模型不稳定,需要检查数据或调参。

参数说明:n_splits=5是常用折数,样本少可以到 10,样本多 5 折够用。shuffle=True打乱顺序,避免数据本身有序带来的偏差。

4. 评估与可解释性:准确率之外必须看的指标

4.1 混淆矩阵、召回率、F1 怎么读

临床死亡风险预测里,漏报一个高危病人(假阴性)的代价远大于误报一个低危病人(假阳性)。所以召回率(Recall)比准确率重要得多。一个模型准确率 95% 但召回率只有 40%,意味着六成死亡病例被漏掉,这种模型不能上线。

from sklearn.metrics import classification_report, confusion_matrix y_pred = clf.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, digits=4))

逻辑说明:混淆矩阵四个格子分别是真阴、假阳、假阴、真阳。分类报告给出每类的精确率、召回率、F1。重点看死亡类(标签 1)的召回率。

参数说明:digits=4控制小数位。如果召回率低,可以调低预测阈值,比如把 0.5 改成 0.3,牺牲精确率换召回率。

4.2 特征重要性与 SHAP

论文和答辩里几乎一定会被问「哪些因素最重要」。随机森林和 XGBoost 都能输出特征重要性,但内置重要性对高基数特征有偏。SHAP 更可靠,能给出每个样本每个特征的贡献方向。

import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(dtest) shap.summary_plot(shap_values, X_test)

逻辑说明:TreeExplainer对树模型有精确快速实现。summary_plot画出每个特征对预测的贡献分布,点的颜色表示特征值高低,位置表示推动预测往正类还是负类。

参数说明:SHAP 计算量随样本数增长,样本大时可以抽样几百条算。shap_values对二分类返回的是正类贡献。

5. 避坑与排查:那些让 95% 变成 70% 的细节

5.1 数据泄漏:准确率虚高的头号原因

现象:交叉验证 AUC 0.98,换一批数据掉到 0.7。原因:标准化、插补、特征选择在切分之前对整个数据集做了,测试集信息泄漏进训练。解决:所有预处理放进 Pipeline,只在训练集 fit。

5.2 标签定义不一致

现象:模型效果忽好忽坏,同一份代码两次结果差很多。原因:死亡标签有的按院内死亡,有的按 30 天死亡,混在一起。解决:确认标签口径统一,必要时重新定义。

5.3 缺失指示列被标准化

现象:加了_missing列后效果反而变差。原因:0/1 指示列被 StandardScaler 标准化成负数,失去含义。解决:指示列不参与标准化,或者用树模型跳过标准化。

5.4 阈值默认 0.5

现象:召回率低,漏掉大量高危。原因:默认 0.5 阈值不适合不平衡数据。解决:在验证集上画 PR 曲线,选召回率满足要求的阈值。

5.5 过拟合到训练集

现象:训练集准确率 99%,测试集 85%。原因:树太深、特征太多、样本太少。解决:限制max_depth,加min_samples_leaf,用早停,减少特征。

6. 把模型跑稳的一个技巧:分层抽样加固定随机种子

最后说一个我踩过坑之后养成的习惯:所有涉及随机的环节都固定random_state,并且用分层抽样。这不是为了好看,是为了可复现。答辩时老师让你再跑一遍,结果对不上,解释起来很被动。

具体做法是把random_state=42贯穿 train_test_split、模型初始化、交叉验证的 shuffle。同时用StratifiedKFold而不是普通 KFold,保证每折正负比例一致。如果样本量允许,做 10 折比 5 折更稳,但训练时间翻倍,自己权衡。

还有一个细节:保存模型时用joblib而不是pickle,对 sklearn 模型兼容性更好,加载速度快。

import joblib joblib.dump(clf, "ami_death_model.pkl") loaded = joblib.load("ami_death_model.pkl") print(loaded.predict_proba(X_test[:5]))

逻辑说明:joblib.dump序列化模型,load反序列化。保存后重新加载预测,确认结果一致,这是上线前必做的一步。

参数说明:文件后缀.pkl是惯例,不是强制。大模型可以加compress=3压缩,但加载会慢一点。

验证模型是否真的稳,我的做法是:换三个不同的random_state各跑一遍交叉验证,看 AUC 均值波动是否在 0.02 以内。如果波动超过 0.05,说明数据或流程有问题,别急着报结果。这个习惯帮我挡掉过好几次「看起来很美」的假象。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询