心血管预测模型Python源码拆解:从数据到推理的完整方案
简介这份Python源码集锦聚焦心血管疾病预测模型的完整实现面向具备一定Python基础、希望进入医疗数据分析或人工智能领域的学习者与参赛者。资源包共3个文件包含1个ipynb交互式笔记本、1个py脚本和1个csv数据集压缩包约83KB体量轻巧却覆盖从数据读取到模型落地的关键环节。内容围绕数学建模与机器学习展开涉及数据清洗、缺失值与异常值处理、相关性分析和主成分分析等特征选择方法并演示逻辑回归、随机森林、支持向量机等算法的训练流程。读者可借助交叉验证、ROC曲线、AUC与召回率等指标评估模型表现再通过网格搜索或随机搜索完成超参数调优同时利用Matplotlib、Seaborn绘制学习曲线与特征重要性图理解模型行为。代码还包含pandas数据读取、pickle与csv存储、joblib模型保存加载等基础操作。已有1129人学习适合作为医疗健康预测方向的实践案例帮助读者在真实数据上打通特征工程、建模、评估与可视化的完整链路。1. 心血管预测模型源码拆解从数据到推理一套能跑通的 Python 方案长什么样拿到一份「心血管预测模型数据代码」的 Python 源码集锦很多人第一反应是打开文件夹看有多少个.py然后随便挑一个train.py就python train.py结果不是报路径错误就是指标低得离谱。心血管预测这个场景在机器学习里属于典型的表格二分类任务特征维度不高通常十几个临床指标样本量从几百到几千不等但它的坑集中在数据泄漏、类别不平衡和阈值选择这三件事上。这套源码真正值得看的不是模型有多深而是它怎么把一份带缺失值的临床表格一步步变成能给出风险概率的推理接口。适合已经会python安装numpy库的方法、能看懂sklearn基础 API 的读者也适合做机器学习期末复习想找一个完整案例的学生。下面我按「数据怎么进、模型怎么选、结果怎么验」的顺序把这类源码的骨架拆开讲。2. 数据管道与特征工程心血管表格进模型前必须过的四道关2.1 先看清字段类型别把数值列当类别列读心血管预测数据集常见字段包括年龄、性别、收缩压、舒张压、胆固醇、血糖、吸烟、胸痛类型等。这些字段里性别、胸痛类型、心电图结果是类别型年龄、血压、胆固醇是数值型。源码里如果统一用pd.read_csv读进来不做dtype指定类别列会被推断成int64后面做标准化时会把「性别 0/1」也一起缩放模型会学到错误的距离关系。import pandas as pd import numpy as np # 显式声明类别列避免被当成连续值处理 cat_cols [sex, cp, fbs, restecg, exang, slope, thal] num_cols [age, trestbps, chol, thalach, oldpeak] df pd.read_csv(heart.csv) # 把类别列转成 category 类型后续 one-hot 或 target encoding 都基于它 for c in cat_cols: df[c] df[c].astype(category) # 检查缺失值分布心血管数据里 oldpeak 和 thal 经常有缺 print(df.isnull().sum().sort_values(ascendingFalse))这段代码的关键在astype(category)它让 pandas 在内存里用整数编码存储同时保留类别语义。参数上cat_cols必须根据实际列名调整不同来源的心血管数据集列名差异很大常见的有cp胸痛类型、thal地中海贫血、ca主要血管数。如果缺失值集中在oldpeak和thal不要直接dropna因为这两列对缺血判断贡献大源码里通常用中位数填数值列、用众数填类别列。2.2 缺失值填充与异常值截断的顺序不能反很多人先做异常值截断再填缺失这是反的。异常值检测算法比如 IQR在含缺失的列上计算分位数会偏正确顺序是先填缺失再截断。心血管数据里chol胆固醇偶尔出现 0 或 500 以上的极端值这些是录入错误不是真实生理值。from sklearn.impute import SimpleImputer # 数值列用中位数填充类别列用众数填充 num_imputer SimpleImputer(strategymedian) cat_imputer SimpleImputer(strategymost_frequent) df[num_cols] num_imputer.fit_transform(df[num_cols]) df[cat_cols] cat_imputer.fit_transform(df[cat_cols]) # 对胆固醇做生理范围截断低于 100 或高于 400 视为录入异常 df[chol] df[chol].clip(lower100, upper400) # 静息血压同理低于 80 或高于 200 截断 df[trestbps] df[trestbps].clip(lower80, upper200)SimpleImputer的fit_transform只能在训练集上 fit验证集和测试集必须用训练集的统计量 transform否则就是数据泄漏。源码里如果看到对全量数据直接fit_transform那这个模型的验证指标是虚高的。clip的上下界来自临床常识不是拍脑袋胆固醇低于 100 在成年人里极罕见高于 400 属于家族性高胆固醇血症的极端情况一般数据集里出现这种值大概率是录入错误。2.3 类别编码one-hot 和 target encoding 的取舍心血管数据的类别列基数都不高cp有 4 类restecg有 3 类thal有 3 类这种低基数场景直接用 one-hot 最稳。target encoding 在样本量小于 2000 时容易过拟合因为每个类别的均值估计方差大。源码里如果用了 target encoding要检查它有没有做 K 折平滑。from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 用 ColumnTransformer 把数值和类别处理串成一条管道 preprocessor ColumnTransformer( transformers[ (num, passthrough, num_cols), # 数值列已填充截断直接过 (cat, OneHotEncoder(dropfirst, sparse_outputFalse), cat_cols) ]) # dropfirst 去掉冗余的基准类避免共线性 # sparse_outputFalse 让输出是稠密数组方便后面接树模型dropfirst对线性模型逻辑回归很重要能避免虚拟变量陷阱对树模型无所谓但统一处理更省心。sparse_outputFalse在数据量不大时建议开因为稀疏矩阵喂给sklearn的树模型会报错或自动转稠密不如一开始就稠密。这条管道的好处是训练时fit在训练集上推理时transform新样本编码规则完全一致不会出现训练用 one-hot 推理用 label encoding 的翻车。2.4 标准化只对线性模型和神经网络必要随机森林和 XGBoost 这类树模型对特征尺度不敏感标准化不会提升效果反而增加推理时的计算。但逻辑回归、SVM、MLP 必须标准化否则血压100-200和 oldpeak0-6的量纲差异会让梯度下降很难收敛。from sklearn.preprocessing import StandardScaler # 只在需要时加标准化树模型管道里不要放 linear_pipeline Pipeline([ (prep, preprocessor), (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, class_weightbalanced)) ]) # 树模型管道直接接分类器 tree_pipeline Pipeline([ (prep, preprocessor), (clf, RandomForestClassifier(n_estimators300, class_weightbalanced)) ])class_weightbalanced是心血管预测里几乎必开的参数因为阳性样本患病通常少于阴性样本不开的话模型会偏向预测多数类召回率很低。max_iter1000是逻辑回归的常见调整默认 100 在标准化后的数据上有时不收敛。这两条管道可以共用同一个preprocessor保证特征处理一致只在下游分类器上做区分。3. 模型训练与评估为什么你的 AUC 高但临床没用3.1 交叉验证要分层否则折间方差大到没法看心血管数据阳性率可能只有 20%-30%普通 K 折会让某一折里阳性样本极少AUC 波动超过 0.1。StratifiedKFold保证每折的类别比例和全集一致这是表格二分类的标准做法。from sklearn.model_selection import StratifiedKFold, cross_val_score cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(tree_pipeline, X, y, cvcv, scoringroc_auc) print(fAUC: {scores.mean():.3f} ± {scores.std():.3f})random_state42是为了可复现shuffleTrue在数据按时间或 ID 排序时必须开否则每折的分布不一样。scoringroc_auc比 accuracy 更适合不平衡数据accuracy 在阳性率 20% 时全预测阴性也有 80% 准确率毫无意义。如果折间标准差超过 0.05说明样本量太小或特征区分度不够这时候加模型复杂度没用得回去看特征。3.2 阈值不是 0.5用约登指数找最佳切点模型输出的是概率临床决策需要二分类标签。默认 0.5 阈值在 unbalanced 数据上会让召回率很低。约登指数Youdens J等于 sensitivity specificity - 1最大化它的阈值就是最佳切点。from sklearn.metrics import roc_curve import numpy as np # 在验证集上拿预测概率 y_prob tree_pipeline.predict_proba(X_val)[:, 1] fpr, tpr, thresholds roc_curve(y_val, y_prob) # 约登指数最大处对应的阈值 j_scores tpr - fpr best_idx np.argmax(j_scores) best_threshold thresholds[best_idx] print(f最佳阈值: {best_threshold:.3f}, 敏感度: {tpr[best_idx]:.3f}, 特异度: {1-fpr[best_idx]:.3f})predict_proba返回两列第一列是阴性概率第二列是阳性概率取[:, 1]。roc_curve返回的thresholds是从高到低排列的argmax找到约登指数最大位置。这个阈值要在验证集上找然后固定下来用于测试集和推理不能每次推理都重新算。源码里如果直接用predict而不调阈值那它的召回率大概率不达标。3.3 校准曲线比 AUC 更能反映概率是否可信AUC 只关心排序不关心概率绝对值。如果模型说「风险 0.9」但实际阳性率只有 0.6这个概率就没法直接给医生用。校准曲线reliability diagram把预测概率分桶看每桶里的实际阳性率。from sklearn.calibration import calibration_curve import matplotlib.pyplot as plt prob_true, prob_pred calibration_curve(y_val, y_prob, n_bins10) plt.plot(prob_pred, prob_true, markero, label模型) plt.plot([0, 1], [0, 1], linestyle--, label理想校准) plt.xlabel(预测概率) plt.ylabel(实际阳性率) plt.legend() plt.show()如果曲线在对角线下方说明模型高估风险在上方则低估。树模型尤其是随机森林倾向于把概率推向 0 和 1 两端校准差可以用CalibratedClassifierCV做 Platt scaling 或 isotonic 校准。逻辑回归本身输出校准较好但加了class_weightbalanced后概率会被扭曲也需要校准。这一步在科研论文里经常被审稿人追问源码里有没有校准模块是判断它是否「能落地」的分水岭。3.4 特征重要性要看排列重要性不看默认的 impurity随机森林的feature_importances_基于不纯度减少对高基数类别列有偏好容易误导。排列重要性permutation importance在验证集上打乱某一列看 AUC 掉多少更可靠。from sklearn.inspection import permutation_importance result permutation_importance( tree_pipeline, X_val, y_val, n_repeats10, random_state42, scoringroc_auc) # 按重要性均值排序输出 for i in result.importances_mean.argsort()[::-1][:10]: print(f{X_val.columns[i]}: {result.importances_mean[i]:.4f})n_repeats10表示每列打乱 10 次取平均次数越多越稳但越慢。scoringroc_auc保证和评估指标一致。心血管场景里通常thal、cp、oldpeak、ca排在前列如果age排第一要警惕是不是数据泄漏比如年龄和标签有隐藏关联。排列重要性在管道上直接跑它会自动处理预处理不用手动拆。4. 推理接口与部署把 pickle 模型变成能调用的服务4.1 保存管道而不是只保存模型只保存model.pkl而忘了preprocessor推理时新数据的编码和训练时不一致结果全错。joblib保存整个Pipeline最省事。import joblib # 保存完整管道包含预处理和分类器 joblib.dump(tree_pipeline, heart_model_pipeline.pkl) # 推理时直接加载管道 loaded_pipeline joblib.load(heart_model_pipeline.pkl) # 新样本必须是 DataFrame列名和训练时一致 new_sample pd.DataFrame([{ age: 55, sex: 1, cp: 0, trestbps: 140, chol: 250, fbs: 0, restecg: 1, thalach: 150, exang: 0, oldpeak: 1.5, slope: 2, ca: 0, thal: 2 }]) prob loaded_pipeline.predict_proba(new_sample)[:, 1] print(f风险概率: {prob[0]:.3f})joblib比pickle更适合含 numpy 数组的对象序列化更快。新样本的列名必须和训练时完全一致顺序可以不同但名字要对ColumnTransformer按名字选列。如果训练时用了dropfirst的 one-hot推理时类别值必须在训练集出现过否则OneHotEncoder会报错或忽略取决于handle_unknown参数默认是error。生产环境建议设handle_unknownignore但要在训练时就想好。4.2 用 FastAPI 包一层 HTTP 接口源码集锦里如果带了app.py或serve.py大概率是用 Flask 或 FastAPI。FastAPI 自带数据校验和文档更适合这种结构化输入。from fastapi import FastAPI from pydantic import BaseModel import joblib import pandas as pd app FastAPI() model joblib.load(heart_model_pipeline.pkl) class PatientFeatures(BaseModel): age: int sex: int cp: int trestbps: int chol: int fbs: int restecg: int thalach: int exang: int oldpeak: float slope: int ca: int thal: int app.post(/predict) def predict(features: PatientFeatures): df pd.DataFrame([features.dict()]) prob model.predict_proba(df)[:, 1][0] return {risk_probability: round(float(prob), 4)}PatientFeatures用 pydantic 做类型校验字段类型不对会直接返回 422不用自己写 if-else。features.dict()把请求体转成字典再包成 DataFrame列名自动对齐。返回时round到 4 位小数避免浮点长尾。启动命令是uvicorn app:app --host 0.0.0.0 --port 8000--reload只在开发时加。这个接口没有做鉴权内网用可以公网必须加 token 或网关。4.3 批量推理用 pandas 的 apply 还是向量化如果一次要预测几千条逐条调predict_proba会很慢。直接把整个 DataFrame 喂给管道它内部会批量处理。# 批量推理df 的列和训练时一致 batch_df pd.read_csv(new_patients.csv) probs loaded_pipeline.predict_proba(batch_df)[:, 1] batch_df[risk_probability] probs batch_df.to_csv(predictions.csv, indexFalse)predict_proba接受二维数组或 DataFrame返回形状是(n_samples, 2)。批量推理时注意内存如果数据超过百万行要分块读chunksize。to_csv时indexFalse避免多出一列索引。这一步的坑在于new_patients.csv里如果有训练时没见过的类别值OneHotEncoder会报错所以生产环境要么设handle_unknownignore要么在入库前做类别值校验。5. 避坑与排查心血管预测源码里最容易翻车的五个地方5.1 现象交叉验证 AUC 0.95测试集掉到 0.7原因在划分训练测试之前做了缺失值填充或标准化验证集的信息泄漏到了训练集。解决所有fit操作只能在训练集上做用Pipeline把预处理和模型绑在一起cross_val_score会自动在每折内部 fit。5.2 现象模型把所有样本预测为阴性原因类别不平衡且没设class_weight或者阈值用了默认 0.5。解决分类器加class_weightbalanced用验证集约登指数找阈值不要用predict直接用predict_proba加自定义阈值。5.3 现象推理时报Feature names seen at fit time错误原因训练时用 DataFrame推理时传了 numpy 数组列名丢失。解决推理时也构造 DataFrame列名和顺序与训练一致或者训练时就用 numpy 并记录列顺序。5.4 现象OneHotEncoder遇到未知类别报错原因新数据的类别值不在训练集里默认handle_unknownerror。解决初始化时设handle_unknownignore同时监控未知类别的比例如果太高说明训练集覆盖不够。5.5 现象模型文件加载后预测结果和训练时不一致原因保存的是模型对象而不是管道预处理步骤丢失或者joblib版本不兼容。解决保存整个Pipeline加载后先用训练集的一条样本验证输出是否和训练时一致不一致就检查预处理是否被跳过。6. 把模型做成可复现的实验固定随机种子与版本记录心血管预测模型的源码最容易被忽视的是可复现性。同一个脚本今天跑 AUC 0.85明天跑 0.82审稿人或同事问起来没法解释。我一般会在脚本开头固定三处随机种子numpy、random、以及sklearn的random_state参数。但光固定种子不够还要记录库版本因为sklearn不同版本的RandomForestClassifier默认参数可能变。import numpy as np import random import sklearn import json # 固定全局种子 SEED 42 np.random.seed(SEED) random.seed(SEED) # 记录环境版本和模型一起保存 env_info { numpy: np.__version__, sklearn: sklearn.__version__, seed: SEED } with open(env_info.json, w) as f: json.dump(env_info, f, indent2)np.random.seed影响train_test_split的 shufflerandom.seed影响 Python 内置随机sklearn的random_state要显式传给每个有随机性的对象StratifiedKFold、RandomForestClassifier、permutation_importance。env_info.json和模型文件放一起换机器时先pip install对应版本再加载。这一步在机器学习期末复习里可能不考但在实际项目里是后悔药没有它三个月后你自己都复现不出当时的指标。另一个技巧是给每次实验打标签用datetime加参数哈希做目录名把模型、日志、混淆矩阵图都存进去。这样跑几十组超参数时不会覆盖也方便对比。我习惯在train.py里加一个--tag参数默认用时间戳跑完在终端打印最佳参数和对应 AUC。心血管数据量不大一组 5 折交叉验证在普通笔记本上几分钟就跑完与其手动记结果不如让脚本自己归档。最后提醒一句临床数据的伦理和隐私问题不在代码层面解决但源码里如果带了真实患者数据分享前务必脱敏这是底线。希望帮到你。本文还有配套的精品资源点击获取