
简介面向医疗数据分析与机器学习实践者这是一份完整的急性心肌梗死AMI死亡风险预测项目。资源聚焦利用逻辑回归、随机森林、XGBoost等模型对高危患者进行风险分层覆盖数据清洗、特征工程、交叉验证、参数调优及模型解释等核心环节适合希望掌握医疗AI建模全流程的学习者。包内共10个文件包括4个Python预处理与训练脚本、3个CSV实验数据、2个SQL查询脚本及1个Excel信息表压缩包约5.56MB结构紧凑便于对照运行。已有169人学习下载。通过该项目可获取从医疗数据整理到模型部署的完整代码与数据集理解缺失值插值、独热编码、LightGBM调参等实操细节并借助AUC-ROC等评估指标建立科学的模型验证思路快速迁移到其他临床预测场景。1. 为什么用机器学习做 AMI 死亡风险预测急性心肌梗死AMI的院内死亡风险预测是个典型的二分类监督学习任务输入患者入院后数小时内的生命体征、实验室检验和病史输出“是否发生死亡”。临床上常用的 TIMI、GRACE 评分本质是固定权重评分卡特征交互和非线性关系基本没被利用机器学习恰好擅长在结构化数据里找出这类模式这是标题里“机器学习”四个字的核心价值。另一个反直觉的结论是死亡预测项目里最容易出问题的往往不是模型选得不够新而是预测窗口怎么定、缺失值怎么处理、验证集怎么切。见过不少团队在参数上调了三周效果反而不如把时间窗统一成 24 小时、把缺失标志加进特征来得明显。这篇文章给一条能复现的路从数据清洗、特征定义、模型基线到评估和部署前验证全程用结构化临床数据不依赖影像和文本。2. 数据与特征处理把 AMI 记录变成机器学习输入从原始病案到机器学习输入最关键的一步不是选算法而是定义“死亡”和“预测窗口”。同一份数据把终点从“院内死亡”改成“30 天死亡”AUC 可能差出 0.05 以上因为随访口径、出院后失访率完全不一样。项目里如果不写明白后面所有复现都会失真。2.1 预测窗口与标签先定义“死亡”再谈模型我一般会把预测窗口固定在入院后 7 天内全因死亡理由有两个一是这个窗口和临床早期干预与重症监测的重合度高二是公开重症数据集里随访记录最稳定的往往是短窗口长窗口的失访偏倚会直接污染标签。标签构造在代码里就两行但必须同时处理“未死亡”和“随访不足 7 天”两类情况import pandas as pd import numpy as np # df 每行对应一位 AMI 患者包含入院时间和死亡时间 df[label] ( df[death_date].notna() (df[death_date] - df[admit_time] pd.Timedelta(days7)) ).astype(int) # 检查事件率方便后面做类别不平衡处理 print(f样本量: {len(df)}, 阳性事件: {df[label].sum()}, 阳性率: {df[label].mean():.2%})这段代码把“死亡日期存在”和“死亡发生在入院后 7 天内”两个条件做了与运算避免把出院后死亡误标成事件。随后打印的阳性率是整条建模流水线的关键指标——如果低于 10%后面就必须用类别不平衡策略不能直接拿准确率当目标。2.2 特征体系哪些变量是 AMI 风险预测的稳定输入特征按来源分成四组每组对应不同的缺失处理逻辑不要把数值型和 0/1 型混在一起填充。下面这个表是 AMI 死亡预测项目里最高频的特征集合也是后续代码的基础特征分组变量示例编码方式取数窗口人口学年龄、性别数值 / 0-1入院时生命体征心率、收缩压、呼吸频率、SpO2数值取最差值入院后 24 小时实验室指标肌钙蛋白、NT-proBNP、肌酐、乳酸数值取极值入院后 24 小时合并症与心电图高血压、糖尿病、心衰史、ST 段抬高0-1既往史与首份心电图实际建特征时要注意“取最大值还是最小值”不是统计问题是临床问题收缩压取最低值才代表循环崩溃的风险肌钙蛋白取最高值才代表心肌损伤程度。聚合窗口统一用入院后 24 小时太短丢信息太长把后续治疗的影响也掺进了基线特征。features [ age, gender, hr_min, sbp_min, rr_max, spo2_min, troponin_max, ntprobnp_max, creatinine_max, lactate_max, htn, dm, chf, prior_mi, st_elevation, lbbb, ] X df[features].copy() y df[label].copy()这里hr_min代表 24 小时内最低心率sbp_min同理troponin_max取检测峰值的逻辑则是判断心肌坏死范围。把这些聚合函数写清楚比直接把所有原始测量值灌进模型更稳定也更好解释。2.3 缺失值和约束用临床逻辑处理 NaN 而不是删行临床数据里行级删减代价很高因为“缺失”本身往往和病情有关重症患者更容易出现没来得及抽血、检验项目不全的情况。直接把缺失行丢掉相当于剔除了最重的一批病人训练集分布会被明显带偏。我常用的处理方式来分三类# 1) 合并症与心电图缺失按“未见/未记录”处理置0 for c in [htn, dm, chf, prior_mi, st_elevation]: X[c] X[c].fillna(0) # 2) 实验室指标用中位数填充同时生成缺失标志列 for c in [troponin_max, ntprobnp_max, creatinine_max, lactate_max]: X[c _missing] X[c].isna().astype(int) X[c] X[c].fillna(X[c].median()) # 3) 生命体征同样保留缺失标志 for c in [hr_min, sbp_min, rr_max, spo2_min]: X[c _missing] X[c].isna().astype(int) X[c] X[c].fillna(X[c].median())这段代码的核心不是fillna而是新增了_missing列。这样模型能学习到“这项检验没做”这个状态本身携带的风险信息。中位数填充比均值更抗异常值在肌钙蛋白这类呈长尾分布的指标上尤其明显。提示缺失标志列不需要太多。当某个特征缺失率超过 80% 时标志列的意义大于填充列本身可以直接只保留标志列。3. 模型选择与训练从逻辑回归到梯度提升的机器学习基线拿到清洗后的特征先不要急着上复杂模型。一个严谨的机器学习项目应该从可解释的基线开始再逐步引入非线性模型。对 AMI 死亡预测来说逻辑回归和 XGBoost 是最常用也最有效的一对组合。3.1 逻辑回归医学场景里最稳的起点逻辑回归是二分类问题默认的起点。它输出的概率天然在 0-1 之间而且系数方向能直接说明特征风险方向收缩压最低值系数为负、年龄系数为正医生可以一眼理解。先跑一遍逻辑回归还有个作用是给后面复杂模型定一个“及格线”。from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) lr_pipe make_pipeline( StandardScaler(), LogisticRegression(max_iter1000, C1.0, class_weightbalanced) ) lr_pipe.fit(X_train, y_train)这里StandardScaler是必要的逻辑回归对特征尺度敏感肌钙蛋白上千、年龄只有几十不标准化会让惩罚项失去公平性。class_weightbalanced根据样本比例自动加权是处理稀有事件的第一道防线。3.2 梯度提升树XGBoost 的核心参数怎么定梯度提升树在表格数据上的表现通常优于线性模型因为它不需要预设特征关系能自动建模年龄与肌钙蛋白之间的交互项。XGBoost 的每一轮迭代会拟合损失函数的负梯度方向这也就是热词里常说的“机器学习中的梯度”在实际工程里的落点。下面是两个必须调的核心参数表参数作用常见范围说明max_depth单棵树深度3-6深度越大越容易过拟合医疗数据样本量不大建议从 3 起步learning_rate步长0.01-0.1调小后需要更多棵树精确但慢subsample行采样比例0.7-0.9降低方差colsample_bytree列采样比例0.5-0.8提高树之间多样性scale_pos_weight正负样本权重比负样本数/正样本数类别不平衡时的核心参数eval_metric验证指标aucpr/auc稀有事件建议用aucpr训练时使用早停避免过拟合验证集单独保留不参与交叉验证import xgboost as xgb pos_weight (y_train.shape[0] - y_train.sum()) / y_train.sum() xgb_model xgb.XGBClassifier( n_estimators1000, learning_rate0.02, max_depth3, subsample0.8, colsample_bytree0.6, scale_pos_weightpos_weight, eval_metricaucpr, use_label_encoderFalse, verbosity0, ) xgb_model.fit( X_train, y_train, eval_set[(X_val, y_val)], early_stopping_rounds50, verboseFalse, )scale_pos_weight直接取负样本数与正样本数的比值是 XGBoost 处理不平衡数据最有效的手段比class_weight更直接。early_stopping_rounds50表示验证集指标连续 50 轮没有改善就停能省下大量训练时间。3.3 验证策略临床数据不满足独立同分布假设这是整个项目里最容易被忽略、又最影响结论可信度的一环。随机划分训练集和验证集时默认假设所有样本独立同分布但回顾性队列的真实情况是不同年份的诊疗方案、药物使用和监护水平都在变化2018 年的病人和 2024 年的病人在风险分布上完全不一样。用随机划分得到的 AUC 往往会偏高因为模型提前“见过”了未来样本。我一般改用时间外验证用早期数据训练、后期数据验证模拟模型真正上线后的场景from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train_t, X_val_t X.iloc[train_idx], X.iloc[val_idx] y_train_t, y_val_t y.iloc[train_idx], y.iloc[val_idx] # 在每个fold上重新训练并记录验证集AUCTimeSeriesSplit和train_test_split的区别在于它严格按时间顺序切分前一个 fold 的训练集永远比验证集早。临床数据一旦出现时间漂移这种做法给出的性能才接近真实部署水平。如果项目数据量很小至少也要按年份分为两段做样本外验证。4. 类别不平衡与评估指标不要只看 Accuracy 和 AUCAMI 死亡预测里阳性样本通常只有 5%-15%。在这种数据上一个“全部预测为存活”的模型也能拿到 90% 以上准确率却没有任何临床价值。评估指标的选择必须围绕稀有事件展开。4.1 为什么 AUC 不够用ROC-AUC 对类别不平衡相对不敏感。因为 ROC 曲线的横纵轴分别是假正例率和真正例率分母涵盖大量负样本正样本很少时曲线仍能拉得很平滑给人的错觉是模型不错。但临床场景里真正关心的是如果模型预警了有多大比例是真正会死亡的患者。这个问题 AUC 答不了需要用 PR 曲线和校准曲线。4.2 用 AUPRC 评估稀有事件PR 曲线的横轴是召回率纵轴是精确率它的面积AUPRC直接反映模型在少数类上的识别能力。随机模型的 AUPRC 等于阳性率所以一个 10% 阳性率的数据集里AUPRC 跑到 0.3 以上才算有真实区分度。from sklearn.metrics import precision_recall_curve, auc, roc_auc_score prob_lr lr_pipe.predict_proba(X_val)[:, 1] prob_xgb xgb_model.predict_proba(X_val)[:, 1] auc_lr roc_auc_score(y_val, prob_lr) auc_xgb roc_auc_score(y_val, prob_xgb) prec_lr, rec_lr, _ precision_recall_curve(y_val, prob_lr) prec_xgb, rec_xgb, _ precision_recall_curve(y_val, prob_xgb) print(fLR ROC-AUC{auc_lr:.3f} AUPRC{auc(rec_lr, prec_lr):.3f}) print(fXGB ROC-AUC{auc_xgb:.3f} AUPRC{auc(rec_xgb, prec_xgb):.3f})这段代码同时打印两类指标。实际项目里经常出现 ROC-AUC 差不多、AUPRC 差了 0.1 的情况这时应优先相信 AUPRC。正负样本比例变化时AUPRC 也会跟着变因此报告指标时要同时注明阳性率。4.3 阈值选择从概率到是否预警模型输出的是连续概率而临床上最终需要的是“预警/不预警”决策。默认阈值 0.5 在稀有事件里几乎永远不是最优选择因为模型输出的概率整体偏低0.5 会把所有样本都归入负类。prec, rec, thresholds precision_recall_curve(y_val, prob_xgb) # 以 F2 为目标漏掉死亡病例的代价高于误报 beta 2 f2_scores (1 beta**2) * prec * rec / ((beta**2 * prec) rec 1e-9) best_idx np.argmax(f2_scores) best_threshold thresholds[best_idx] print(f最佳阈值: {best_threshold:.3f})为什么用 F2 而不是 F1临床预警场景里漏掉一个高危患者的代价远高于多报一个低危患者F2 给召回率更高权重阈值会自动偏向严查。选择阈值后还要结合科室的实际容量评估每天会多出多少预警、护士站处理得过来吗。4.4 概率校准让 0.8 真的是 0.8如果模型的输出要展示给临床医生概率值本身必须可靠。XGBoost 输出的概率往往偏极端需要做校准。CalibratedClassifierCV用交叉验证拟合一个校正函数常用等渗回归或 Platt scalingfrom sklearn.calibration import CalibratedClassifierCV calibrated_model CalibratedClassifierCV( estimatorxgb_model, methodisotonic, cv3, ) calibrated_model.fit(X_train, y_train) prob_cal calibrated_model.predict_proba(X_val)[:, 1]等渗回归isotonic在校准曲线形状较复杂时表现更好但需要较多数据支撑数据量小时用sigmoid更稳。校准后别忘了再算一次 AUPRC校准过程偶尔会轻微降低区分度如果降幅超过 0.02就需要重新考虑模型结构。5. 用 SHAP 值验证模型行为并完成部署前检查最后一层工作不是“上线”而是“确认模型真的在学习临床规律”。SHAP 是机器学习模型可解释性的事实标准能同时回答“哪个特征最重要”和“单个病患为什么被判为高危”两个问题。import shap explainer shap.TreeExplainer(xgb_model) shap_values explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val)看 SHAP summary plot 时重点检查三点一是年龄、收缩压、肌钙蛋白这些临床公认的强危险因素是否排在特征重要性前列二是特征方向是否符合医学直觉比如sbp_min的 SHAP 值是否越低越推高死亡风险三是是否存在某个特征方向完全不符合常识那通常指向数据口径出错而不是模型超预期。提示校准后的模型不能直接再做 SHAP 解释CalibratedClassifierCV内部包装了多个底模型。要对 SHAP 做解释用原始xgb_model校准前后的特征方向通常不会变化但 SHAP 数值只代表原始模型。部署前还需要检查一件事训练集和上线时特征口径是否完全一致。常见坑包括新数据里出现训练时没见过的缺失模式、历史数据里某检验单位从 ng/mL 换成了 pg/mL、时间窗口从 24 小时变成了入院到出院全周期。任何一项变化都会让 AUPRC 明显掉点。稳妥做法是把特征构造函数单独封装并通过单元测试固定下来上线时直接复用同一套代码不要手工复制粘贴。对高风险模型的机器学习项目最后跑一遍校准曲线和分时间段 AUC 趋势图确认没有随时间衰减再交给临床决策流程。本文还有配套的精品资源点击获取