简介本资源为基于机器学习的心血管疾病风险预测系统设计与实现项目资料面向医疗信息化学习者、人工智能入门者及需要疾病风险识别实战案例的开发者。内容围绕临床数据处理与智能算法融合展开涵盖数据采集整理、变量提取、逻辑回归与决策树集成建模、特征筛选、多维度评价指标及参数调优等完整流程并借助可视化图表呈现分析结果帮助理解人工智能在医疗场景中的落地路径。资源包共12个文件包含Python脚本、Jupyter Notebook、CSV数据集、Markdown说明、日志文件、动态链接库及Word文档等压缩包约4.63MB结构紧凑便于按模块查阅。目前已有90人学习下载适合希望掌握疾病风险预测全流程、对照代码复现实验并积累医疗AI项目经验的学习者参考。1. 心血管风险预测系统从体检表格到可解释概率的落地路径体检报告上那一列低密度脂蛋白、收缩压、吸烟史单独看都认识合在一起到底意味着多大风险多数人没有概念。心血管疾病风险预测系统要做的就是把年龄、血压、血脂、血糖、吸烟、家族史这些常规字段喂给一个机器学习模型输出一个未来若干年内的发病概率并且给出这个概率是怎么来的。它解决的不是诊断问题而是筛查和分层问题把人群按风险高低排个序让高风险的人优先去做进一步检查。适合谁做有结构化体检数据、想做慢病管理或健康筛查的团队以及想拿一个完整项目练手机器学习全流程的工程师。这个方向数据门槛不高公开数据集成熟但真正落地时特征工程和阈值选择比模型选型更决定成败。2. 数据从哪来、特征怎么定心血管风险预测的地基2.1 公开数据集与字段选择做心血管风险预测绕不开几个经典公开数据集。最常见的是 UCI 的 Cleveland Heart Disease 数据集303 条样本、14 个字段字段含义清晰适合跑通流程。另一个是 Framingham Heart Study 的衍生数据集4000 多条样本字段更贴近长期随访场景包含性别、年龄、吸烟、降压药、糖尿病等。选哪个取决于你的目标想快速验证建模流程用 Cleveland想模拟真实筛查场景用 Framingham 衍生集。字段选择上我一般会保留这几类人口学年龄、性别、体征收缩压、舒张压、BMI、生化指标总胆固醇、高密度脂蛋白、血糖、行为吸烟、饮酒、运动、病史糖尿病、高血压、家族史。字段不是越多越好缺失率高、定义模糊的字段要果断砍掉。比如 Cleveland 里的ca荧光透视下主要血管数和thal地中海贫血在普通体检场景拿不到做通用筛查系统时就不该依赖它们。提示字段的可得性比字段的预测力更重要。一个 AUC 高但需要做冠脉造影才能拿到的特征在筛查场景里没有意义。2.2 缺失值、异常值与类别不平衡的处理真实体检数据几乎没有干净的。缺失值处理分两步先看缺失比例超过 40% 的字段直接删剩下的按类型处理连续字段用中位数填充类别字段用众数填充同时加一列缺失指示变量让模型知道这个值原本是缺的。异常值不要急着删先判断是录入错误还是真实极端值。收缩压 300 这种基本是录入错误按分位数截断而收缩压 180 可能是真实的高血压患者删了反而丢信息。类别不平衡是心血管预测的常态阳性样本往往只占百分之十几。处理方式有三种调整类别权重、过采样少数类、调整决策阈值。我一般优先用class_weightbalanced因为它不改变数据分布只是让模型在损失函数里更重视少数类。过采样如 SMOTE在样本量小的时候容易过拟合要谨慎。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler # 读取数据假设最后一列是标签 df pd.read_csv(heart_disease.csv) X df.drop(columns[target]) y df[target] # 区分数值列和类别列 num_cols X.select_dtypes(include[np.number]).columns.tolist() cat_cols X.select_dtypes(exclude[np.number]).columns.tolist() # 数值列中位数填充 缺失指示 num_imputer SimpleImputer(strategymedian, add_indicatorTrue) X_num pd.DataFrame(num_imputer.fit_transform(X[num_cols]), columnsnum_cols [f{c}_missing for c in num_cols if f{c}_missing in num_imputer.get_feature_names_out()]) # 类别列众数填充 cat_imputer SimpleImputer(strategymost_frequent) X_cat pd.DataFrame(cat_imputer.fit_transform(X[cat_cols]), columnscat_cols) # 合并并做标准化 X_processed pd.concat([X_num, X_cat], axis1) scaler StandardScaler() X_scaled scaler.fit_transform(X_processed) # 分层切分保证训练集和测试集阳性比例一致 X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, stratifyy, random_state42 )这段代码的关键点在三个地方。add_indicatorTrue会为每个有缺失的数值列生成一个 0/1 指示列模型能区分“真实的中位数”和“填充出来的中位数”。stratifyy保证切分后训练集和测试集的阳性比例与原始数据一致否则小样本下测试集可能一个阳性都没有。StandardScaler对逻辑回归和 SVM 是必须的对树模型不是必须但也没坏处统一处理省心。参数上test_size在样本量小于 1000 时建议用 0.2 到 0.3样本量大时可以降到 0.1。random_state固定住方便复现。如果做交叉验证用StratifiedKFold而不是普通KFold。3. 模型选型与训练逻辑回归为什么常常打赢复杂模型3.1 基线模型与树模型的取舍心血管风险预测这个任务逻辑回归是绕不过去的基线。原因不是它准而是它可解释每个特征的系数直接对应优势比医生和业务方看得懂。Framingham 风险评分本身就是逻辑回归的产物几十年验证下来依然在用。所以我的做法是先用逻辑回归跑一个基线记录 AUC 和召回率再上树模型对比。树模型里XGBoost 和 LightGBM 在表格数据上通常表现最好。它们能自动处理特征交互对缺失值也有一定容忍度。但要注意树模型在样本量只有几百条时容易过拟合max_depth和min_child_weight要调小。随机森林比单棵树稳但解释性差一些可以用 SHAP 值来补。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score, recall_score, precision_score models { LogisticRegression: LogisticRegression( class_weightbalanced, max_iter1000, C1.0 ), RandomForest: RandomForestClassifier( n_estimators300, max_depth6, min_samples_leaf5, class_weightbalanced, random_state42 ), XGBoost: XGBClassifier( n_estimators300, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight3, eval_metriclogloss, random_state42 ), } for name, model in models.items(): model.fit(X_train, y_train) proba model.predict_proba(X_test)[:, 1] pred (proba 0.5).astype(int) print(f{name}: AUC{roc_auc_score(y_test, proba):.3f}, fRecall{recall_score(y_test, pred):.3f}, fPrecision{precision_score(y_test, pred):.3f})逻辑回归的C是正则化强度的倒数C越小正则化越强小样本时建议从 0.1 到 1.0 之间试。随机森林的max_depth6和min_samples_leaf5是防止过拟合的关键样本量几百条时不要设太深。XGBoost 的scale_pos_weight用来处理不平衡一般设成负样本数除以正样本数这里假设比例约 3:1。learning_rate0.05配合n_estimators300是比较稳的组合学习率再大容易震荡。3.2 交叉验证与阈值选择单次切分的评估结果波动很大尤其是小样本。我一般用 5 折分层交叉验证看 AUC 的均值和标准差。如果标准差超过 0.05说明模型不稳定要么样本太少要么特征噪声太大。阈值选择是心血管预测里最容易被忽视的一步。默认 0.5 的阈值在筛查场景里往往不合适因为漏诊的代价远高于误诊。实际做法是画 ROC 曲线找到约登指数敏感度 特异度 - 1最大的点或者根据业务要求固定召回率比如要求召回不低于 0.85再反推阈值。from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.metrics import roc_curve # 5 折分层交叉验证 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) for name, model in models.items(): scores cross_val_score(model, X_scaled, y, cvcv, scoringroc_auc) print(f{name}: AUC{scores.mean():.3f} ± {scores.std():.3f}) # 用约登指数选阈值 best_model models[LogisticRegression] best_model.fit(X_train, y_train) proba best_model.predict_proba(X_test)[:, 1] fpr, tpr, thresholds roc_curve(y_test, proba) youden tpr - fpr best_threshold thresholds[youden.argmax()] print(f最佳阈值: {best_threshold:.3f})交叉验证的scoring用roc_auc而不是准确率因为不平衡数据下准确率没有参考价值。约登指数选出的阈值通常低于 0.5意味着模型会更倾向于判为高风险这正是筛查场景想要的。如果业务方要求召回不低于某个值就把youden换成tpr找tpr 目标值的最小阈值。4. 系统落地从模型文件到可调用的预测接口4.1 模型持久化与特征对齐模型训练完只是第一步要变成系统得把模型和预处理流程一起存下来。只存模型不存 scaler 是新手最常翻的车线上推理时特征没做同样的标准化预测结果全乱。正确做法是用Pipeline把填充、标准化、模型串起来整体持久化。from sklearn.pipeline import Pipeline import joblib # 把预处理和模型串成 Pipeline pipe Pipeline([ (imputer, SimpleImputer(strategymedian, add_indicatorTrue)), (scaler, StandardScaler()), (clf, LogisticRegression(class_weightbalanced, max_iter1000, C1.0)), ]) pipe.fit(X_train_raw, y_train) # X_train_raw 是未处理的原始特征 joblib.dump(pipe, cardio_risk_pipeline.pkl) # 线上加载并预测 loaded joblib.load(cardio_risk_pipeline.pkl) single pd.DataFrame([{ age: 58, sex: 1, trestbps: 145, chol: 260, thalach: 130, oldpeak: 2.1, smoke: 1, diabetes: 0 }]) risk loaded.predict_proba(single)[0, 1] print(f风险概率: {risk:.3f})Pipeline 的好处是预处理和模型绑定线上不会漏步骤。joblib比pickle更适合存 numpy 数组速度快。注意X_train_raw必须是原始未处理的特征Pipeline 内部会自己做填充和标准化。线上传入的单条数据字段顺序和名称要和训练时一致否则会静默出错。4.2 接口封装与输入校验系统对外一般暴露一个 HTTP 接口接收 JSON返回风险概率和风险等级。用 FastAPI 写最省事自带参数校验和文档。关键点是输入校验年龄不能是负数血压不能超过合理范围缺失字段要有默认处理策略。from fastapi import FastAPI from pydantic import BaseModel, Field import joblib import pandas as pd app FastAPI() model joblib.load(cardio_risk_pipeline.pkl) class PatientFeatures(BaseModel): age: int Field(..., ge18, le120) sex: int Field(..., ge0, le1) trestbps: float Field(..., ge80, le250) chol: float Field(..., ge100, le600) thalach: float Field(..., ge60, le220) oldpeak: float Field(0.0, ge0, le10) smoke: int Field(0, ge0, le1) diabetes: int Field(0, ge0, le1) app.post(/predict) def predict(features: PatientFeatures): df pd.DataFrame([features.dict()]) proba model.predict_proba(df)[0, 1] level 高风险 if proba 0.35 else (中风险 if proba 0.15 else 低风险) return {risk_probability: round(float(proba), 4), risk_level: level}Field的ge和le是硬性边界超出直接返回 422避免脏数据进模型。风险等级的分界值 0.35 和 0.15 不是拍脑袋是根据约登指数阈值和业务分层需求定的实际项目里要和业务方一起确认。返回概率保留四位小数够用且不误导精度。5. 避坑与排查心血管风险预测里最容易翻车的五件事现象一离线 AUC 0.85上线后预测全是低风险。原因通常是线上特征没做和训练一致的标准化或者字段顺序错位。解决用 Pipeline 把预处理和模型绑定线上传入原始特征加一层字段名和顺序的断言校验。现象二交叉验证 AUC 波动超过 0.1。原因多半是样本量太小或者某个特征在部分折里分布差异大。解决检查样本量低于 500 条时考虑用留一法或增加数据对分布不稳的特征做分箱处理减少极端值影响。现象三模型把“是否服用降压药”当成最强特征。这是典型的数据泄漏。服用降压药的人本身就是高血压患者这个特征间接编码了标签。解决排查所有特征凡是“因为得病才产生”的字段都要剔除或谨慎处理比如降压药、降糖药、既往手术史。现象四召回率很高但精确率极低业务方不买账。原因是阈值设得太低把大量正常人判成高风险。解决和业务方确认可接受的误诊率用精确率-召回率曲线找平衡点而不是只看 ROC。现象五模型更新后旧接口报错。原因是新模型的特征集变了但接口没同步。解决模型文件里存一份特征清单接口启动时校验传入字段是否匹配不匹配直接拒绝并告警。注意心血管预测系统的输出是筛查参考不是诊断结论。接口返回里要明确标注这一点避免被当成医疗建议使用。6. 让模型可信SHAP 解释与风险分层的进阶做法模型给出一个概率业务方和用户都会问“为什么”。逻辑回归可以用系数解释但树模型和集成模型需要 SHAP 值。SHAP 的核心思想是把预测值分解到每个特征上告诉你这个特征把风险推高了多少。对单条样本可以画出瀑布图直观看到年龄贡献了多少、血压贡献了多少。import shap # 用树模型做解释 explainer shap.TreeExplainer(models[XGBoost]) shap_values explainer.shap_values(X_test) # 单条样本的特征贡献 shap.waterfall_plot( shap.Explanation( valuesshap_values[0], base_valuesexplainer.expected_value, dataX_test[0], feature_namesX_processed.columns.tolist() ) )SHAP 值不仅能解释单条预测还能做全局特征重要性排序。如果发现某个特征重要性异常高要回头检查是不是数据泄漏。我一般会把 SHAP 排序和逻辑回归系数排序对比两者差异大的特征重点排查。风险分层的进阶做法是放弃二分类改用生存分析。心血管疾病的风险本质是“未来若干年内发病的概率”Cox 比例风险模型或随机生存森林能直接输出时间相关的风险曲线比二分类更贴近临床需求。如果数据里有随访时间字段值得往这个方向走一步。最后说个我自己的习惯每次模型上线前我会手动构造几条边界样本——极低风险、极高风险、缺失值最多的那条——跑一遍预测看输出是否合理。这个动作花不了五分钟但帮我拦下过好几次特征错位和阈值设反的问题。模型可以复杂验证要简单。希望帮到你。本文还有配套的精品资源点击获取