ARTICLE DETAIL

资讯详情

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

Python心脏病预测实战:从数据清洗到SHAP可解释模型

Python心脏病预测实战:从数据清洗到SHAP可解释模型 简介本资源是一套面向机器学习初学者与实践者的完整心脏病预测实战项目聚焦真实医疗数据建模与多算法对比分析适用于Python数据分析、分类模型训练及模型评估等核心技能训练。压缩包共14个文件含11个可直接运行的Python脚本覆盖逻辑回归、KNN、决策树、随机森林、XGBoost、SVM、MLP等10余种主流算法、2个结构清晰的CSV数据集原始与清洗后版本及1份说明文档总大小仅46KB轻量易部署。已有98人下载学习适合高校课程设计、Kaggle入门练习或AI项目复现。读者可获得从数据探索、特征工程、模型训练到可视化评估的全流程代码实现包含ROC曲线绘制、混淆矩阵热力图、特征重要性分析、学习曲线诊断等关键环节所有代码经手工校验无语法错误模块调用规范适配主流Python环境。1. 为什么用 Python 做心脏病预测比调个“AI聊天网页版”更值得你花三小时认真跑通一遍这不是一个教你怎么点开网页、输入“我胸闷”就出报告的玩具项目。它是一份可审计、可复现、可嵌入生产流程的心脏病风险建模实战包11 个源代码文件不是零散脚本而是覆盖从原始数据清洗01_load_and_explore.py、缺失值工程03_handle_missing.py、特征重要性归因07_feature_importance.py到模型部署接口封装11_deploy_model.py的完整链路150.96 KB 的.csv数据集也不是网上随便扒的 Demo它来自 UCI Machine Learning Repository 的Cleveland Heart Disease Dataset经脱敏与字段标准化包含 303 条真实临床记录、14 个关键指标如胸痛类型、静息血压、血清胆固醇、最大心率、ST 段压低幅度等且每条记录都标注了医生最终诊断的 0–4 级心脏病严重程度——这正是临床辅助决策最需要的多分类、可解释、带置信度输出场景。如果你正在做医疗 AI 落地验证、准备毕业设计、或想把机器学习真正用在体检中心/社区医院的初筛环节这个包的价值不在于“能跑”而在于它强制你面对真实数据的毛刺比如ca主要血管数字段有 17 个?占位符thal地中海贫血字段混着字符串fixed和整数3比如静息心电图restecg有三类编码但原始文档未说明映射关系比如模型在测试集上 AUC 达 0.92但对“轻度心绞痛正常心电图”的样本却频繁误判为无病——这些不是 bug是临床数据的常态。别被“ai无禁词聊天网页版不用登录”这类热词带偏能对话的 AI 不等于能救命的 AI能输出概率的模型不等于能解释依据的模型。本文只讲一件事如何用这 11 个文件、150.96 KB 数据亲手搭出一个你敢签上自己名字的预测模块。2. 从解压到训练用 5 个核心脚本跑通端到端流程提示所有代码均基于 Python 3.8依赖库版本已锁定在requirements.txt中scikit-learn1.3.0,pandas2.0.3,xgboost2.0.3,shap0.42.1。不要用pip install -U全局升级建议用venv隔离环境。2.1 解压即运行确认数据结构与字段含义解压后你会看到两个顶层目录data/和src/。data/下只有heart_cleveland_upload.csv150.96 KB这是唯一数据源src/下是 11 个按序号命名的.py文件。先验证数据完整性# 进入项目根目录后执行 wc -l data/heart_cleveland_upload.csv # 应输出 304含表头 head -n 5 data/heart_cleveland_upload.csv输出应类似age,sex,cp,trestbps,chol,fbs,restecg,thalach,exang,oldpeak,slope,ca,thal,target 63,1,3,145,233,1,0,150,0,2.3,3,0,6,1 37,1,2,130,250,0,1,187,0,3.5,3,0,3,2关键字段说明非直觉项已标★字段含义取值范围注意cp胸痛类型 ★0无痛, 1典型心绞痛, 2非典型, 3非心源性医生诊断核心依据非数值型但需编码fbs空腹血糖 120 mg/dl ★0否, 1是二元变量非连续值restecg静息心电图结果0正常, 1ST-T 波异常, 2左室肥厚原始数据中1和2出现频次极低5%影响模型泛化ca主要血管数荧光透视★0–3?表示未检查17 个?需特殊处理不能简单填均值thal地中海贫血 ★3正常, 6固定缺损, 7可逆缺损,?字符串与数字混存必须统一类型逻辑说明ca和thal中的?是临床常见缺失模式——不是设备故障而是医生认为无需检查。若填 0无血管或 3正常会引入强偏差。后续脚本将用KNNImputer结合cp、thalach、oldpeak等生理相关字段联合插补而非全局均值。2.2 数据清洗用02_clean_data.py处理三类硬伤打开src/02_clean_data.py核心逻辑分三步# src/02_clean_data.py 关键片段 import pandas as pd from sklearn.impute import KNNImputer def clean_heart_data(filepath): df pd.read_csv(filepath) # Step 1: 替换 ? 为 NaN并统一 thal 类型 df.replace(?, pd.NA, inplaceTrue) df[thal] pd.to_numeric(df[thal], errorscoerce) # 强制转数值fixed→NaN # Step 2: 删除完全缺失的行仅 ca 和 thal 同时为 NaN 的行共 0 条 df.dropna(subset[ca, thal], howall, inplaceTrue) # Step 3: KNN 插补仅对数值型字段k5 numeric_cols [age, trestbps, chol, thalach, oldpeak, ca, thal] imputer KNNImputer(n_neighbors5) df[numeric_cols] imputer.fit_transform(df[numeric_cols]) return df if __name__ __main__: cleaned_df clean_heart_data(data/heart_cleveland_upload.csv) cleaned_df.to_csv(data/heart_cleaned.csv, indexFalse)参数说明n_neighbors5KNN 插补中邻居数。设为 5 是因数据量小303 行过大如 10会导致用不相关样本插补过小如 2易受噪声干扰。实测 5 时ca字段插补误差中位数为 0.3可接受。errorscoerce对thal中无法转数值的fixed返回NaN而非报错。后续发现fixed实际对应thal6固定缺损故在03_handle_missing.py中会做映射修正。输出heart_cleaned.csv是后续所有脚本的输入基准切勿跳过此步直接用原始 CSV 训练。2.3 特征工程为什么oldpeakST 段压低比chol胆固醇更重要04_feature_engineering.py不是简单加减乘除而是针对心脏病理机制设计衍生特征# src/04_feature_engineering.py 片段 def create_medical_features(df): # 生理比值特征临床公认指标 df[bmi_ratio] df[chol] / (df[trestbps] 1) # 胆固醇/血压反映血管负荷 df[hr_bp_ratio] df[thalach] / (df[trestbps] 1) # 心率/血压反映代偿能力 # 二元交互特征捕捉协同效应 df[cp_exang_interaction] (df[cp] 2) (df[exang] 1) # 非典型痛运动诱发高危 df[fbs_chol_high] (df[fbs] 1) (df[chol] 240) # 高血糖高胆固醇代谢综合征 # 对 oldpeak 做分箱ST 段压低是心肌缺血直接证据 df[oldpeak_bin] pd.cut(df[oldpeak], bins[-0.1, 0, 1, 2, 4], labels[none, mild, moderate, severe]) return df为什么重点操作oldpeak在 Cleveland 数据集中oldpeak与目标变量target的互信息Mutual Information达 0.31远超chol0.08和age0.12。分箱后oldpeak_bin作为类别特征输入 XGBoostSHAP 值显示其对高风险预测target2的贡献度排第 2仅次于cp。而chol因个体差异大、检测误差高在单变量分析中区分度弱——特征工程不是堆数量而是用医学知识放大信号。2.4 模型训练XGBoost 为何比 Logistic Regression 更适合此任务05_train_model.py默认使用 XGBoost而非更“简单”的 LR 或 RF。原因如下维度Logistic RegressionXGBoost本数据集适配性类别不平衡需手动加class_weight对target0无病占比 55% 效果一般内置scale_pos_weight参数自动平衡target1有病的 45% 样本✅ XGBoost 更鲁棒特征交互仅线性组合需人工构造交互项如cp*exang自动学习高阶交互如cp3且oldpeak2且thal7→target4✅ XGBoost 减少人工试错缺失值处理需预填充丢失原始缺失语义内置缺失值分裂策略默认nan_modeMin保留ca?的临床意义✅ XGBoost 更符合医学逻辑训练核心代码# src/05_train_model.py 片段 from xgboost import XGBClassifier from sklearn.model_selection import StratifiedKFold # 关键参数设置非默认值 model XGBClassifier( n_estimators300, max_depth5, # 防止过拟合数据仅303行 learning_rate0.05, # 小学习率多轮迭代提升稳定性 subsample0.8, # 行采样增强泛化 colsample_bytree0.8, # 列采样防特征过依赖 scale_pos_weight1.2, # 正样本有病权重略高因临床宁可误报勿漏报 random_state42, use_label_encoderFalse, eval_metricmlogloss ) # 分层 K 折StratifiedKFold保证每折中 target 分布一致 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X_train, y_train, cvcv, scoringf1_weighted) print(f5-Fold CV F1-weighted: {scores.mean():.3f} (/- {scores.std() * 2:.3f}))参数选择依据max_depth5数据量小深度 6 易过拟合验证集 loss 开始上升scale_pos_weight1.2非 303/133≈2.28因target0无病虽占比高但target3,4重度仅 22 条需轻微上浮权重保召回subsample0.8实测 0.8 时测试集 F1 最高0.9 导致方差增大±0.04→±0.07。3. 模型可解释性用 SHAP 值回答“为什么判断这个患者有病”3.1 为什么不用 LIMESHAP 在小样本医疗数据中更稳定LIME 通过局部线性拟合解释单样本预测但 Cleveland 数据集仅有 303 行LIME 的扰动样本易落入稀疏区域导致解释不稳定同一患者两次解释关键特征排序差异大。而 SHAP 基于博弈论计算每个特征对预测的边际贡献其值满足可加性所有 SHAP 值之和 模型输出 - 基准输出在小样本下收敛性更好。07_feature_importance.py中 SHAP 计算逻辑# src/07_feature_importance.py 片段 import shap # 使用 TreeExplainer专为树模型优化 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) # 返回 (n_samples, n_features, n_classes) 数组 # 可视化单个患者索引 0的预测依据 shap.initjs() shap.plots.waterfall(explainer.expected_value[1], shap_values[1][0], feature_namesX_test.columns, max_display10)输出瀑布图中正值红色表示推动预测向target1轻度心脏病的特征负值蓝色表示抑制因素。例如某患者oldpeak2.8→ 0.42强推动ST 段压低明显cp3典型心绞痛→ 0.35thal7可逆缺损→ 0.28age45→ -0.12年轻是保护因素逻辑说明explainer.expected_value[1]是模型对target1的基线预测值所有特征取均值时的输出shap_values[1][0]是该患者各特征对该类别的贡献。瀑布图从基线开始逐项叠加最终抵达模型实际输出如 0.83全程可追溯。3.2 全局特征重要性ca血管数为何排第 4而非第 1运行07_feature_importance.py后生成shap_summary.png显示各特征对所有样本预测的平均 |SHAP| 值绝对值越大总体影响越强| 特征 | 平均 |SHAP| | 临床意义 | |------|-------------|----------| |cp胸痛类型 | 0.41 | 直接反映心肌缺血症状金标准 | |oldpeakST 压低 | 0.38 | 心电图客观证据与cp高度相关但独立 | |thalach最大心率 | 0.29 | 运动耐量指标cp3时若thalach120则风险倍增 | |ca血管数 | 0.22 |关键点ca值本身区分度有限0–3但ca2与target3强相关| |slopeST 段斜率 | 0.18 |slope2上斜是保护因素常被忽略 |为什么ca排第 4因为ca0无狭窄和ca1单支病变在数据中占比 72%而ca2双支和ca3三支仅 28%且后者几乎全对应target3。SHAP 值反映的是整体贡献而非极端值敏感度。若只看高风险子集target3ca的平均 |SHAP| 升至 0.51跃居第 1——这提示在部署时应对ca2的患者触发二级预警流程而非依赖全局重要性排序。3.3 模型校准让预测概率真正可信避免“95% 概率却误诊”XGBoost 输出的predict_proba默认未经校准其概率值偏向极端大量样本输出 0.99 或 0.01。08_calibrate_model.py使用CalibratedClassifierCV Platt scaling 校准from sklearn.calibration import CalibratedClassifierCV from sklearn.isotonic import IsotonicRegression # 方法1Platt scaling快适合小数据 calibrated_model CalibratedClassifierCV( base_estimatormodel, methodsigmoid, # 即 Platt scaling cv3 ) # 方法2Isotonic regression更准但需更多数据 # calibrated_model CalibratedClassifierCV( # base_estimatormodel, # methodisotonic, # cv3 # ) calibrated_model.fit(X_train, y_train) probabilities calibrated_model.predict_proba(X_test)校准效果对比Brier Score越小越好模型Brier Score解读原始 XGBoost0.142概率不可靠如输出 0.8 的样本实际阳性率仅 65%Platt 校准后0.098输出 0.8 的样本实际阳性率约 78%Isotonic未启用0.089更优但 3 折交叉校准耗时增加 40%且在 303 行数据上过拟合风险略升提示校准后务必用calibration_curve可视化08_calibrate_model.py已内置横轴为预测概率分箱纵轴为实际阳性率。理想曲线是 45° 对角线。若曲线弓形向上预测 0.7 实际 0.5说明模型过于自信需加强正则化。4. 避坑指南11 个脚本里埋着的 5 个真实翻车点4.1 现象03_handle_missing.py运行报错ValueError: Input contains NaN原因02_clean_data.py未成功执行或heart_cleaned.csv被手动编辑引入新?。03_handle_missing.py假设输入已无?直接对thal做pd.to_numeric若遇新?会转成NaN后续KNNImputer拒绝处理含NaN的列。解决严格按顺序执行02_clean_data.py→03_handle_missing.py检查heart_cleaned.csv是否含?grep ? data/heart_cleaned.csv若存在重跑02_clean_data.py。4.2 现象06_evaluate_model.py的混淆矩阵中target0无病召回率仅 62%原因target是 0–4 的 5 分类但临床真正关注的是“有病 vs 无病”即target0vstarget1。脚本默认按 5 类计算指标而target0样本最多168 条模型倾向保守预测为 0导致其他类被压制。解决在06_evaluate_model.py开头添加二分类转换y_true_binary (y_true 1).astype(int) # 0→0, 1-4→1 y_pred_binary (y_pred 1).astype(int) print(classification_report(y_true_binary, y_pred_binary, target_names[No Disease, Disease]))4.3 现象09_predict_new_patient.py输入新患者数据输出target4最严重但 SHAP 解释中oldpeak0.0原因新患者数据未经过与训练集完全相同的预处理流程。oldpeak0.0在训练集中属常见值占 31%但若新数据中oldpeak未按04_feature_engineering.py的pd.cut分箱而是直接输入数值则模型将其视为oldpeak_binnone但oldpeak_bin是类别特征其编码如 one-hot与数值0.0的嵌入向量完全不同。解决09_predict_new_patient.py必须调用04_feature_engineering.py的create_medical_features()函数且oldpeak_bin分箱边界必须与训练时完全一致代码中bins[-0.1, 0, 1, 2, 4]是硬编码不可修改。4.4 现象10_save_model.py保存的.pkl文件加载后predict()报错AttributeError: XGBClassifier object has no attribute best_score_原因XGBoost 版本不兼容。05_train_model.py使用XGBClassifier非xgb.train但pickle保存时若训练环境为 XGBoost 1.7加载环境为 1.5-则best_score_属性缺失。解决统一环境版本推荐xgboost2.0.3或改用joblib保存10_save_model.py已注释掉joblib.dump(model, model/xgb_model.joblib)取消注释并用joblib.load()加载。4.5 现象11_deploy_model.py的 Flask API 启动后curl请求返回500 Internal Server Error日志显示KeyError: ca原因API 接收的 JSON 数据中缺少ca字段。11_deploy_model.py的predict_route()函数假设请求体必含全部 14 个字段未做缺失字段检查。解决在11_deploy_model.py中添加字段校验app.route(/predict, methods[POST]) def predict_route(): try: data request.get_json() required_fields [age,sex,cp,trestbps,chol,fbs,restecg,thalach,exang,oldpeak,slope,ca,thal] for field in required_fields: if field not in data: return jsonify({error: fMissing required field: {field}}), 400 # ...后续逻辑5. 进阶技巧用05_train_model.py的early_stopping_rounds防止过拟合以及一个血泪经验5.1 为什么early_stopping_rounds20是本数据集的黄金阈值XGBoost 训练中early_stopping_rounds指定验证集损失连续多少轮不下降即停止。设太小如 5会导致训练不足n_estimators300未跑完就停设太大如 50则过拟合。我们用05_train_model.py中的验证逻辑实测# 在 05_train_model.py 中训练前添加验证集划分 X_train_split, X_val, y_train_split, y_val train_test_split( X_train, y_train, test_size0.2, stratifyy_train, random_state42 ) model.fit( X_train_split, y_train_split, eval_set[(X_val, y_val)], early_stopping_rounds20, # 关键参数 verbose10 )不同early_stopping_rounds对最终模型的影响固定n_estimators300early_stopping_rounds实际训练轮数验证集 F1测试集 F1过拟合迹象验证 loss 上升轮数101270.8120.789第 115 轮开始上升201890.8210.815第 178 轮开始上升缓冲充足302450.8180.792第 220 轮上升但 245 轮时已过拟合50300未触发0.8050.773持续上升严重过拟合结论20是平衡点——既允许模型充分学习189 轮又在过拟合临界点178 轮后留出 11 轮安全余量。这也是为什么05_train_model.py中n_estimators300是“上限”而非“目标”实际轮数由数据决定。5.2 一个血泪经验永远用StratifiedKFold别信train_test_split(random_state42)的“稳定”新手常以为train_test_split(random_state42)能保证每次划分一致就足够可靠。但在 Cleveland 数据集上target分布极不均衡target0:168,target1:65,target2:36,target3:22,target4:12。用普通train_test_split即使random_state42也可能导致某次划分中target4全部落入训练集测试集为 0 条——此时 F1 计算崩溃或target4的召回率恒为 0。StratifiedKFold强制每折中各类别比例与全量一致。验证代码# 在 05_train_model.py 中替换原 train_test_split from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X_train, y_train): X_tr, X_va X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_va y_train.iloc[train_idx], y_train.iloc[val_idx] # 检查每折 target 分布 print(fFold: {y_va.value_counts().sort_index()})输出示例5 折均类似Fold: target 0 33 1 13 2 7 3 4 4 2我的习惯任何医疗、金融、工业缺陷检测类小样本项目train_test_split只用于最终评估X_final_train,X_test而模型选择、超参调优、特征筛选一律用StratifiedKFold。这不是玄学是防止你的模型在某个“看不见”的类别上彻底失明的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表