简介:心脏衰竭致死相关因素的分析与早期预测,是临床数据挖掘中的常见课题;这份压缩包提供了一套基于心脏病临床记录的完整分析方案,面向有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(normalize=True))df.info() 会列出每个字段的 dtype 以及非空数量,能快速发现数据类型异常;isna().sum() 对医疗数据尤其重要,因为很多临床记录存在大量留空。这份数据集本身比较干净,一般情况下 isna().sum() 全为 0,不需要填充。
提示:如果换成别的临床数据集遇到缺失,连续变量建议用中位数填充,二值变量用众数填充,尽量避免直接删行——样本量本来就小。
value_counts(normalize=True) 返回比例而不是计数,一眼就能看出标签是否平衡。后续所有评估都要盯紧少数类,因为这类临床场景里 DEATH_EVENT 通常占比不到四成。
2.3 可视化检查:从箱线图里找区分度
摘要里提到的"数据可视化"环节,落到代码上最快的方式是画按标签分组的箱线图。我一般会先看两个最可疑的连续变量:血清肌酐和射血分数。
import matplotlib.pyplot as plt import seaborn as sns fig, axes = plt.subplots(1, 2, figsize=(10, 4)) sns.boxplot(x="DEATH_EVENT", y="serum_creatinine", data=df, ax=axes[0]) sns.boxplot(x="DEATH_EVENT", y="ejection_fraction", data=df, ax=axes[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_size=0.3, random_state=42, stratify=y ) print("训练集死亡比例:", y_train.mean()) print("测试集死亡比例:", y_test.mean())test_size=0.3 是常见做法,上下浮动到 0.2 也可以,但 299 条小样本不建议测试集低于三成,否则评估曲线会非常抖。random_state=42 固定划分,保证后续任何随机过程都能复现。stratify=y 的作用是让训练集和测试集的标签比例都与原始数据一致——不写这个参数,两边死亡比例可能差 10 个点,模型评估就失真了。
3. Lasso 回归做特征选择:L1 惩罚怎么把无关变量压成 0
3.1 分类任务里为什么用 Lasso:稀疏解的价值
把 Lasso 用在分类场景里,严格说是对逻辑回归加 L1 惩罚得到稀疏权重;而 LASSOreg.py 里更朴素的做法是直接对二值标签做带 L1 惩罚的线性回归。工程上把它当特征筛选器用就够了:输出一组系数,系数为 0 的特征就是被淘汰的特征。
原理上,Lasso 的损失函数由两部分组成:拟合误差加上 alpha 乘以权重的 L1 范数。L1 范数在零点处有尖角,优化时某些系数会被精确推到 0,而不是像岭回归那样只压小。Ridge 永远给不出稀疏解,Lasso 可以。
| 方法 | 正则项 | 系数特点 | 299 条小样本下的表现 |
|---|---|---|---|
| 无正则回归 | 无 | 全部保留,方差偏大 | 容易过拟合 |
| Ridge L2 | sum(w²) | 系数压缩但不归零 | 适合强共线性 |
| Lasso L1 | sum(abs(w)) | 可精确归零 | 直接特征选择 |
我建议把 Lasso 当作"粗筛"而不是"结论"。它擅长在相关特征之间选一个代表,比如血清肌酐和射血分数都和心衰严重程度相关,Lasso 可能只保留其中一个。这是它的优势,也是它的局限——选出来的是预测贡献,不是因果机制。
3.2 LASSOreg.py 核心流程:StandardScaler 加 LassoCV
LASSOreg.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( cv=5, max_iter=100000, random_state=42, alphas=None ) lasso.fit(X_train_s, y_train) coef_series = pd.Series(lasso.coef_, index=X.columns) print("选中的 alpha:", lasso.alpha_) print("系数为 0 的特征:", coef_series[coef_series == 0].index.tolist()) print("保留的特征:", coef_series[coef_series != 0].index.tolist())LassoCV 用交叉验证自动选 alpha,不需要手动调。cv=5 是常用折数,299 条样本折数太多反而慢;max_iter=100000 是坐标下降的最大迭代次数,给大一点防止收敛警告。alphas=None 会触发 sklearn 自动生成一条从大到小的 alpha 路径,覆盖足够宽。
注意:scaler 只 fit_transform 训练集,测试集只用 transform。如果对整个 X 先 fit 再划分,测试集的均值和方差信息就泄漏到了训练流程里,后面评估出的指标会虚高。
如果你发现自动 alpha 路径选出的特征不理想,可以手动指定 alphas=np.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, eps=1e-3) for i, col in enumerate(X.columns): plt.plot(alphas, coefs[i], label=col) plt.xscale("log") plt.xlabel("alpha") plt.ylabel("coefficient") plt.legend(loc="upper 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, lr=0.01, n_iters=1000): 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, threshold=0.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。lr=0.01 是学习率,太大会震荡,太小收敛慢;n_iters=1000 是迭代轮数,跑完后可以和 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( penalty="l1", solver="liblinear", C=1.0, random_state=42 ) lr_sk.fit(X_train_s, y_train) coef_dict = dict(zip(X.columns, lr_sk.coef_[0])) print("LR L1 系数:", coef_dict)penalty="l1" 就是 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")family="binomial" 指定逻辑回归,alpha=1 表示 L1 惩罚,lambda.min 取交叉验证误差最小的那组系数。同一份数据两边跑出差不多的稀疏系数,基本可以确认流程没写错。
4.3 Svm.py:RBF 核的 C 和 gamma 直觉
SVM 在这个场景里的好处是:特征数不多,样本量小,RBF 核能刻画非线性边界,训练也很快。
from sklearn.svm import SVC from sklearn.metrics import classification_report svm = SVC( kernel="rbf", C=1.0, gamma="scale", probability=True, random_state=42 ) 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" 模式会根据特征数量自动设初值,比手动给 gamma=0.1 之类的更稳。
提示:probability=True 会额外做概率校准,让 predict_proba 可用,但训练时间明显变长。如果只做分类不画 ROC,可以关掉。第 6 章要画 AUC,所以这里开着。
实际复现时,一般先用默认 C=1.0 和 gamma="scale" 跑一遍,再考虑用 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_estimators=200, max_depth=5, min_samples_leaf=2, max_features="sqrt", random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) importance = pd.Series(rf.feature_importances_, index=X.columns) print("特征重要性排序:") print(importance.sort_values(ascending=False))n_estimators=200 是树的数量,100 到 300 之间比较稳妥,太少方差大,太多收益递减;max_depth=5 限制单棵树的深度,防止单棵树记住太多训练样本;min_samples_leaf=2 保证叶子节点至少 2 个样本,对 299 条的小样本很重要,否则很容易长出纯叶子然后把噪声也学进去;max_features="sqrt" 是分类默认策略,每次分裂只看特征总数的平方根个特征,增加树与树之间的独立性。
跑完后把 random forest 的 importance 前几名和 Lasso 的保留特征对比。如果两个方法选出的集合高度重合,比如血清肌酐、射血分数、时间这三个都在,那这几列基本可以确定是核心预测变量。
5. 避坑清单:不平衡标签、标准化泄漏和随机种子
这一章写的都是实际拆包时容易翻车的点,很多看起来是小细节,但直接影响结果能不能复现、报告怎么解释。
5.1 只报准确率,少数类模型彻底失效
现象:测试集 accuracy 显示 0.78,看着不错,但打开混淆矩阵发现死亡样本几乎全被预测成存活,少数类的 recall 只有 0.1。
原因:DEATH_EVENT 正样本占比不到四成,模型只要永远预测多数类,准确率也能到 0.6 以上。在这个标签分布下,accuracy 天然虚高,根本衡量不了模型对高危病人的识别能力。
解决:评估至少看三样——混淆矩阵、precision/recall、AUC。必要时给模型加 class_weight="balanced",或对训练集做 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_state=42。需要提醒的是,做网格搜索的时候要同时固定搜索内部的 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_names=X.columns)summary_plot 输出的图能看出每个特征在什么数值区间把预测推向死亡还是推向存活。这一步是可选但加分的,尤其是答辩时有人问"为什么这几个特征重要",光靠 Lasso 系数和随机森林重要性回答得不够有说服力,SHAP 图可以直接说明方向关系。
我第一次复现这个项目时就栽在标准化的顺序上,测试集 AUC 高到不敢信,重查代码才发现 5.2 那个泄漏问题。从那以后,我每次跑分类项目都强制走一遍固定流程:先 split,再 scaler,统一评估 AUC,最后用 SHAP 验证特征方向。这份资源把数据、脚本和报告都打包好了,按上面的顺序跑一遍,再对着报告.pdf 核对结论,能省掉不少自己拼流程的时间。希望帮到你。
本文还有配套的精品资源,点击获取