心脏病预测实战:从数据清洗到模型部署的完整指南 简介这份资源面向机器学习入门与进阶学习者聚焦心脏病数据集的分类预测实战帮助读者打通从数据清洗到多模型对比的完整流程。包内共14个文件以11个Python源代码为主另含2个CSV数据集与1份readme说明压缩包约46KB代码手工整理、无语法错误且可直接运行。内容覆盖逻辑回归、KNN、高斯朴素贝叶斯、决策树、SVM、随机森林、XGBoost、MLP等多种分类器并涉及MinMaxScaler、StandardScaler、KNNImputer、ColumnTransformer与Pipeline等预处理手段同时包含混淆矩阵、分类报告、ROC曲线、AUC、学习曲线、排列重要性及dtreeviz可视化等评估与解释方法。已有98人学习下载适合希望系统练习特征工程、模型调参与结果可视化的读者参考借鉴。1. 心脏病预测这套数据集为什么值得你花一个周末跑通拿到「AI实战-心脏病数据集分析预测实例」这个标题时我第一反应不是它有多高深而是它足够小、足够脏、足够真实。150.96 KB 的数据集11 个源代码文件这个体量放在今天动辄几个 G 的 imagenet1k 或者 ccpd 数据集面前几乎可以忽略但恰恰是这种规模能让你在一个周末内把从数据清洗到模型部署的完整链路走一遍而不是卡在下载和解压上。心脏病预测是医学二分类问题的经典入口UCI 的 Cleveland 数据集被无数论文和教程用过特征维度低、样本量适中、缺失值分布有规律非常适合用来验证一套特征工程流程是否真的有效。如果你正在做 AI 测试开发、想找一个能快速复现的端到端项目或者你手头有 phm2012 数据集训练这类工业故障预测任务、想先拿个小数据集练手这套东西的性价比很高。它不解决什么惊天动地的问题但它能让你把逻辑回归、随机森林、XGBoost 在同一个任务上的表现差异摸清楚这就够了。2. 先搞清楚心脏病数据集里到底有什么13 个特征与 303 条样本的底细2.1 特征字段的医学含义与数据类型分布常见的心脏病 UCI 数据集包含 303 条样本、13 个特征加 1 个标签。特征大致分四类连续型数值age、trestbps、chol、thalach、oldpeak、类别型数值sex、cp、fbs、restecg、exang、slope、ca、thal、以及最终标签 target0 或 1。很多人拿到手直接df.describe()看一眼就扔进模型结果被ca和thal里的缺失值坑到怀疑人生。这两个字段的缺失不是随机分布的ca缺失的样本往往伴随thal也缺失直接 dropna 会丢掉将近 6 条记录对 303 条的体量来说就是 2% 的信息损失不能随便扔。我一般会先做一件事把每个字段的取值域打印出来确认没有异常编码。比如cp胸痛类型理论上是 0-3 四个值但有些版本的数据里会出现 4这就是编码错误需要当作缺失处理。thal正常是 1、2、3出现 0 就是缺失。这些细节不检查后面模型给你一个虚高的准确率你还以为调参调对了。import pandas as pd import numpy as np df pd.read_csv(heart.csv) # 打印每个字段的取值域和缺失情况 for col in df.columns: print(f{col}: unique{sorted(df[col].dropna().unique())}, null{df[col].isnull().sum()}) # 把医学上不可能的编码替换为 NaN df[ca] df[ca].replace({4: np.nan}) df[thal] df[thal].replace({0: np.nan})这段代码的逻辑是先摸清每个字段的实际取值再把明显越界的编码标记为缺失。参数上replace里的映射关系要根据你拿到的具体数据版本调整UCI 原始版和 Kaggle 上流传的版本在编码上略有差异跑之前一定先看 unique 输出。2.2 标签分布与类别不平衡的真实情况303 条样本里target1 的通常占 165 条左右target0 占 138 条比例大约 1.2:1。这个不平衡程度不算严重不需要上 SMOTE 或者 focal loss 这类重型武器但也不能完全无视。我见过有人在划分训练集时没做分层抽样结果测试集里正负比变成 2:1模型直接偏向多数类recall 掉到 0.7 以下。用train_test_split的时候把stratifyy加上这是基本操作但每年都有新人在这翻车。另外要注意这个数据集里的 target 定义在不同版本里可能相反。有的版本 1 表示有病有的版本 1 表示没病。你拿到手第一件事是看数据字典或者用逻辑回归跑一遍看系数方向别等到模型上线了才发现预测反了。这种玄学问题在医学数据集里特别常见血泪经验就是永远不要假设标签编码方向。3. 从原始 CSV 到模型可用的特征矩阵清洗、编码与缩放3.1 缺失值填充策略中位数还是模型插补ca和thal的缺失值处理方式直接影响模型表现。我试过三种方案直接删除、中位数填充、KNN 插补。在 303 条样本上删除 6 条对结果的影响其实不大但如果你要做交叉验证每次 fold 里删除的样本不一样会导致评估结果波动。中位数填充简单粗暴对ca这种取值只有 0-3 的离散变量来说中位数可能落在 0.5 这种无意义的值上需要取整。KNN 插补在这么小的数据集上容易过拟合邻居数 k 设 3 还是 5 对结果影响明显。我一般会这么做ca用众数填充thal用众数填充因为这两个都是类别型变量众数比中位数更合理。填充完之后再检查一遍取值域确保没有引入新的非法值。from sklearn.impute import SimpleImputer # 对类别型缺失字段用众数填充 imputer SimpleImputer(strategymost_frequent) df[[ca, thal]] imputer.fit_transform(df[[ca, thal]]) # 确认填充后没有缺失 assert df.isnull().sum().sum() 0, 还有缺失值没处理干净SimpleImputer的most_frequent策略就是取众数适合离散字段。注意fit_transform要在训练集上 fit然后 transform 测试集不然会数据泄露。很多人图省事在全量数据上做填充交叉验证的分数会虚高。3.2 类别特征编码one-hot 还是 ordinalcp、restecg、slope、thal这些字段是有序还是无序决定了你用 one-hot 还是 label encoding。cp胸痛类型分典型心绞痛、非典型心绞痛、非心绞痛疼痛、无症状这四个之间没有明显的顺序关系用 one-hot 更安全。slope是 ST 段斜率分上升、平坦、下降这个有顺序可以用 ordinal 编码成 0、1、2。thal是地中海贫血类型正常、固定缺陷、可逆缺陷也有顺序。但实际做的时候我倾向于全部用 one-hot让模型自己学权重。303 条样本、13 个特征one-hot 之后维度撑死到 30 左右不会造成维度灾难。用pd.get_dummies的时候记得drop_firstTrue避免虚拟变量陷阱。# 对无序类别特征做 one-hot 编码 cat_cols [cp, restecg, slope, thal] df pd.get_dummies(df, columnscat_cols, drop_firstTrue) # 打印编码后的列名和维度 print(f编码后特征数: {df.shape[1]}) print(df.columns.tolist())drop_firstTrue会丢掉每个类别组的第一个水平作为基准防止完全共线性。编码后的列名会变成cp_1、cp_2这种形式后面做特征重要性分析时要注意对应关系。3.3 数值特征标准化StandardScaler 还是 MinMaxScalerage、trestbps、chol、thalach、oldpeak这五个连续特征的量纲差异很大。chol胆固醇范围 100-500 多oldpeak范围 0-6.2如果不做缩放逻辑回归的系数会被大量纲特征主导树模型虽然对量纲不敏感但做特征重要性排序时也会有偏差。我一般用StandardScaler做 z-score 标准化因为心脏病数据里这几个连续特征近似正态分布标准化后更符合线性模型的假设。MinMaxScaler适合有明确边界且分布不均匀的数据这里不太需要。注意 scaler 只能在训练集上 fit然后应用到测试集这个和 imputer 一样是防止数据泄露的铁律。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split X df.drop(target, axis1) y df[target] # 分层划分保证训练集和测试集的正负比一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() num_cols [age, trestbps, chol, thalach, oldpeak] X_train[num_cols] scaler.fit_transform(X_train[num_cols]) X_test[num_cols] scaler.transform(X_test[num_cols])stratifyy保证划分后训练集和测试集的标签比例与原始数据一致。random_state42固定随机种子方便复现。scaler 在训练集上 fit在测试集上只做 transform这是标准流程但每年都有人在这里翻车。4. 三个基线模型跑通与调参逻辑回归、随机森林、XGBoost4.1 逻辑回归作为可解释性基线逻辑回归在医学数据集上永远是第一个该跑的模型因为它系数可解释能告诉你哪个特征对心脏病风险影响最大。303 条样本、30 个左右的 one-hot 特征逻辑回归不会过拟合收敛也快。我一般用 L2 正则C设 1.0 起步然后看交叉验证的 AUC 来调。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score lr LogisticRegression(C1.0, penaltyl2, solverlbfgs, max_iter1000) lr_scores cross_val_score(lr, X_train, y_train, cv5, scoringroc_auc) print(fLR AUC: {lr_scores.mean():.4f} ± {lr_scores.std():.4f}) lr.fit(X_train, y_train) coef_df pd.DataFrame({feature: X_train.columns, coef: lr.coef_[0]}) print(coef_df.sort_values(coef, ascendingFalse).head(10))C是正则化强度的倒数越小正则越强。solverlbfgs适合小数据集。max_iter1000防止不收敛警告。交叉验证用 AUC 而不是准确率因为医学场景下我们更关心排序能力而不是硬分类。4.2 随机森林的特征重要性排序随机森林在这个数据集上通常能跑到 AUC 0.90 左右比逻辑回归高两三个点。它的优势是能捕捉非线性关系比如年龄和最大心率之间的交互效应。我一般用 500 棵树max_depth不设限让树自己长然后用feature_importances_看哪些特征真正在起作用。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators500, max_depthNone, min_samples_split5, min_samples_leaf2, random_state42, n_jobs-1 ) rf_scores cross_val_score(rf, X_train, y_train, cv5, scoringroc_auc) print(fRF AUC: {rf_scores.mean():.4f} ± {rf_scores.std():.4f}) rf.fit(X_train, y_train) importance_df pd.DataFrame({ feature: X_train.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance_df.head(10))min_samples_split5和min_samples_leaf2是防止过拟合的保守设置303 条样本经不起太深的树。n_jobs-1用满所有 CPU 核心加速训练。特征重要性排序里cp、thalach、oldpeak、ca通常排在前列和医学认知一致。4.3 XGBoost 的早停与学习率调优XGBoost 在这个数据集上不一定比随机森林好因为样本量太小梯度提升容易过拟合。但如果调参得当AUC 能到 0.92 左右。关键是learning_rate和n_estimators的配合我一般用 0.05 的学习率配 300 棵树然后加早停。import xgboost as xgb from sklearn.metrics import roc_auc_score xgb_model xgb.XGBClassifier( n_estimators300, learning_rate0.05, max_depth3, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42, use_label_encoderFalse, eval_metriclogloss ) xgb_model.fit( X_train, y_train, eval_set[(X_test, y_test)], early_stopping_rounds20, verboseFalse ) y_pred_proba xgb_model.predict_proba(X_test)[:, 1] print(fXGBoost Test AUC: {roc_auc_score(y_test, y_pred_proba):.4f}) print(fBest iteration: {xgb_model.best_iteration})max_depth3是强正则小数据集上不要用太深的树。subsample0.8和colsample_bytree0.8增加随机性防过拟合。early_stopping_rounds20在验证集 AUC 不提升时提前停止。best_iteration告诉你实际用了多少棵树如果远小于 300说明早停生效了。5. 避坑与排查心脏病预测项目里最容易翻车的 5 个地方5.1 现象交叉验证 AUC 0.95测试集 AUC 0.78原因在全量数据上做了标准化或缺失值填充导致训练集和测试集之间信息泄露。交叉验证的分数虚高一到独立测试集就原形毕露。解决把所有预处理步骤封装进Pipeline在交叉验证的每个 fold 内部单独 fit。sklearn的Pipeline能保证 imputer、scaler、模型按顺序执行且只在训练 fold 上 fit。from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipe Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (scaler, StandardScaler()), (clf, LogisticRegression(C1.0, max_iter1000)) ]) pipe_scores cross_val_score(pipe, X, y, cv5, scoringroc_auc) print(fPipeline AUC: {pipe_scores.mean():.4f} ± {pipe_scores.std():.4f})5.2 现象模型预测全为 1 或全为 0原因标签编码方向搞反了或者类别不平衡导致模型偏向多数类。有时候是因为target列在 one-hot 编码时被误处理了。解决先打印y.value_counts()确认标签分布再用predict_proba看概率分布。如果概率全在 0.5 以上说明模型偏向正类需要调整class_weightbalanced或者检查标签是否反了。5.3 现象特征重要性排序里age排第一但医学上年龄不是最强预测因子原因连续特征没有标准化age的数值范围虽然不大但和 one-hot 特征混在一起时树模型的分裂点会偏向取值多的特征。解决对连续特征做标准化或者用permutation_importance替代feature_importances_后者对特征量纲不敏感。from sklearn.inspection import permutation_importance perm_imp permutation_importance(rf, X_test, y_test, n_repeats10, random_state42) perm_df pd.DataFrame({ feature: X_test.columns, importance: perm_imp.importances_mean }).sort_values(importance, ascendingFalse) print(perm_df.head(10))5.4 现象XGBoost 训练时报错use_label_encoder参数无效原因XGBoost 版本更新后废弃了use_label_encoder参数但很多教程还在用旧写法。解决升级到 1.6 以上版本后直接去掉这个参数或者降级到 1.5。我一般直接去掉然后用eval_metriclogloss替代。5.5 现象测试集 AUC 波动大换一个random_state就差 0.05原因303 条样本太小测试集只有 60 条左右随机划分带来的方差很大。解决用重复交叉验证RepeatedStratifiedKFold替代单次划分报告均值和标准差而不是单次分数。from sklearn.model_selection import RepeatedStratifiedKFold cv RepeatedStratifiedKFold(n_splits5, n_repeats10, random_state42) scores cross_val_score(rf, X, y, cvcv, scoringroc_auc) print(fRepeated CV AUC: {scores.mean():.4f} ± {scores.std():.4f})6. 把模型装进一个可复用的推理脚本从 pickle 到命令行调用训练完模型不是终点能随时加载并预测新样本才算闭环。我一般会把训练好的 scaler、imputer 和模型一起打包成 pickle然后写一个命令行脚本接收 JSON 输入输出预测概率。这样无论是做 AI 测试开发还是集成到 Flask 接口里都只需要调这个脚本。import pickle import json import sys import numpy as np def load_artifacts(pathmodel_artifacts.pkl): with open(path, rb) as f: return pickle.load(f) def predict(input_json): artifacts load_artifacts() scaler artifacts[scaler] model artifacts[model] feature_names artifacts[feature_names] # 把输入 JSON 转成特征向量 input_dict json.loads(input_json) row [input_dict.get(f, 0) for f in feature_names] X np.array(row).reshape(1, -1) # 对连续特征做同样的标准化 num_cols [age, trestbps, chol, thalach, oldpeak] num_idx [feature_names.index(c) for c in num_cols if c in feature_names] X[:, num_idx] scaler.transform(X[:, num_idx]) proba model.predict_proba(X)[0, 1] return {probability: round(float(proba), 4), prediction: int(proba 0.5)} if __name__ __main__: input_json sys.argv[1] if len(sys.argv) 1 else {} result predict(input_json) print(json.dumps(result))这个脚本的关键点在于特征顺序必须和训练时完全一致所以我把feature_names也存进了 pickle。连续特征的索引要动态查找不能硬编码否则 one-hot 编码列顺序一变就全乱了。标准化只用训练时 fit 的 scaler不能重新 fit。调用方式就是python predict.py {age: 55, trestbps: 130, ...}输出一个 JSON。我自己的习惯是每次训练完都跑一遍这个脚本用一条已知样本验证输出和训练时一致确认没有特征错位。这个后悔药成本很低但能省掉后面调试接口时的大量扯皮。希望帮到你。本文还有配套的精品资源点击获取