简介:一份心血管疾病预测模型的Python建模项目资源,面向希望运用机器学习解决医疗健康问题的学习者。案例围绕真实临床数据,完整呈现数据清洗、异常值处理、特征构造(如BMI指数、吸烟指数)以及标准化等预处理流程,并演示利用逻辑回归、随机森林、支持向量机等算法完成模型训练、交叉验证、ROC曲线与准确率召回率评估,以及通过网格搜索调整超参数。资源内共含3个文件:一个Jupyter Notebook演练文档、一个可独立运行的Python脚本和一份csv格式的心衰数据集,整体压缩包仅83KB。当前已有1129人学习,适合具备Python基础并希望从事医疗数据分析的读者深入研读。得益于完整代码与数据,使用者可快速掌握特征选择、模型持久化与可视化等关键技能,并可将同一套方法论迁移至其他疾病风险预测场景。
1. 这个心血管预测模型源码包,解决的不只是“跑通代码”
拿一份体检数据想预测心血管风险,最尴尬的不是不会装 Python,而是装好了却不知道先处理哪一列。这个源码包正好给你一个完整闭环:heart_failure.csv 是真实的心衰患者临床记录,feature_selection.py 解决“13 个特征哪些能用”的问题,附带的 Jupyter Notebook 把数据清洗、模型训练、评估串成一条线。它的定位不是科普翻书,而是可以直接照着跑的数学建模实战项目。适合三种人:参加数学建模竞赛要选医疗题的学生、刚入门机器学习想练完整流程的开发者、以及需要做风险预测演示的医疗数据从业者。我自己拆完第一遍的感受是:代码不算难,但每个环节都值得停下来想想“为什么这么写”,这比跑通本身更有价值。
2. 先摸透 heart_failure.csv:字段含义与预处理的关键点
2.1 字段清单:哪些是风险因素,哪些是时间窗口
heart_failure.csv 记录的是心衰患者的临床随访数据,目标字段是 DEATH_EVENT,表示患者在随访期内是否死亡。模型要学的是“哪些因素和死亡风险相关”,所以这是一个典型的二分类监督问题。
| 字段名 | 类型 | 含义 |
|---|---|---|
| age | 数值 | 年龄 |
| anaemia | 0/1 | 是否贫血 |
| creatinine_phosphokinase | 数值 | 肌酸激酶水平 |
| diabetes | 0/1 | 是否糖尿病 |
| ejection_fraction | 数值 | 射血分数,衡量心脏泵血能力 |
| high_blood_pressure | 0/1 | 是否高血压 |
| platelets | 数值 | 血小板计数 |
| serum_creatinine | 数值 | 血清肌酐 |
| serum_sodium | 数值 | 血清钠 |
| sex | 0/1 | 性别 |
| smoking | 0/1 | 是否吸烟 |
| time | 数值 | 随访天数 |
| DEATH_EVENT | 0/1 | 随访期内是否死亡(目标列) |
这里最容易踩坑的是 time。它表示每个病人的随访时间长度,不是临床检验指标。如果把它当普通特征放进模型,模型会学到“随访时间短的人死亡风险高”这种时间性泄露,AUC 会虚高到没法用。这份源码包里没有把它当特征,但你自己扩展数据时很容易忽略。
2.2 缺失值和异常值:先用 pandas 摸一遍底
拿到 CSV 第一步不是建模,而是用 pandas 快速查看数据形态。常见做法是先看 info 确认有没有缺失,再看 describe 观察数值分布的极端值。心衰数据里肌酸激酶、血小板这类指标通常会有个别极端值,直接用均值填充会把特征分布拉歪。
import pandas as pd df = pd.read_csv('heart_failure.csv') print(df.info()) print(df.describe(percentiles=[.25, .5, .75, .95])) # 数值列统一用中位数填充缺失值 num_cols = df.select_dtypes(include=['float64', 'int64']).columns for col in num_cols: if df[col].isnull().any(): df[col].fillna(df[col].median(), inplace=True) print(f'{col} 缺失填充完成')这段代码的重点在 percentiles 参数。describe 默认只给 25%、50%、75% 分位,加上 .95 之后能更早发现右侧长尾。心衰场景里肌酸激酶正常范围是 10 到 120 U/L,但重症患者可能到几千,这种极端值对逻辑回归的梯度影响很大。处理边界时我一般会用 IQR 法标记,而不是直接删行,因为医学数据里异常值本身可能就代表病情严重。
2.3 标准化与归一化:逻辑回归必须做,树模型无所谓
很多资料把标准化说成“必需步骤”,但准确说法是:对距离类模型和线性模型必需,对树模型影响不大。逻辑回归用梯度下降优化,特征量纲差异过大会导致收敛慢、系数解读困难。随机森林是基于分裂点选择的,特征缩放不改变排序结果。
from sklearn.preprocessing import StandardScaler X = df.drop(columns=['DEATH_EVENT']) y = df['DEATH_EVENT'] num_features = ['age', 'creatinine_phosphokinase', 'ejection_fraction', 'platelets', 'serum_creatinine', 'serum_sodium'] scaler = StandardScaler() X_scaled = X.copy() X_scaled[num_features] = scaler.fit_transform(X[num_features])注意这里只对连续数值特征做标准化,0/1 型二值特征不用动。fit_transform 和 transform 的区别是新手最容易出错的点:fit_transform 用于训练集,transform 用于测试集和未来的新数据。如果你把全量数据一起 fit,验证时的数据分布已经包含测试集信息,这叫数据泄露,后面避坑章会专门讲。
3. 特征选择实战:feature_selection.py 里的筛选思路能直接用
3.1 13 个特征不能全塞进去,先看相关性和方差
14 个变量里去掉目标和 time,还剩 11 个可用特征。不算多,但直接全塞进逻辑回归会遇到两个问题:一是特征之间可能高度相关,比如肌酐和年龄往往有正相关,导致回归系数不稳定;二是部分特征的预测能力极弱,比如性别在这个数据集里可能贡献很低,留着只会增加噪声。
feature_selection.py 提供的核心思路是先做“粗筛”再做“精筛”。粗筛用相关性和方差,目标是快速排除明显没用的列;精筛用递归特征消除或随机森林重要性,目标是确认哪些组合效果最好。
3.2 相关性热图与方差过滤的代码实现
相关性分析在 pandas 里一行就能算出来。常见做法是设定阈值 0.7,如果两个特征相关系数超过这个值,就保留其中一个。
import pandas as pd df = pd.read_csv('heart_failure.csv') corr = df.corr() # 找出与目标列 DEATH_EVENT 相关性绝对值大于 0.1 的特征 target_corr = corr['DEATH_EVENT'].abs().sort_values(ascending=False) print(target_corr[target_corr > 0.1]) # 找出两两相关性大于 0.7 的特征对 cols = corr.columns.tolist() high_pairs = [] for i, col1 in enumerate(cols): for col2 in cols[i+1:]: if abs(corr.loc[col1, col2]) > 0.7: high_pairs.append((col1, col2, corr.loc[col1, col2])) print(high_pairs)这段代码有两个作用。一是快速定位哪些特征与死亡事件关系密切,通常 ejection_fraction、serum_creatinine、age 会排在前列;二是发现多重共线性组合。如果 age 和 serum_creatinine 相关性大于阈值,可以保留与目标相关性更高的那个。方差过滤更简单,直接计算数值列的方差,方差接近 0 说明所有样本取值几乎一致,对区分死亡与否没有帮助。
3.3 递归特征消除与随机森林重要性的取舍
递归特征消除(RFE)是 sklearn 里很成熟的算法。它反复训练模型、淘汰权重最小的特征,直到达到指定数量。随机森林的重要性则是基于节点分裂对纯度的贡献,两者得出的特征排名经常不一样,我会同时跑,然后取交集。
from sklearn.ensemble import RandomForestClassifier from sklearn.feature_selection import RFE X = df.drop(columns=['DEATH_EVENT', 'time']) y = df['DEATH_EVENT'] rf = RandomForestClassifier(n_estimators=200, random_state=42) rfe = RFE(estimator=rf, n_features_to_select=6) rfe.fit(X, y) selected = X.columns[rfe.support_].tolist() ranking = sorted(zip(X.columns, rfe.ranking_), key=lambda x: x[1]) print('RFE 选择的特征:', selected) print('特征重要性排序:', ranking)RFE 的关键参数是 n_features_to_select 和 estimator。n_features_to_select 设为 6 是我在这个数据集上试出来的常见合理值,因为样本量只有 299 条,特征太多会过拟合。estimator 用随机森林比较稳健,它对非线性关系更友好,比用逻辑回归做 RFE 更不容易因为共线性而波动。跑完你会发现,ejection_fraction、serum_creatinine、age、serum_sodium 这几位经常被保留下来,和医学认知基本一致。
4. 模型构建与评估:逻辑回归和随机森林的对比实验
4.1 交叉验证:不要只用一次 train_test_split
299 条样本对机器学习来说非常少,如果只做一次训练测试划分,结果的随机性会很大。同一份数据,换一个 random_state,AUC 可能从 0.72 跳到 0.85。常见做法是用分层 K 折交叉验证,既能保证每一折正负样本比例一致,又能让模型平均使用全量数据。
from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler X = df.drop(columns=['DEATH_EVENT', 'time']) y = df['DEATH_EVENT'] lr = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(lr, X, y, cv=cv, scoring='roc_auc') print('逻辑回归交叉验证 AUC: %.3f (+/- %.3f)' % (scores.mean(), scores.std()))这里用 make_pipeline 把标准化和逻辑回归包在一起,可以避免在每一折交叉验证里对测试集提前 fit 标准化器。n_splits 设 5 是因为数据集只有 299 条,折数太多每折样本太少,折数太少则评估不稳。shuffle 加 True 是为了打乱分布,固定 random_state 才能在两次实验间复现结果。
4.2 逻辑回归与随机森林的代码实现
逻辑回归的优势是可解释性强,每个特征的系数能直接对应风险方向;随机森林的优势是能自动捕捉非线性关系,对特征量纲不敏感。两个模型对比跑一遍,你就能看到它们在 AUC 上的差异。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, accuracy_score, recall_score X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 逻辑回归 lr = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)) lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) print('LR AUC: %.3f, ACC: %.3f, Recall: %.3f' % ( roc_auc_score(y_test, lr.predict_proba(X_test)[:, 1]), accuracy_score(y_test, y_pred_lr), recall_score(y_test, y_pred_lr) )) # 随机森林 rf = RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) print('RF AUC: %.3f, ACC: %.3f, Recall: %.3f' % ( roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1]), accuracy_score(y_test, y_pred_rf), recall_score(y_test, y_pred_rf) ))stratify=y 必须在 train_test_split 里加上。不加上这个参数,随机划分可能让测试集里的死亡样本太少,算出来的指标全是偶然结果。随机森林的 n_estimators 我设到 200,max_depth 限制 5,是为了防止在 299 条样本上长成完全记忆训练集的树。如果 max_depth 不设,树会一直分裂到叶子节点纯,训练集 AUC 接近 1,测试集掉到 0.65。
4.3 ROC/AUC、准确率、召回率:医疗模型看重什么
医疗预测场景里,把真正有风险的患者漏掉,比把正常人误判成高风险更严重。所以关注顺序应该是:AUC 责任总体区分能力,Recall 决定漏诊率,准确率最后看。这个数据集的死亡事件占比约 32%,不是极端不平衡,但如果只盯准确率,模型全预测为不死亡也能拿到 68% 的准确率,毫无意义。
AUC 的代码之前已经出现过,核心是用 predict_proba 取正类概率。一定要注意,predict 返回的是 0/1 类别,用 predict 出来的结果算 AUC 会丢信息。ROC 曲线本身只是不同阈值下真正例率和假正例率的关系图,AUC 是曲线下面积。如果两个模型 AUC 接近,优先选逻辑回归,因为系数能解释,好和医疗领域的实际认知对齐。
5. 避坑:训练心血管预测模型最容易翻车的五个细节
5.1 现象:AUC 虚高到 0.9,原因:把 time 字段当成了特征
我第一次跑类似数据时,把 time 当成普通指标放进了特征列表,逻辑回归交叉验证 AUC 直接到 0.94,当时以为自己发现了金矿。后来把 time 单独和 DEATH_EVENT 做相关性分析,才发现两者高度相关。随访时间长的病人当然更可能存活到随访结束,这属于目标泄露,因为预测时根本不知道未来的随访时长。解决:特征矩阵里永远删掉 time,同时检查数据字典里有没有类似“入院天数”“观察周期”这类字段。拿到新数据先做字段溯源,再进模型。
5.2 现象:模型只会预测“不死亡”,原因:类别不平衡没处理
这个数据集死亡事件占 32%,还不算严重,但如果你换了其他心血管数据集,死亡占比可能只有 10%,那么逻辑回归会倾向于预测高比例的负样本,召回率掉到 0.1 以下。解决思路是先看分类报告,确认 recall 和 f1-score,不要只看 accuracy。如果确实不平衡,可以用 class_weight='balanced' 让模型自动调整类别权重,或者用 SMOTE 做少数类过采样,但过采样要在交叉验证内部做,否则又有泄露风险。
5.3 现象:标准化时 fit 了整个数据集,导致测试集信息混入
在 4.1 我用了 make_pipeline 就是为了避免这个问题。常见的错误写法是先 scaler.fit_transform(X),然后把整个 X 划分成训练集和测试集。这样标准化器已经看过测试集的均值和方差,测试集不在“未知数据”的模拟条件下,得到的 AUC 是偏乐观的。解决:先划分数据集,再在训练集上 fit,在测试集上只 transform。如果使用 cross_val_score,就一定要用 Pipeline 或 make_pipeline 包装。
5.4 现象:随机森林调参只动 n_estimators,遗憾效果没变化
n_estimators 决定的是树的棵数,超过一定值后模型性能进入平台期,200 棵和 500 棵差别不大。真正影响随机森林性能的是 max_depth、min_samples_leaf、max_features,以及是否启用 oob_score。在这个数据量级,把 max_depth 从默认的 None 改成 4 到 6,min_samples_leaf 设 5 到 10,往往能更有效抑制过拟合。解决:调参不要只盯 n_estimators,优先用网格搜索调 max_depth 和 min_samples_leaf。
5.5 现象:用准确率评估二分类模型,结果被负样本带偏
逻辑回归在这个数据集上的准确率可能只有 75% 左右,但 AUC 能到 0.85。如果只看准确率,你会觉得模型不行;如果只看召回率,你又可能觉得模型漏诊太严重。准确率在类别分布不均匀时意义不大。解决:至少同时输出 AUC、Recall、Precision、F1,再画 ROC 曲线,综合判断。医疗类项目尤其要把 Recall 放在仅次于 AUC 的位置,宁可多转诊几例疑似患者,也不要漏掉真病人。
6. 把模型用起来:joblib 保存、加载与单条预测封装
6.1 用 Pipeline 把预处理和模型绑定在一起
前面所有实验都是在代码里手写流程,真正要落地时,必须把标准化和模型绑成一个整体。否则每次预测新数据,你都要手动执行一遍填补缺失、标准化、特征选择,漏一步结果就全错。sklearn 的 Pipeline 可以把这些步骤串成一个对象,保存和加载都只需要面对一个文件。
from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler import joblib X = df.drop(columns=['DEATH_EVENT', 'time']) y = df['DEATH_EVENT'] num_features = ['age', 'creatinine_phosphokinase', 'ejection_fraction', 'platelets', 'serum_creatinine', 'serum_sodium'] pipe = Pipeline(steps=[ ('scaler', StandardScaler()), ('clf', LogisticRegression(max_iter=1000)) ]) pipe.fit(X, y) joblib.dump(pipe, 'heart_failure_model.pkl')这里的 Pipeline 会自动记住它是在哪些列上 fit 的,但不会记住列名,所以加载之后需要保证新数据的列顺序和训练时完全一致。我一般会把特征列顺序存成一个 JSON 文件,和 pkl 放在一起,避免日后遗忘。
6.2 封装预测函数:新病人数据直接出风险概率
保存模型只是第一步,真正的落地场景是:来一条新的患者记录,输入原始化验单数据,返回一个风险概率。这里我习惯封装一个函数,输入一个字典,输出风险概率和风险等级。
import json import joblib import pandas as pd pipe = joblib.load('heart_failure_model.pkl') with open('feature_order.json', 'r') as f: feature_order = json.load(f) def predict_risk(patient_dict): # 按训练时的特征顺序构造 DataFrame input_df = pd.DataFrame([{col: patient_dict[col] for col in feature_order}]) # 缺失值用中位数填充 for col in num_features: if input_df[col].isnull().any(): input_df[col].fillna(pipe.named_steps['scaler'].mean_[num_features.index(col)], inplace=True) prob = pipe.predict_proba(input_df)[0][1] risk_level = '高风险' if prob >= 0.5 else '低风险' return {'风险概率': round(prob, 4), '风险等级': risk_level} # 示例 sample = {'age': 68, 'anaemia': 1, 'creatinine_phosphokinase': 560, 'diabetes': 0, 'ejection_fraction': 25, 'high_blood_pressure': 1, 'platelets': 230000, 'serum_creatinine': 1.9, 'serum_sodium': 135, 'sex': 1, 'smoking': 0} print(predict_risk(sample))这段代码要注意的是缺失值处理方式,我直接用标准化器保存的均值来填充,准确说应该用训练集的中位数,但示例为了简洁用了均值的近似逻辑。实际项目中建议把每列的中位数也存成 JSON。predict_proba 返回的是二维数组,[0][1] 表示正类概率。阈值 0.5 只是默认值,临床场景下你可能需要调低到 0.3 来提高召回率。从那以后我每做一次医疗模型,都会强制把特征顺序、缺失值字典、模型文件三者一起打包,再顺手写一个这样的预测函数,省掉无数手动转格式的时间。希望帮到你。
本文还有配套的精品资源,点击获取