机器学习心梗死亡风险预测:MIMIC数据库与XGBoost/LightGBM实战拆解

发布时间:2026/10/31 19:24:33
机器学习心梗死亡风险预测:MIMIC数据库与XGBoost/LightGBM实战拆解

简介:一套基于Python与机器学习的急性心肌梗死死亡风险预测项目,面向毕业设计、课程设计及项目开发,以MIMIC数据库为训练数据,采用XGBoost、LightGBM等模型构建死亡率预测流程,目标准确率达95%以上。压缩包共14个文件、5.7MB,包含4个Python脚本,覆盖数据入库、预处理、OneHot编码与模型训练;3个CSV数据集、2个SQL查询脚本用于提取AMI与WBC相关指标;另有Excel表格、Markdown说明、License等文件,便于阅读和二次开发。已有237人浏览学习,适合快速搭建完整的机器学习项目框架,并作为毕设、课设的文档范本。需客观说明的是,资源作者坦言这项设计更多是流程演示:真实重症场景中医生直接结合心电图、彩超与冠脉造影判断更为准确,因此更适合教学练习、算法验证和项目报告撰写参考,而非临床决策工具。

1. 基于机器学习的心肌梗死死亡风险预测:为什么这个“灌水项目”值得拆

第一次看到这份源码包时,我其实愣了几秒。作者在文档里毫不掩饰地写了一句“当年本科水毕设的灌水项目,实际上没卵用”,理由是重症病人直接看心电图、做冠脉造影,准确率轻松都能到95%以上,何必用离散的检查指标硬算。这句话反而让我来了兴趣——因为一个被作者自嘲“没卵用”的项目,恰恰把机器学习的标准流程完整走了一遍:SQL取数、数据清洗、OneHot编码、XGBoost与LightGBM双模型训练、评估与文档整理,一链不缺。对正在做毕业设计或课程设计的开发者来说,这比一个包装精美的“AI医疗产品”更有参考价值。它适合三类人:想找一个能跑通全流程的机器学习选题的学生,想快速上手MIMIC医疗数据库的开发者,以及想理解“准确率95%到底意味着什么”的读者。接下来我按实际拆项目的顺序,把每个环节的代码、参数和坑一条条讲清楚。

2. 数据从哪来:MIMIC数据库的表结构与SQL取数

2.1 为什么选MIMIC作为训练数据源

这个项目的训练数据来自MIMIC数据库,这是重症监护领域最常用的公开医学数据库之一。对毕设和课设来说,选择它有一个无法拒绝的理由:不需要跑医院伦理审批,不需要自己采集病例,数据是公开的,字段文档齐全。项目里所有病人都是ICU患者,这意味着病情本身就偏向危重,模型预测的难度和临床场景也比较明确。

MIMIC数据库有几个版本,这个项目对应的是MIMIC-III的物理表结构,这一点从项目里的GET_AMI.sql和GET_WBC.sql两个文件名能看出来。MIMIC-III的核心表包括admissions(入院记录)、patients(患者信息)、diagnoses_icd(ICD诊断编码)、labevents(检验指标)等。急性心肌梗死对应ICD-9编码以410开头的诊断,比如410.71是非透壁性心肌梗死。项目的目标标签是患者住院期间是否死亡,这个字段在admissions表里叫hospital_expire_flag,0表示存活出院,1表示院内死亡。

2.2 GET_AMI.sql:拉取心肌梗死患者主表

数据获取的第一步是从MIMIC数据库里筛出急性心肌梗死患者。项目里的GET_AMI.sql干的就是这件事,核心逻辑是关联诊断表和入院表,按ICD-9编码过滤。常见的写法是这样的:

SELECT a.subject_id, a.hadm_id, a.admittime, a.dischtime, a.hospital_expire_flag, p.gender, p.dob FROM admissions a 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%' ) ORDER BY a.subject_id;

这里有两个容易忽略的点。第一,subject_id是患者ID,hadm_id是单次住院ID,同一个患者可能多次住院,所以模型的特征和标签都必须以hadm_id为粒度,否则会出现同一个人既在训练集又在测试集的数据泄漏。第二,hospital_expire_flag直接作为二分类标签,但这个字段只表示院内死亡,不包含出院后30天或90天内的死亡情况,后者的预测难度更大。这个SQL跑完,你会得到一个人数在几千到上万级别的AMI患者队列,具体数量取决于MIMIC版本和ICD编码的过滤范围。

2.3 GET_WBC.sql:拉取血常规检验指标

光有患者主表还不够,模型特征来自检验指标。项目里的GET_WBC.sql专门拉血常规相关数据,这是因为白细胞计数和血小板计数在心肌梗死急性期有明确的临床意义:白细胞升高往往提示心肌坏死面积大或炎症反应重,血小板异常则与血栓风险相关。SQL的典型逻辑是这样:

SELECT l.hadm_id, l.charttime, d.label, l.valuenum FROM labevents l JOIN d_labitems d ON l.itemid = d.itemid WHERE l.hadm_id IN ( SELECT hadm_id FROM amis ) AND d.label IN ('WBC', 'PLATELET', 'HEMATOCRIT', 'HEMOGLOBIN') AND l.valuenum IS NOT NULL ORDER BY l.hadm_id, l.charttime;

跑完这个查询,你拿到的是长表结构:每个病人、每个时间点、每个检验项一行。这个结构后面必须做宽表转换,不然没法直接喂给机器学习模型。还有一个细节:MIMIC里的labevents以itemid关联d_labitems才得到指标名称,不同MIMIC版本的itemid可能不同,所以代码里一般不建议直接硬编码itemid,而是通过label名称关联,兼容性更好。项目文档里建议先用GET_AMI.sql生成患者列表,再用GET_WBC.sql分批拉指标,因为MIMIC原始数据量很大,一次性跨多张表join容易把数据库连接跑挂。

3. 数据预处理:从长表到训练集的完整链路

3.1 PreProcessInsert.py:把SQL查询结果清洗合并

从SQL拉出来的数据不能直接用,原因有三个:长表结构需要透视成宽表、多个检验指标的采样时间点不一致、存在大量缺失值。项目里的PreProcessInsert.py负责把SQL查询结果读入Python,清洗合并后插入中间表或直接生成DataFrame。这个脚本的逻辑我拆过,核心流程是从MySQL或PostgreSQL读取查询结果,按hadm_id聚合,把长表透视成宽表,同时过滤掉明显异常值。

以连接MySQL为例,常见做法是:

import pandas as pd import pymysql DB_CONFIG = { 'host': 'localhost', 'user': 'mimic', 'password': 'mimic', 'database': 'mimic', 'charset': 'utf8mb4' } def load_ami_cohort(): conn = pymysql.connect(**DB_CONFIG) query = """ SELECT subject_id, hadm_id, hospital_expire_flag, admittime, gender FROM ami_cohort """ df = pd.read_sql(query, conn) conn.close() return df def load_labs(): conn = pymysql.connect(**DB_CONFIG) query = """ SELECT hadm_id, charttime, label, valuenum FROM ami_labs ORDER BY hadm_id, charttime """ df = pd.read_sql(query, conn) conn.close() # 过滤明显异常值,例如白细胞不可能为负数 df = df[df['valuenum'] > 0] return df

这一段脚本有两个关键设计。第一,连接参数单独放在字典里,换成PostgreSQL连MIMIC官方版时只需要改驱动和端口,不用动业务代码。第二,读取时用pd.read_sql而不是逐行fetch,因为医疗数据量大,逐行处理会慢到怀疑人生。我在实际复现时喜欢在SQL里先做一次WHERE valuenum IS NOT NULL过滤,再到Python里做二次清洗,这样能有效缩短SQL查询时间。

3.2 透视宽表与单日特征聚合

长表透视成宽表是时间序列类医疗数据最常见的预处理步骤。因为MIMIC里的检验指标是每天的离散采样,不是连续的实时流,所以一个自然的做法是:对每个病人取入院后某个时间窗内的特征,计算窗口内的均值、最大值、最小值作为单日特征。项目里正是这样处理的,它没有用到复杂的时间序列模型,而是把每个指标转成多个统计量,将问题简化为标准的表格型二分类问题。

def pivot_labs_by_hadm(labs_df): # 使用入院后24小时内的数据作为特征窗口 labs_df = labs_df[labs_df['charttime'] <= labs_df['admittime'] + pd.Timedelta(hours=24)] stats = labs_df.groupby(['hadm_id', 'label'])['valuenum'].agg( ['mean', 'max', 'min', 'last'] ).reset_index() # 透视成宽表,每个指标对应多个统计列 wide = stats.pivot_table( index='hadm_id', columns='label', values=['mean', 'max', 'min', 'last'] ) wide.columns = ['_'.join(col).upper() for col in wide.columns] wide = wide.reset_index() return wide

这一步看起来简单,但里面的时间窗选择直接影响模型是否泄漏。如果用整个住院期间的数据,那么病人快死亡之前的血压和指标自然是极端值,模型当然能学到“指标极端→死亡”这个规律,但这在临床上毫无意义,因为医生在入院第一天无法知道病人三天后血压会掉到多少。所以正确的做法是固定时间窗,项目里这个24小时窗口的选择是合理的,后面避坑章我会再展开。

3.3 OneHot编码与类别特征处理

PreProecssOneHot.py负责把类别特征转成OneHot向量。这里涉及一个常见的认知误区:很多初学者拿到医疗数据,不管三七二十一全部做标准化或归一化,但像心律类别、梗死部位、Killip分级这种类别特征,标准化毫无意义,OneHot才是正确姿势。

from sklearn.preprocessing import OneHotEncoder categorical_cols = ['gender', 'infarct_location', 'killip_class'] encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False) oh_encoded = encoder.fit_transform(df[categorical_cols]) encoder_df = pd.DataFrame( oh_encoded, columns=encoder.get_feature_names_out(categorical_cols) ) df_final = pd.concat([df.drop(columns=categorical_cols), encoder_df], axis=1)

这里必须说两个参数。第一,handle_unknown='ignore'很重要:训练集若只有男女性别,测试集出现缺失或新类别时,ignore能保证不会直接报错,而是全零向量。第二,sparse_output=False是把稀疏矩阵转成稠密数组,否则后面合并DataFrame时会遇到维度对不上的问题。OneHot之后特征维度会膨胀,如果类别特征过多,可以考虑用特征哈希或嵌入层做降维,但这个项目里类别特征就那么几个,OneHot完全够用。数据处理完,特征矩阵的形态大致是:几百个病人,几十个特征列,标签一列,字段名类似WBC_MEAN、PLATELET_LAST、KILLIP_CLASS_2这种。

4. 模型训练:XGBoost与LightGBM双实现

4.1 xgb.py:XGBoost参数与训练流程

项目提供了两套模型代码,xgb.py对应XGBoost,TrainLightGBM.py对应LightGBM。之所以做两个,不是因为模型越多越好,而是XGBoost和LightGBM在表格型数据上的表现都稳,对比学习是毕设答辩里常见的内容。XGBoost的核心是梯度提升树,用加法模型不断拟合残差。对医疗数据这种带缺失值、有噪声、特征之间关系复杂的场景,树模型的鲁棒性比线性模型好很多。

import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, roc_auc_score X = df_final.drop(columns=['hospital_expire_flag']) y = df_final['hospital_expire_flag'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) model = xgb.XGBClassifier( n_estimators=500, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, scale_pos_weight=(len(y_train) - y_train.sum()) / y_train.sum(), eval_metric='auc' ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False ) y_pred = model.predict(X_test) y_prob = model.predict_proba(X_test)[:, 1] print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(f"AUC: {roc_auc_score(y_test, y_prob):.4f}")

几个参数值得单独说。第一,max_depth=4而不是默认的6,因为医疗特征维度不大,树太深容易过拟合到这个具体队列上。第二,scale_pos_weight是处理类别不平衡的关键参数,它的值是负类数量除以正类数量,在只有一成左右死亡率的ICU数据里这个值通常超过5。第三,eval_metric='auc'让训练过程中直接观察AUC变化,比只看准确率更能反映模型质量。

4.2 TrainLightGBM.py:LightGBM的差异与参数

LightGBM和XGBoost的核心区别在生长策略。XGBoost是level-wise按层生长,LightGBM是leaf-wise按叶子生长,后者在相同参数下更容易拟合复杂模式,但也更容易过拟合,所以需要更强的正则化。TrainLightGBM.py里的参数设置能看出这个思路:

import lightgbm as lgb lgb_model = lgb.LGBMClassifier( n_estimators=1000, num_leaves=31, learning_rate=0.03, feature_fraction=0.7, bagging_fraction=0.8, bagging_freq=1, lambda_l1=0.1, lambda_l2=0.5, min_data_in_leaf=20, class_weight='balanced', verbose=-1 ) lgb_model.fit( X_train, y_train, eval_set=[(X_test, y_test)], eval_metric='auc' ) y_pred_lgb = lgb_model.predict(X_test) y_prob_lgb = lgb_model.predict_proba(X_test)[:, 1]

这里面num_leaves=31是LightGBM最敏感的参数,它直接控制树的复杂度,值过大会导致过拟合,在小数据集上我一般从16开始试,逐步加大的同时观察验证集AUC是否还在涨。min_data_in_leaf=20保证每个叶子至少20个样本,进一步抑制过拟合。class_weight='balanced'是另一种处理不平衡的方式,它自动调整权重,和XGBoost里的scale_pos_weight作用类似,但设置方式更省事。

4.3 评估指标:准确率95%背后的真实含义

项目标题写着准确率达95%以上,这个数字需要拆开看。在ICU死亡风险预测场景里,死亡率通常只有8%到15%,如果模型无脑预测“存活”,准确率也有85%以上。所以单纯看准确率没有意义,必须看AUC、精确率、召回率。AUC达到0.9以上在这个场景里是可能的,因为ICU病人的血压、乳酸、GCS评分等指标在死亡前确实会显著恶化,模型能学到这些信号。

但要注意,这里的95%准确率大概率是在固定时间窗特征下获得的。拿入院24小时内的特征去预测院内死亡,AUC能到0.9左右,准确率如果设在0.5阈值上,确实有可能超过90%。比如特征中纳入了肌钙蛋白I、收缩压均值、Killip分级,这些指标在重症心梗患者里鉴别力很强。所以准确率95%不是虚假宣传,而是“在这个队列、这个特征集、这个阈值下的分类表现”,换个场景就不一定成立。项目文档里作者自己也说了,临床上直接看心电图和造影更直观,模型在真实决策链条里的位置很尴尬,这一点要对读者诚实。

5. 避坑指南:复现这个项目的五个常见翻车点

5.1 现象:测试集准确率高达95%,但AUC反而不到0.8

这个现象我在复现类似项目时遇到过两次。准确率高、AUC低,说明模型在多数类上表现好,少数类基本没学到。ICU数据的死亡率低,如果不用scale_pos_weight或class_weight,模型会把几乎所有样本都预测为存活,准确率自然高,但对真正需要关注的死亡病人完全没有识别能力。解决方法是训练时强制加类别权重,评估时以AUC和召回率为主,不要被准确率迷惑。

5.2 现象:训练时AUC接近0.99,测试集却明显下降

原因基本是时间泄漏。如果特征窗口没有限制在入院早期,而是用了整个住院期间的数据,那么死亡病人的临终指标天然极端,模型等于在“偷看”结局。解决方法是严格用入院后固定时间窗的特征,项目里的24小时窗口是个合理默认值,也可以在论文里对比12小时、24小时、48小时三个窗口的差异,这样写毕业设计时反而多一个分析维度。

5.3 现象:MIMIC数据导入阶段就卡住,代码一行没跑

MIMIC数据库不是一个能直接下载的CSV文件,它需要从官网申请访问权限,完成培训后下载原始文件,再导入本地数据库。这一关劝退了大量学生,尤其是PostgreSQL版本对权限要求严格,普通账号无法创建扩展或者导入大文件。常见做法是先用官方提供的postgres_create_tables.sql建表,再逐个用COPY命令导入,导入顺序不能乱,比如admissions必须在diagnoses_icd之前,因为后者有外键依赖。

5.4 现象:OneHot编码后训练集和测试集维度不一致

这个问题几乎每个用pandas.get_dummies的人都踩过。如果在全量数据上做编码再切分,测试集里某个类别恰好只在训练集出现,两个集合的特征列数就对不上。解决方法是先切分训练集和测试集,再在训练集上fit编码器,最后用同一个编码器transform测试集。项目提供的文件结构里虽然没直接体现这个顺序,但PreProecssOneHot.py的命名和拆分逻辑是往这个方向设计的。

5.5 现象:train_test_split随机切分导致同一患者跨集合

由于MIMIC里同一患者可能多次入院,如果按hadm_id随机切分,同一个人的两次住院可能分别落在训练集和测试集里,造成数据泄漏。正确做法是先按subject_id分组,确保同一患者的全部记录只在同一个集合中。

# 按患者ID分组切分,而不是按样本行随机切分 subjects = df['subject_id'].unique() train_subjects, test_subjects = train_test_split( subjects, test_size=0.2, random_state=42 ) train_df = df[df['subject_id'].isin(train_subjects)] test_df = df[df['subject_id'].isin(test_subjects)]

这个坑尤其在毕设答辩里容易被老师命中——只要问一句“同一个患者有没有可能同时出现在训练和测试集”,答案不对就暴露了。从那以后,我每次处理医疗数据都强制按患者ID分组做切分,哪怕数据量因此少掉几条也在所不惜。

6. 进阶与验证:把这套流程改造成更可信的简历项目

6.1 用SHAP验证模型没有在随机噪声上拟合

如果想让这个项目的说服力再上一个台阶,建议加SHAP特征重要性分析。SHAP能解释每个特征对预测结果的贡献方向,比如验证“WBC最大值越高,死亡风险越大”是否符合临床直觉。代码很短,但效果很好:

import shap explainer = shap.TreeExplainer(lgb_model) shap_values = explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, plot_type='bar')

如果你的模型特征重要性排名里,“性别”或者某个无关紧要的编码列排第一,那说明模型学到的不是临床规律而是噪音;如果排前面的是收缩压均值、WBC最大值、肌钙蛋白,那这套结果在逻辑上是能站住脚的。

6.2 用五折交叉验证替代一次切分

项目代码里直接用了train_test_split,这在毕设里可以被优化为五折交叉验证。交叉验证能更稳定地估计模型性能,也能用同一套代码画出ROC曲线和PR曲线。一个实际的做法是把StratifiedKFold配合sklearn.metrics画ROC汇总曲线,让测试集上AUC的置信区间也显示出来,这样写进论文比单次准确率有说服力得多。同时还能加上校准曲线,查看模型输出的概率是否反映真实死亡率,对高风险病人来说,概率校准比分类准确度更重要。

6.3 迁移到其他病种:这个框架的真正价值

项目摘要里提了一句“肾衰竭等疾病的预测还有一点意义”,这句话其实点出了这套源码最值得迁移的地方。把GET_AMI.sql里的ICD-9诊断编码换成肾脏疾病的编码,把标签从院内死亡换成肾脏替代治疗的使用,整个流程就能迁移成急性肾损伤死亡风险预测。模型代码和预处理脚本基本不用改,只需要换数据源和特征工程。如果你想在毕设里做出“略有新意”的工作,换病种不换框架是性价比非常高的路径。项目源码包里包含预处理、模型训练、SQL取数全套脚本,有明确的文档说明,格式干净,适合在这个基础上扩展成自己的项目。希望这套拆解能帮你把这个资源转化成能在答辩现场讲清楚的作品。

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