简介:这份资源面向机器学习入门与进阶学习者,聚焦心脏病数据集的分类预测实战,帮助读者打通从数据清洗到多模型对比的完整流程。包内共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)、以及最终标签 target(0 或 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 条样本里,target=1 的通常占 165 条左右,target=0 占 138 条,比例大约 1.2:1。这个不平衡程度不算严重,不需要上 SMOTE 或者 focal loss 这类重型武器,但也不能完全无视。我见过有人在划分训练集时没做分层抽样,结果测试集里正负比变成 2:1,模型直接偏向多数类,recall 掉到 0.7 以下。用train_test_split的时候把stratify=y加上,这是基本操作,但每年都有新人在这翻车。
另外要注意,这个数据集里的 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(strategy='most_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 还是 ordinal
cp、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_first=True,避免虚拟变量陷阱。
# 对无序类别特征做 one-hot 编码 cat_cols = ['cp', 'restecg', 'slope', 'thal'] df = pd.get_dummies(df, columns=cat_cols, drop_first=True) # 打印编码后的列名和维度 print(f"编码后特征数: {df.shape[1]}") print(df.columns.tolist())drop_first=True会丢掉每个类别组的第一个水平作为基准,防止完全共线性。编码后的列名会变成cp_1、cp_2这种形式,后面做特征重要性分析时要注意对应关系。
3.3 数值特征标准化:StandardScaler 还是 MinMaxScaler
age、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', axis=1) y = df['target'] # 分层划分,保证训练集和测试集的正负比一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) 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])stratify=y保证划分后训练集和测试集的标签比例与原始数据一致。random_state=42固定随机种子,方便复现。scaler 在训练集上 fit,在测试集上只做 transform,这是标准流程,但每年都有人在这里翻车。
4. 三个基线模型跑通与调参:逻辑回归、随机森林、XGBoost
4.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(C=1.0, penalty='l2', solver='lbfgs', max_iter=1000) lr_scores = cross_val_score(lr, X_train, y_train, cv=5, scoring='roc_auc') print(f"LR 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', ascending=False).head(10))C是正则化强度的倒数,越小正则越强。solver='lbfgs'适合小数据集。max_iter=1000防止不收敛警告。交叉验证用 AUC 而不是准确率,因为医学场景下我们更关心排序能力而不是硬分类。
4.2 随机森林的特征重要性排序
随机森林在这个数据集上通常能跑到 AUC 0.90 左右,比逻辑回归高两三个点。它的优势是能捕捉非线性关系,比如年龄和最大心率之间的交互效应。我一般用 500 棵树,max_depth不设限,让树自己长,然后用feature_importances_看哪些特征真正在起作用。
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=500, max_depth=None, min_samples_split=5, min_samples_leaf=2, random_state=42, n_jobs=-1 ) rf_scores = cross_val_score(rf, X_train, y_train, cv=5, scoring='roc_auc') print(f"RF 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', ascending=False) print(importance_df.head(10))min_samples_split=5和min_samples_leaf=2是防止过拟合的保守设置,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_estimators=300, learning_rate=0.05, max_depth=3, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42, use_label_encoder=False, eval_metric='logloss' ) xgb_model.fit( X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=False ) y_pred_proba = xgb_model.predict_proba(X_test)[:, 1] print(f"XGBoost Test AUC: {roc_auc_score(y_test, y_pred_proba):.4f}") print(f"Best iteration: {xgb_model.best_iteration}")max_depth=3是强正则,小数据集上不要用太深的树。subsample=0.8和colsample_bytree=0.8增加随机性防过拟合。early_stopping_rounds=20在验证集 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(strategy='most_frequent')), ('scaler', StandardScaler()), ('clf', LogisticRegression(C=1.0, max_iter=1000)) ]) pipe_scores = cross_val_score(pipe, X, y, cv=5, scoring='roc_auc') print(f"Pipeline AUC: {pipe_scores.mean():.4f} ± {pipe_scores.std():.4f}")5.2 现象:模型预测全为 1 或全为 0
原因:标签编码方向搞反了,或者类别不平衡导致模型偏向多数类。有时候是因为target列在 one-hot 编码时被误处理了。
解决:先打印y.value_counts()确认标签分布,再用predict_proba看概率分布。如果概率全在 0.5 以上,说明模型偏向正类,需要调整class_weight='balanced'或者检查标签是否反了。
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_repeats=10, random_state=42) perm_df = pd.DataFrame({ 'feature': X_test.columns, 'importance': perm_imp.importances_mean }).sort_values('importance', ascending=False) print(perm_df.head(10))5.4 现象:XGBoost 训练时报错use_label_encoder参数无效
原因:XGBoost 版本更新后废弃了use_label_encoder参数,但很多教程还在用旧写法。
解决:升级到 1.6 以上版本后直接去掉这个参数,或者降级到 1.5。我一般直接去掉,然后用eval_metric='logloss'替代。
5.5 现象:测试集 AUC 波动大,换一个random_state就差 0.05
原因:303 条样本太小,测试集只有 60 条左右,随机划分带来的方差很大。
解决:用重复交叉验证(RepeatedStratifiedKFold)替代单次划分,报告均值和标准差而不是单次分数。
from sklearn.model_selection import RepeatedStratifiedKFold cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42) scores = cross_val_score(rf, X, y, cv=cv, scoring='roc_auc') print(f"Repeated 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(path='model_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。
我自己的习惯是每次训练完都跑一遍这个脚本,用一条已知样本验证输出和训练时一致,确认没有特征错位。这个后悔药成本很低,但能省掉后面调试接口时的大量扯皮。希望帮到你。
本文还有配套的精品资源,点击获取