ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习预测心脏衰竭死亡风险:从特征选择到多模型对比的完整流程

机器学习预测心脏衰竭死亡风险:从特征选择到多模型对比的完整流程 简介心脏衰竭致死相关因素的分析与早期预测是临床数据挖掘中的常见课题这份压缩包提供了一套基于心脏病临床记录的完整分析方案面向有Python/R基础的医疗数据分析学习者。资源共10个文件以5个Python脚本、1个R脚本为主另含PDF报告、CSV原始数据集、License及README整体仅240KB且轻量易用其中脚本分别实现了数据可视化、统计学相关性检验、逻辑回归、支持向量机、随机森林以及LASSO特征选择等关键环节CSV数据可直接用于复现。已有804人浏览学习。通过该资源可完整掌握从12项临床指标的相关性分析、Lasso重要因素筛选到构建三种分类模型进行预测的一整套流程并能借助PDF报告理解各步骤的模型评价与结论报告与脚本结合便于二次开发和课程设计参考。1. 心脏衰竭死亡预测一份能从头跑到尾的机器学习资源用机器学习做心脏衰竭死亡风险预测最常见的卡点不是模型不会调而是不知道流程从哪一步开始。这份资源围绕 heart_failure_clinical_records_dataset.csv 的 299 条临床记录把 LASSO 回归、逻辑回归、SVM、随机森林串成完整链路先对 12 个特征可视化挖掘关系再用 Lasso 筛重要变量最后训练三个分类器对比效果报告.pdf 把每一步的统计检验和图表结论都写好了。对正在做机器学习课程设计的学生或想完整跑一遍特征选择加分类器流程的从业者它最大的价值是流程不用自己拼。下面按我拆这个包的实际顺序把能直接照抄的代码和参数坑都展开。2. 数据理解与预处理12 个临床特征和 299 条记录怎么读2.1 字段清单先分清连续变量和二值变量数据集 heart_failure_clinical_records_dataset.csv 记录的是心衰患者随访期间的临床指标299 条记录每行对应一个病人。任务是二分类预测随访期内是否死亡标签是 DEATH_EVENT。拿到数据先列字段清单是我拆这类项目的第一步因为后面标准化、特征选择、可视化全都要依靠字段类型判断。字段名类型临床含义建模角色age连续年龄基础风险因素anaemia二值 0/1是否贫血共病状态creatinine_phosphokinase连续肌酸磷酸激酶 CPK心肌损伤标志物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是否死亡目标标签这张表里 age、creatinine_phosphokinase、ejection_fraction、platelets、serum_creatinine、serum_sodium、time 是连续变量anaemia、diabetes、high_blood_pressure、sex、smoking 是 0/1 二值变量。二值变量做不做标准化影响很小连续变量之间尺度差异很大——platelets 动辄几十万serum_sodium 只有一百三四十。如果直接把原始值丢给 Lasso数值大的维度必然占便宜这个细节直接关系到第 3 章为什么要先标准化。2.2 pandas 体检缺失值、类型和标签平衡一次看清拿到 CSV 的第一步我习惯用 pandas 做一次快速体检确认数据类型、缺失情况和标签分布而不是直接建模。import pandas as pd df pd.read_csv(heart_failure_clinical_records_dataset.csv) print(样本数:, df.shape[0], 特征数:, df.shape[1]) print(df.info()) print(缺失值统计) print(df.isna().sum()) print(DEATH_EVENT 分布) print(df[DEATH_EVENT].value_counts(normalizeTrue))df.info() 会列出每个字段的 dtype 以及非空数量能快速发现数据类型异常isna().sum() 对医疗数据尤其重要因为很多临床记录存在大量留空。这份数据集本身比较干净一般情况下 isna().sum() 全为 0不需要填充。提示如果换成别的临床数据集遇到缺失连续变量建议用中位数填充二值变量用众数填充尽量避免直接删行——样本量本来就小。value_counts(normalizeTrue) 返回比例而不是计数一眼就能看出标签是否平衡。后续所有评估都要盯紧少数类因为这类临床场景里 DEATH_EVENT 通常占比不到四成。2.3 可视化检查从箱线图里找区分度摘要里提到的数据可视化环节落到代码上最快的方式是画按标签分组的箱线图。我一般会先看两个最可疑的连续变量血清肌酐和射血分数。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(10, 4)) sns.boxplot(xDEATH_EVENT, yserum_creatinine, datadf, axaxes[0]) sns.boxplot(xDEATH_EVENT, yejection_fraction, datadf, axaxes[1]) plt.tight_layout() plt.show()boxplot 展示的是四分位数、中位数和离群点。如果两组能明显分开说明该特征天然有区分度如果箱子几乎重叠后面 Lasso 大概率会把它淘汰。实际跑下来死亡组血清肌酐整体偏高射血分数整体偏低方向相反这种线索在预处理阶段就应该心里先有数。2.4 训练测试划分stratify 保证标签比例不漂移数据没有缺失也没有明显异常值接下来就是划分训练集和测试集。这里有一个容易被忽略的参数stratify。from sklearn.model_selection import train_test_split X df.drop(columns[DEATH_EVENT]) y df[DEATH_EVENT] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) print(训练集死亡比例:, y_train.mean()) print(测试集死亡比例:, y_test.mean())test_size0.3 是常见做法上下浮动到 0.2 也可以但 299 条小样本不建议测试集低于三成否则评估曲线会非常抖。random_state42 固定划分保证后续任何随机过程都能复现。stratifyy 的作用是让训练集和测试集的标签比例都与原始数据一致——不写这个参数两边死亡比例可能差 10 个点模型评估就失真了。3. Lasso 回归做特征选择L1 惩罚怎么把无关变量压成 03.1 分类任务里为什么用 Lasso稀疏解的价值把 Lasso 用在分类场景里严格说是对逻辑回归加 L1 惩罚得到稀疏权重而 LASSOreg.py 里更朴素的做法是直接对二值标签做带 L1 惩罚的线性回归。工程上把它当特征筛选器用就够了输出一组系数系数为 0 的特征就是被淘汰的特征。原理上Lasso 的损失函数由两部分组成拟合误差加上 alpha 乘以权重的 L1 范数。L1 范数在零点处有尖角优化时某些系数会被精确推到 0而不是像岭回归那样只压小。Ridge 永远给不出稀疏解Lasso 可以。方法正则项系数特点299 条小样本下的表现无正则回归无全部保留方差偏大容易过拟合Ridge L2sum(w²)系数压缩但不归零适合强共线性Lasso L1sum(abs(w))可精确归零直接特征选择我建议把 Lasso 当作粗筛而不是结论。它擅长在相关特征之间选一个代表比如血清肌酐和射血分数都和心衰严重程度相关Lasso 可能只保留其中一个。这是它的优势也是它的局限——选出来的是预测贡献不是因果机制。3.2 LASSOreg.py 核心流程StandardScaler 加 LassoCVLASSOreg.py 里最核心的一段标准化在前LassoCV 在后。顺序不能反原因我在 2.1 里提过血小板动辄几十万血清钠只有一百多如果不标准化Lasso 的惩罚是按数值大小分配的大数值特征天然被保留这不是它更重要而是尺度占了便宜。from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LassoCV import pandas as pd scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) lasso LassoCV( cv5, max_iter100000, random_state42, alphasNone ) lasso.fit(X_train_s, y_train) coef_series pd.Series(lasso.coef_, indexX.columns) print(选中的 alpha:, lasso.alpha_) print(系数为 0 的特征:, coef_series[coef_series 0].index.tolist()) print(保留的特征:, coef_series[coef_series ! 0].index.tolist())LassoCV 用交叉验证自动选 alpha不需要手动调。cv5 是常用折数299 条样本折数太多反而慢max_iter100000 是坐标下降的最大迭代次数给大一点防止收敛警告。alphasNone 会触发 sklearn 自动生成一条从大到小的 alpha 路径覆盖足够宽。注意scaler 只 fit_transform 训练集测试集只用 transform。如果对整个 X 先 fit 再划分测试集的均值和方差信息就泄漏到了训练流程里后面评估出的指标会虚高。如果你发现自动 alpha 路径选出的特征不理想可以手动指定 alphasnp.logspace(-4, 0, 100)把网格放宽再扫一遍结果。3.3 看系数路径哪些特征最先被淘汰除了看最终系数我还会画一张系数路径图。横轴是惩罚强度 alpha纵轴是每个特征的系数从弱到强化惩罚看谁能撑到最后。import matplotlib.pyplot as plt from sklearn.linear_model import lasso_path alphas, coefs, _ lasso_path(X_train_s, y_train, eps1e-3) for i, col in enumerate(X.columns): plt.plot(alphas, coefs[i], labelcol) plt.xscale(log) plt.xlabel(alpha) plt.ylabel(coefficient) plt.legend(locupper right) plt.show()解读方法很直接alpha 最小时所有特征都有非零系数alpha 增大系数逐个变成 0。哪条曲线最先贴到 0 轴代表对应特征在模型里最不抗淘汰。这张图在报告.pdf 里通常也有一版自己能画出来更好答辩或汇报时解释 Lasso 的选参逻辑会清楚很多。3.4 选完特征后做什么把保留列直接喂给分类器Lasso 的产出不能停在打印层面最终要落到后续模型到底用哪些列。selected_cols coef_series[coef_series ! 0].index.tolist() print(保留特征, selected_cols) X_train_sel X_train[selected_cols] X_test_sel X_test[selected_cols]后面第 4 章的分类器可以用两种方式跑一是直接用标准化后的全量特征二是只用 Lasso 选出的 selected_cols。我一般两个都跑结果放一起对比。如果 Lasso 删掉的变量加回去之后AUC 反而明显上升说明 Lasso 的 alpha 选得太狠需要回调如果 AUC 基本不变那就放心用稀疏特征集模型更简单解释起来也更省事。4. 逻辑回归、SVM、随机森林三个分类器从训练到参数解释4.1 logicalGrad.py手写梯度下降的逻辑回归资源里的 logicalGrad.py 是用 numpy 从零实现逻辑回归的脚本。这种写法对理解模型特别有帮助因为 sklearn 的 LogisticRegression 是一个黑匣子你不知道每一步更新了什么。下面是核心结构完全对照逻辑回归的梯度推导import numpy as np class LogisticRegressionGD: def __init__(self, lr0.01, n_iters1000): self.lr lr self.n_iters n_iters self.weights None self.bias 0.0 def _sigmoid(self, z): z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) for _ in range(self.n_iters): pred self._sigmoid(np.dot(X, self.weights) self.bias) dw (1 / n_samples) * np.dot(X.T, (pred - y)) db (1 / n_samples) * np.sum(pred - y) self.weights - self.lr * dw self.bias - self.lr * db return self def predict(self, X, threshold0.5): proba self._sigmoid(np.dot(X, self.weights) self.bias) return (proba threshold).astype(int)sigmoid 把线性输出转成概率损失函数对权重求导后梯度恰好是 (pred - y) 与 X 的内积形式。np.clip 是为了防止 exp 溢出产生 NaN这一行别看小没有它手写版本很容易跑几次就出.inf。lr0.01 是学习率太大会震荡太小收敛慢n_iters1000 是迭代轮数跑完后可以和 sklearn 的结果对比权重验算梯度有没有写错。4.2 regression.py 与 heart_failure.R两种实现交叉验证logicalGrad.py 是教学版本regression.py 里通常还有 sklearn 实现用来做精调。如果要给逻辑回归加 L1 惩罚等价于在分类任务里再跑一次 Lasso 逻辑回归这个操作可以在 sklearn 里直接做from sklearn.linear_model import LogisticRegression lr_sk LogisticRegression( penaltyl1, solverliblinear, C1.0, random_state42 ) lr_sk.fit(X_train_s, y_train) coef_dict dict(zip(X.columns, lr_sk.coef_[0])) print(LR L1 系数, coef_dict)penaltyl1 就是 L1 正则solver 必须换成 liblinear因为这是唯一在 sklearn 里支持 L1 的求解方式。C 是正则强度的倒数C 越小系数越稀疏和 Lasso 的 alpha 正好方向相反调参时别搞混。如果你习惯 R 语言heart_failure.R 里的 glmnet 版本也是干同样的事结果可以和 Python 互相核对library(glmnet) x - as.matrix(heart_data[, 1:12]) y - heart_data$DEATH_EVENT fit - cv.glmnet(x, y, family binomial, alpha 1) coef(fit, s lambda.min)familybinomial 指定逻辑回归alpha1 表示 L1 惩罚lambda.min 取交叉验证误差最小的那组系数。同一份数据两边跑出差不多的稀疏系数基本可以确认流程没写错。4.3 Svm.pyRBF 核的 C 和 gamma 直觉SVM 在这个场景里的好处是特征数不多样本量小RBF 核能刻画非线性边界训练也很快。from sklearn.svm import SVC from sklearn.metrics import classification_report svm SVC( kernelrbf, C1.0, gammascale, probabilityTrue, random_state42 ) svm.fit(X_train_s, y_train) y_pred_svm svm.predict(X_test_s) print(classification_report(y_test, y_pred_svm))调 C 和 gamma 是玄学但直觉可以建立。C 是错分惩罚越大越不容忍训练集错误容易过拟合gamma 控制单个样本的影响半径scale 模式会根据特征数量自动设初值比手动给 gamma0.1 之类的更稳。提示probabilityTrue 会额外做概率校准让 predict_proba 可用但训练时间明显变长。如果只做分类不画 ROC可以关掉。第 6 章要画 AUC所以这里开着。实际复现时一般先用默认 C1.0 和 gammascale 跑一遍再考虑用 GridSearchCV 在 C[0.1, 1, 10] 和 gamma[0.01, 0.1, 1] 上搜一搜。299 条小样本搜索成本几乎可以忽略。4.4 forest.py随机森林参数和特征重要性随机森林是三个模型里唯一不需要标准化的树模型对量纲天然免疫。用它有两个目的参与分类对比以及产出特征重要性和 Lasso 结果互相印证。from sklearn.ensemble import RandomForestClassifier import pandas as pd rf RandomForestClassifier( n_estimators200, max_depth5, min_samples_leaf2, max_featuressqrt, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) importance pd.Series(rf.feature_importances_, indexX.columns) print(特征重要性排序) print(importance.sort_values(ascendingFalse))n_estimators200 是树的数量100 到 300 之间比较稳妥太少方差大太多收益递减max_depth5 限制单棵树的深度防止单棵树记住太多训练样本min_samples_leaf2 保证叶子节点至少 2 个样本对 299 条的小样本很重要否则很容易长出纯叶子然后把噪声也学进去max_featuressqrt 是分类默认策略每次分裂只看特征总数的平方根个特征增加树与树之间的独立性。跑完后把 random forest 的 importance 前几名和 Lasso 的保留特征对比。如果两个方法选出的集合高度重合比如血清肌酐、射血分数、时间这三个都在那这几列基本可以确定是核心预测变量。5. 避坑清单不平衡标签、标准化泄漏和随机种子这一章写的都是实际拆包时容易翻车的点很多看起来是小细节但直接影响结果能不能复现、报告怎么解释。5.1 只报准确率少数类模型彻底失效现象测试集 accuracy 显示 0.78看着不错但打开混淆矩阵发现死亡样本几乎全被预测成存活少数类的 recall 只有 0.1。原因DEATH_EVENT 正样本占比不到四成模型只要永远预测多数类准确率也能到 0.6 以上。在这个标签分布下accuracy 天然虚高根本衡量不了模型对高危病人的识别能力。解决评估至少看三样——混淆矩阵、precision/recall、AUC。必要时给模型加 class_weightbalanced或对训练集做 SMOTE 过采样。判断一个模型在医疗预测场景里能不能用先看少数类的 recall不能只看整体准确率。下面这段是每次评估前我都会先跑一遍的代码from sklearn.metrics import confusion_matrix, classification_report print(classification_report(y_test, y_pred_svm)) print(confusion_matrix(y_test, y_pred_svm))classification_report 输出的 recall 一列对少数类才是真正有用的数字。5.2 先标准化再划分测试集信息泄漏进训练现象同一个脚本把 StandardScaler 放在 train_test_split 之前对整个 X 做 fit训练后测试集指标异常地好可一换新数据就明显回落。原因scaler 在全部数据上学习了均值和方差等于测试集的分布信息提前参与了训练。这不是模型变强了是信息泄漏。解决顺序固定为三步——先 split再 scaler.fit_transform(X_train)最后 scaler.transform(X_test)。第 3.2 的代码就是按这个顺序写的照抄不会踩。这个坑在报告里最难看出来因为数字越好看越不容易质疑。5.3 随机种子不固定同一个代码两次跑出两个结果现象昨天跑 AUC 是 0.86今天重启电脑再跑变成 0.82说不清模型真实水平。原因train_test_split、LassoCV、SVM、随机森林全部涉及随机过程。只要有一个环节没固定 random_state整条链路的结果都会漂移。解决所有带随机性的地方统一 random_state42。需要提醒的是做网格搜索的时候要同时固定搜索内部的 cv 和模型自身的 random_state否则每次搜出来的最优参数都不同这个比单次训练更隐蔽。5.4 Lasso 的 alpha 不调整就默认现象用 Lasso 跑完特征选择结果两级分化——要么一个特征都没筛掉要么所有系数全变 0。原因alpha 太大惩罚过重把所有系数全逼到零alpha 太小L1 惩罚没起作用和普通回归没区别。单靠 Lasso 默认的 alpha 不一定适配这份数据的尺度。解决不要手调单个值直接用 LassoCV 自动选 alpha。如果自动路径出来的结果依然不理想把 alphas 换成手动网格 np.logspace(-4, 0, 100)覆盖范围拉大再跑一轮。选出来的特征集要回第 4 章的分类器里验证不要只凭 Lasso 系数就下结论。6. 验证与进阶混淆矩阵、ROC-AUC 和 SHAP 解释6.1 统一评估AUC 加混淆矩阵看少数类三个模型跑完后我会写一段统一评估代码保证评估标准完全一致from sklearn.metrics import roc_auc_score, confusion_matrix proba_lr lr_sk.predict_proba(X_test_s)[:, 1] proba_svm svm.predict_proba(X_test_s)[:, 1] proba_rf rf.predict_proba(X_test)[:, 1] for name, proba in [(LR, proba_lr), (SVM, proba_svm), (RF, proba_rf)]: print(name, AUC:, round(roc_auc_score(y_test, proba), 4))AUC 不需要指定阈值对不平衡数据远比 accuracy 稳定。三者放在一起比较逻辑回归通常可解释性好但上限偏低SVM 在非线性小样本上往往精度不差随机森林对噪声更稳。如果 AUC 差距很小我一般直接选更简单的逻辑回归而不是挑一个黑匣子。6.2 SHAP给随机森林的特征重要性做个验算随机森林自带的 feature_importances 只给排序不给方向和交互信息。进入报告阶段想再进一步可以装 shap 包对随机森林做一次解释import shap explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, feature_namesX.columns)summary_plot 输出的图能看出每个特征在什么数值区间把预测推向死亡还是推向存活。这一步是可选但加分的尤其是答辩时有人问为什么这几个特征重要光靠 Lasso 系数和随机森林重要性回答得不够有说服力SHAP 图可以直接说明方向关系。我第一次复现这个项目时就栽在标准化的顺序上测试集 AUC 高到不敢信重查代码才发现 5.2 那个泄漏问题。从那以后我每次跑分类项目都强制走一遍固定流程先 split再 scaler统一评估 AUC最后用 SHAP 验证特征方向。这份资源把数据、脚本和报告都打包好了按上面的顺序跑一遍再对着报告.pdf 核对结论能省掉不少自己拼流程的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表