ARTICLE DETAIL

资讯详情

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

心血管预测模型完整流程:从数据清洗到特征选择与随机森林评估

心血管预测模型完整流程:从数据清洗到特征选择与随机森林评估 简介心血管疾病预测模型的Python实现源码与配套数据集面向数学建模、数据分析及机器学习初学者提供一个从真实医疗数据到风险预测的完整实践案例。资源包共3个文件包含可交互执行的Notebook、模块化脚本以及CSV格式的心血管样本数据压缩后仅83KB便于快速下载并直接运行。目前已有1123人学习下载。代码覆盖数据清洗与缺失值处理、相关性分析与主成分分析、标准化归一化等预处理环节并示范逻辑回归、随机森林、支持向量机、神经网络等模型构建同时通过交叉验证、ROC曲线、AUC值、准确率、召回率等指标完成模型评估结合网格搜索与随机搜索调参并使用绘图工具生成特征重要性图、学习曲线等直观揭示模型行为。资源中还包含特征筛选脚本、模型保存与加载等工具实用性强既适合课堂实训与竞赛演练也可作为医疗数据分析或智能健康应用的起步模板。1. 心血管预测模型的第一关把 heart_failure.csv 当产品而不是表格很多人拿到这份“心血管预测模型数据代码”资源第一反应是打开.ipynb直接跑一遍看到 accuracy 0.85 就觉得完事了。实际在医疗风控场景里这个动作恰恰是最不重要的。做过心衰患者风险分层的人都知道真正的难点不在于用随机森林还是 XGBoost而在于数据本身心率缺失怎么补、血清钠异常值怎么界定、年龄和肌酐的交互项要不要保留。这个资源包的脉络很清晰heart_failure.csv是数据底座feature_selection.py是特征筛选脚本.ipynb是完整建模流程适合数学建模竞赛选手、转行医疗数据分析和想要一套可复现代码的机器学习初学者。下面按数据清洗、特征选择、模型训练、结果验证四个层次拆开讲每段代码都可以直接改路径复现。2. 数据清洗与特征工程从 heart_failure.csv 到可训练特征矩阵2.1 先读字段heart_failure.csv 里每一列的背后是临床含义这份数据是典型的心衰临床记录表字段结构大致包含年龄、贫血、肌酐磷酸激酶、糖尿病史、射血分数、高血压史、血小板、血清肌酐、血清钠、性别、吸烟史、随访时间和死亡事件。英文列名如age、anaemia、creatinine_phosphokinase、diabetes、ejection_fraction、high_blood_pressure、platelets、serum_creatinine、serum_sodium、sex、smoking、time、DEATH_EVENT其中DEATH_EVENT是标签列1 表示随访期内死亡0 表示存活。建模前必须先做一次字段体检常见做法是写一个数据预览函数把每列的类型、缺失率、唯一值数量一次性打出来。很多人拿到表直接data.describe()就过了这是不够的因为anaemia、diabetes、smoking这类二值变量在 describe 里看不出分布失衡程度。import pandas as pd data pd.read_csv(heart_failure.csv) summary pd.DataFrame({ dtype: data.dtypes.astype(str), missing_rate: data.isnull().mean().round(4), nunique: data.nunique(), min: data.min(numeric_onlyTrue), max: data.max(numeric_onlyTrue) }) print(summary)这段代码的核心是isnull().mean()它直接给出每列缺失比例而nunique帮你快速识别哪些列其实是类别变量而不是连续变量。比如high_blood_pressure如果 nunique 为 2说明它就是一个 0/1 标志不需要做标准化。min和max则用于初步判断异常值比如serum_sodium的正常范围在 135 到 145 mmol/L如果出现 100 以下或 150 以上就要考虑是录入错误还是真实病例不能盲目删除。2.2 缺失值、异常值与数据切分医疗数据的删除要谨慎医疗数据最忌讳直接dropna()。比如血清肌酐对心衰患者来说是肾功能的核心指标如果整行删除会连带丢掉年龄、射血分数等有效信息。常见的做法是分类型处理连续变量用中位数填充类别变量用众数填充同时对异常值做分位数截断。from sklearn.model_selection import train_test_split continuous_cols [age, creatinine_phosphokinase, ejection_fraction, platelets, serum_creatinine, serum_sodium, time] categorical_cols [anaemia, diabetes, high_blood_pressure, sex, smoking] for col in continuous_cols: q_low data[col].quantile(0.01) q_high data[col].quantile(0.99) data[col] data[col].clip(q_low, q_high) data[col] data[col].fillna(data[col].median()) for col in categorical_cols: data[col] data[col].fillna(data[col].mode()[0]) X data.drop(DEATH_EVENT, axis1) y data[DEATH_EVENT] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )这里的clip(0.01, 0.99)是分位数截断把极值压缩到 1% 到 99% 分位范围内比直接删除行更稳妥因为保留了样本量。stratifyy是分层抽样保证训练集和测试集中死亡事件的比例和原始数据一致。心衰数据集的死亡事件占比通常只有三成左右不做分层的话小概率随机种子可能导致测试集里几乎没有正样本AUC 计算结果会失真。2.3 构造有医学解释性的派生特征原始字段足够建模但想要提升模型上限可以基于医学常识构造交互特征。常见的是把年龄和肾功能结合起来老年心衰患者如果血清肌酐偏高死亡风险会显著上升这种交互效应在原始特征中无法直接体现。import numpy as np X_train[age_creatinine] X_train[age] * X_train[serum_creatinine] X_train[ef_sodium_ratio] X_train[ejection_fraction] / (X_train[serum_sodium] 1e-6) X_test[age_creatinine] X_test[age] * X_test[serum_creatinine] X_test[ef_sodium_ratio] X_test[ejection_fraction] / (X_test[serum_sodium] 1e-6)age_creatinine的物理意义是估算的肾功能储备随年龄的衰减速度ef_sodium_ratio则是心功能与电解质平衡的比值低射血分数伴随低钠血症在心衰指南里是明确的预后不良标志。需要注意的是这种手工特征必须同时作用于训练集和测试集不能只加在训练集上否则预测时会报特征数不匹配。加1e-6是为了防止血清钠为 0 时除零虽然临床上血清钠不会为 0但代码层面要防御。3. 特征选择机制拆解feature_selection.py 里的筛选逻辑与参数3.1 过滤式、包裹式与嵌入式先明确选择标准再写代码特征工程做完后原始特征加上派生特征大约有 15 个左右。这个体量用随机森林勉强能跑但你想用逻辑回归做业务解释时特征一多就容易出现多重共线性。feature_selection.py的核心逻辑通常分两层第一层是过滤式用统计指标快速排除与标签无关的特征第二层是嵌入式用模型自身的特征重要性做二次确认。方法适用场景计算开销典型指标过滤式特征数量多先快速粗筛低卡方、互信息、方差阈值包裹式特征量少追求最优子集高递归特征消除嵌入式需要特征重要性排序中L1 正则、树模型 feature_importances_在医疗场景里我一般建议先过滤式再做嵌入式不要一上来就递归特征消除因为数据量不大但特征之间有临床关联包裹式容易过拟合。3.2 卡方检验与互信息过滤式选择的具体实现feature_selection.py里最常见的过滤式方案是SelectKBest配合chi2或mutual_info_classif。注意chi2要求输入非负所以要先对数据做 MinMaxScaler 归一化否则负数特征会被直接丢弃。from sklearn.feature_selection import SelectKBest, chi2, mutual_info_classif from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) chi_selector SelectKBest(score_funcchi2, k10) X_train_chi chi_selector.fit_transform(X_train_scaled, y_train) X_test_chi chi_selector.transform(X_test_scaled) selected_chi X_train.columns[chi_selector.get_support()] print(卡方筛选保留特征:, selected_chi.tolist()) mi_selector SelectKBest(score_funcmutual_info_classif, k10) X_train_mi mi_selector.fit_transform(X_train_scaled, y_train) X_test_mi mi_selector.transform(X_test_scaled) selected_mi X_train.columns[mi_selector.get_support()] print(互信息筛选保留特征:, selected_mi.tolist())get_support()返回布尔掩码直接映射到列名。卡方检验适合离散化后的特征对线性和非线性关系都能捕捉互信息更灵活能捕捉非线性关联。实际操作中k10不是固定值我会在 8 到 12 之间循环测试观察 AUC 的变化而不是只看分数排序。3.3 随机森林特征重要性与阈值筛选过滤式只看单个特征与标签的关系忽略了特征之间的冗余。随机森林的特征重要性可以反映特征在树分裂中的贡献程度但它有偏向性数值型特征和取值多的特征容易被高估。所以更稳妥的做法是同时对比多个模型的重要性得分取交集。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth5, min_samples_leaf5, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) importance pd.Series(rf.feature_importances_, indexX_train.columns).sort_values(ascendingFalse) print(importance.head(10)) # 筛选保留累计重要性达到 95% 的最小特征集 cumsum importance.cumsum() selected_rf cumsum[cumsum 0.95].index.tolist() print(随机森林筛选保留特征:, selected_rf)重点解释一下min_samples_leaf5的作用叶节点最小样本数设大一点树不容易学到噪声模式特征重要性会更稳定。cumsum() 0.95的意思是按重要性从高到低累加当累加占比首次超过 95% 时停止保留前面这些特征。这个阈值可以根据调参结果调整如果后续模型 AUC 没有明显下降可以进一步压缩特征数。4. 逻辑回归与随机森林的建模、交叉验证与超参数寻优4.1 基线模型对比与分层交叉验证特征选择完成后进入建模环节。.ipynb里通常先跑一个基线对比把逻辑回归、决策树、随机森林在训练集上的交叉验证分数一次性打出来。这里不要用默认的cross_val_score因为默认是分层 K 折如果你自己指定 KFold 而不分层正样本分布不均会导致验证分数忽高忽低。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import StratifiedKFold, cross_val_score models { logistic: LogisticRegression(max_iter1000, random_state42), decision_tree: DecisionTreeClassifier(max_depth4, random_state42), random_forest: RandomForestClassifier(n_estimators200, max_depth6, random_state42) } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) for name, model in models.items(): scores cross_val_score(model, X_train_chi, y_train, cvcv, scoringroc_auc) print(f{name}: AUC {scores.mean():.4f} ± {scores.std():.4f})这里用roc_auc而不是accuracy作为评估指标是因为心衰数据存在类别不平衡准确率会被多数类主导而 AUC 衡量的是模型区分正负样本的能力对不平衡数据更鲁棒。StratifiedKFold的shuffleTrue很重要它能打乱样本顺序避免按原始顺序划分时引入分组偏差。4.2 类别不平衡class_weight 与阈值移动如果训练集中死亡事件占比低于 30%需要对模型做不平衡处理。最简单的方式是在模型里加class_weightbalanced让少数类获得更高权重。注意加 class_weight 后预测的概率分布会发生偏移后续画 ROC 曲线时要基于调整后的概率重新计算。from sklearn.ensemble import RandomForestClassifier rf_balanced RandomForestClassifier( n_estimators300, max_depth6, min_samples_leaf4, class_weightbalanced, random_state42, n_jobs-1 ) rf_balanced.fit(X_train_chi, y_train) print(训练集类别分布:, y_train.value_counts().to_dict())class_weightbalanced的计算逻辑是n_samples / (n_classes * np.bincount(y))也就是说少数类样本的惩罚系数会自动放大。使用这种策略后模型的召回率通常会上升但精确率可能下降这就是典型的查全查准权衡。如果你对接的业务方更看重不漏掉高风险患者那就优先保召回率。4.3 网格搜索寻优参数网格怎么设才合理调参不是把所有参数扔进GridSearchCV就完事网格太大会让搜索时间膨胀网格太小又容易错过最优组合。heart_failure.csv只有 299 行参数网格应该控制在 50 组以内否则 5 折交叉验证会消耗大量时间在无意义的搜索上。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300], max_depth: [4, 6, 8], min_samples_leaf: [2, 4, 6], max_features: [sqrt, log2] } grid GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42), param_grid, scoringroc_auc, cvStratifiedKFold(n_splits5, shuffleTrue, random_state42), n_jobs-1 ) grid.fit(X_train_chi, y_train) print(最优参数:, grid.best_params_) print(最优 AUC:, grid.best_score_)max_featuressqrt和log2是随机森林特有的随机性来源设置后每棵树只随机选取一部分特征做分裂能降低树之间的相关性。n_jobs-1表示使用所有 CPU 核心如果你的机器内存有限建议改成n_jobs2或去掉否则并行训练多个树模型时内存会吃紧。5. ROC、校准曲线与特征贡献用可视化验证模型能否落地5.1 用 ROC 与校准曲线检验模型真实水平模型训练完测试集上算出 AUC 之后还需要看两条曲线ROC 曲线和校准曲线。ROC 曲线能看不同阈值下的真阳性率和假阳性率校准曲线则能告诉你模型预测的概率是否可信——如果预测 0.8 的患者实际死亡率只有 0.6那这个概率不能直接用于临床沟通。import matplotlib.pyplot as plt from sklearn.metrics import roc_curve, auc, calibration_curve y_prob grid.best_estimator_.predict_proba(X_test_chi)[:, 1] fpr, tpr, _ roc_curve(y_test, y_prob) roc_auc auc(fpr, tpr) fig, axes plt.subplots(1, 2, figsize(12, 5)) axes[0].plot(fpr, tpr, labelfRandom Forest (AUC {roc_auc:.3f})) axes[0].plot([0, 1], [0, 1], linestyle--, colorgray) axes[0].set_xlabel(False Positive Rate) axes[0].set_ylabel(True Positive Rate) axes[0].set_title(ROC Curve) axes[0].legend() prob_true, prob_pred calibration_curve(y_test, y_prob, n_bins5) axes[1].plot(prob_pred, prob_true, markero) axes[1].plot([0, 1], [0, 1], linestyle--, colorgray) axes[1].set_xlabel(Predicted Probability) axes[1].set_ylabel(Actual Probability) axes[1].set_title(Calibration Curve) plt.tight_layout() plt.show()校准曲线的n_bins5是把预测概率分成 5 个区间然后在每个区间内计算实际正样本比例。如果校准曲线在对角线下方说明模型高估了风险需要用CalibratedClassifierCV做概率校正通常选methodisotonic或sigmoid。methodisotonic适合样本量较大的情况sigmoid适合样本量小且曲线呈 S 形的情况。5.2 用 SHAP 验证特征方向是否符合临床直觉最后一步是检查模型学到的规律是否站得住脚。用shap.TreeExplainer对测试集做特征归因如果结果显示ejection_fraction高时死亡风险反而上升那大概率是数据或建模流程出了偏差——正常情况下射血分数越低心衰越重死亡风险越高。import shap explainer shap.TreeExplainer(grid.best_estimator_) shap_values explainer.shap_values(X_test_chi) shap.summary_plot(shap_values, X_test_chi, feature_namesX_test.columns.tolist())summary_plot把每个样本的特征贡献压缩成散点图点越靠右表示该特征把预测推向高风险方向。如果某个特征的方向和医学常识相悖回到特征重要性列表检查是不是存在共线性或数据泄漏。确认方向和逻辑无误后用joblib.dump(grid.best_estimator_, heart_model.pkl)保存模型再把特征选择器和标准化器封装成同一个 Pipeline 一起导出下次新数据进来直接pipeline.predict_proba()出结果不需要重新写一遍预处理流程。本文还有配套的精品资源点击获取
返回列表