ARTICLE DETAIL

资讯详情

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

UCI心脏病数据集实战:Python数据分析与机器学习建模全流程

UCI心脏病数据集实战:Python数据分析与机器学习建模全流程 简介面向计算机相关专业毕业设计、期末大作业与项目实训场景这一基于UCI心脏病数据集的心血管疾病预测课题提供了完整可复用的分析方案。项目包含Python源码、标准化前后两份CSV数据集、PDF分析报告、答辩PPT及大量结果图表覆盖特征工程、模型构建、性能评估等关键环节。包体共84个文件以png可视化图表、py程序脚本、jpg统计图、zbak备份文件为主并有csv数据、pptx和pdf文档整体23.8MB结构按数据、代码、图表、报告分模块组织检索方便。已有56人学习下载。使用者可参考决策树、随机森林、神经网络、支持向量机及Logistic回归的完整建模流程查看各参数下的混淆矩阵、ROC曲线和特征重要性分布也可复用统计图表绘制代码快速完成数据分析与结果展示甚至可直接改造为课程综合项目或毕业设计的演示与答辩材料。1. 用Python做心脏病数据分析系统UCI数据集为什么是老手也绕不开的练手案例如果你刚学完Python基础正想找一个能同时练到pandas、可视化、机器学习完整流程的项目UCI心脏病数据集几乎是绕不开的一个案例。它不算大但字段覆盖了数值、类别、缺失值三类典型问题而且目标变量是多分类稍作处理就能变成二分类预测任务。所谓“基于Python的心脏病数据分析系统”本质就是把UCI官网的原始表读进来走一遍缺失值清洗、特征工程、可视化探索、模型训练与评估的完整链路最后落到一个能对新样本做风险预测的可用流程上。这个方向适合两类人一类是用它做课程设计或简历项目的初学者另一类是准备参加数据竞赛想用一个小而全的数据集把pipeline和坑都踩一遍的从业者。它不要求你懂器官模型或临床指南只需要会Python的基本语法在PyCharm或VS Code里配好Python环境就能开始。真正花时间的往往不是建模而是数据处理——因为UCI这套数据看起来规整实际读取时列名、缺失值、阈值处理全是陷阱。2. 拿到UCI心脏病数据先别急着建模两份常见文件的格式差一点与特征工程落地做法2.1 processed.cleveland.data与heart.csv的读取差异UCI官网上的Heart Disease数据集有多个版本最常见的两个是原始文件processed.cleveland.data和经过第三方整理过的heart.csv。很多新手在这里翻车下载processed.cleveland.data后用pd.read_csv直接读发现第一行变成了列名而且所有字段全部错位。因为这份文件是纯逗号分隔的数据没有表头所以读取时必须显式指定headerNone并把列名传进去。常见做法是先定义字段列表再通过names参数赋值。UCI官网对该数据集的说明里给出了14个字段的原始含义age、sex、cp、trestbps、chol、fbs、restecg、thalach、exang、oldpeak、slope、ca、thal、num。最后一个是目标变量官方叫num取值范围0到4表示心脏病严重程度0是无病1到4是不同等级。import pandas as pd # UCI processed.cleveland.data 没有表头必须手动指定列名 columns [ age, sex, cp, trestbps, chol, fbs, restecg, thalach, exang, oldpeak, slope, ca, thal, num ] df pd.read_csv(processed.cleveland.data, headerNone, namescolumns) print(df.shape) # 输出 (303, 14) print(df.head(3)) # 确认列名和数据没有错位这里有两个参数值得注意。headerNone告诉pandas第一行不是列名names则负责按顺序给每一列命名。如果这两个参数缺了任何一个age列里会出现1.0、0.0这类数值而原本第一行数据被当成表头后后面corr()、groupby都会因为类型混乱报错或出怪图。如果你拿到的是heart.csv情况会稍微好一点因为它通常自带列名直接read_csv就行。但下载前最好先用文本编辑器打开看一眼前两行确认是不是有header再决定要不要传names参数。我的习惯是不管哪种文件都先打印df.dtypes检查数值列是不是object类型——只要出现object就说明读取方式大概率有问题或者文件里的缺失值还没处理。2.2 把“?”缺失值变成能算的数值处理方案与参数选择UCI原始数据里有个很阴间的设计缺失值不用NaN而用问号“?”表示集中在ca和thal这两列。pandas读到这种字符串会把整列推断成object类型导致后面计算相关系数直接报错。处理分两步先把“?”替换成标准缺失标记再把这两列强制转成数值类型。import numpy as np # 原始文件用?表示缺失先统一替换为NaN df.replace(?, np.nan, inplaceTrue) # 把ca和thal从字符串转成数值转换失败的值自动变为NaN df[ca] pd.to_numeric(df[ca], errorscoerce) df[thal] pd.to_numeric(df[thal], errorscoerce) # 检查缺失量决定填充还是删除 print(df.isna().sum()) # 典型结果ca有4个缺失thal有2个缺失其余字段无缺失选择填充还是删除我的经验是看缺失比例。ca和thal各自只有4个和2个缺失相对于303行可以忽略直接用行删除即可df.dropna(inplaceTrue)。如果你的数据是从其他渠道合并来的缺失比例超过10%就需要考虑填充。填充时优先用中位数而不是均值因为age、chol这些字段分布的偏态比较明显中位数对异常值更稳。这里还有一个容易忽略的点转数值之后要重新检查列的类型。pd.to_numeric的errorscoerce参数会把任何不能解释为数字的字符串转换成NaN这是处理UCI这类脏数据最常用的一招。不要先fillna再to_numeric顺序反了会因为“?”还在字符串状态而填不进去。2.3 特征筛选与共线性检查少用两个特征模型反而更稳UCI数据集一共13个输入特征其中fbs空腹血糖是否大于120mg/dl和chol血清胆固醇在大多数模型里对目标的区分度很低。常见做法是先算目标变量与所有数值特征的相关系数把排序靠后的特征丢掉再用方差膨胀因子检查剩余特征之间有没有严重的多重共线性。# 只保留数值列做相关性分析 numeric_cols df.select_dtypes(include[np.number]).columns.tolist() target_col num # 把多分类目标转成二分类0表示无病1-4表示有病 df[target] df[num].apply(lambda x: 1 if x 0 else 0) corr df[numeric_cols].corr()[target].abs().sort_values(ascendingFalse) print(corr) # 通常排序结果oldpeak、cp、thalach、ca 排在前列 # fbs 和 chol 垫底可以直接去掉在这个案例里我一般会去掉fbs和chol这两个特征再建模。原因有两个一是相关系数长期低于0.1几乎不携带判别信息二是在决策树类模型里无关特征会让节点分裂时产生无意义的划分虽然影响不大但会让模型的可解释性和稳定性打折扣。删除特征后再用VIF检查共线性。比如trestbps和age经常存在中等程度的相关但它们各自的业务含义独立不需要强行删除。真正需要警惕的是把两个高度相关的特征同时放进线性模型比如某份数据里如果同时有“总胆固醇”和“LDL胆固醇”这两个字段相关性可能在0.9以上逻辑回归的系数会被拉得极不稳定。遇到这种情况保留业务上更直接的那个或做PCA降维二选一即可。3. 把风险因素画出来用Python可视化探索心脏病数据的关键图与读图方法3.1 目标变量分布与年龄分箱先看数据有没有“偏科”做数据分析与可视化时第一步永远是看目标变量分布而不是急着画散点图。UCI数据集的num字段有0到4五个取值直接画出来会看到0类占比约54%1类约26%2、3、4类加起来约20%。如果按二分类处理正负样本比例大约是31比69接近1:2不算极度不均衡但也足够让只看accuracy的人上当。把类型转成二分类之后我习惯再做一次年龄分箱看患病率随年龄的变化趋势。这一步能同时验证数据质量和业务常识如果数据正常60岁以上的患病率应当明显高于40岁以下。import matplotlib.pyplot as plt import seaborn as sns # 年龄分箱增加一列年龄段 bins [20, 30, 40, 50, 60, 70, 80] labels [20s, 30s, 40s, 50s, 60s, 70s] df[age_group] pd.cut(df[age], binsbins, labelslabels, rightFalse) # 统计每个年龄段内的患病比例 risk_by_age df.groupby(age_group, observedFalse)[target].mean().reset_index() # 画柱状图 plt.figure(figsize(8, 5)) sns.barplot(datarisk_by_age, xage_group, ytarget) plt.title(Heart Disease Rate by Age Group) plt.ylabel(Disease Rate) plt.show()这段代码里有两个常见问题要注意。pd.cut分箱时必须把right参数想清楚rightFalse表示区间是左闭右开20岁会落进20s组而不是30s组。groupby时加上observedFalse是给pandas 2.x版本用的不加的话某些版本会报关于category的FutureWarning。读图时不要只看柱子的绝对高度还要看每组的样本量。如果某个年龄段只有几例它的患病率波动会非常大不能当作可靠结论。另一个值得做的是sex字段的对比——男性样本量约为女性的两倍患病率也明显更高这个特征在后续建模时必须保留。3.2 相关性热力图与分组箱线图找到与患病强相关的特征探索特征和目标关系热力图只能给一个大方向真正细看还是要画分组箱线图。UCI数据里最经典的判别特征是oldpeak运动相对休息时的ST段压低值它在无病组和有病组的分布差异非常大几乎每个做这个案例的人都会第一眼看到这个特征。# 相关性热力图只画数值特征的相关系数矩阵 plt.figure(figsize(12, 10)) numeric_cols df.select_dtypes(include[np.number]).columns.tolist() sns.heatmap(df[numeric_cols].corr(), cmapRdBu_r, center0, annotFalse, squareTrue) plt.title(Correlation Matrix of UCI Heart Disease Features) plt.show() # 分组箱线图重点看oldpeak和thalach fig, axes plt.subplots(1, 2, figsize(12, 5)) sns.boxplot(axaxes[0], xtarget, yoldpeak, datadf) sns.boxplot(axaxes[1], xtarget, ythalach, datadf) plt.show()热力图里可以优先找与target(0/1)的相关系数绝对值超过0.4的列通常是oldpeak、cp、thalach、ca。这几个特征可以作为后续模型的核心特征组合。但同时也要看特征之间的配对颜色——如果某个深红色方块两端恰好是两个业务上近似的字段就要考虑共线性问题。箱线图的读法有一点容易被忽略不要只看中位数要看箱体重叠程度和异常值分布。如果两组的中位数接近但箱体都很宽说明该特征区分度有限如果oldpeak这类特征的箱体几乎没有重叠那它单独拉出来也能构建一个粗糙的判别规则。3.3 可视化结果如何反哺特征工程可视化不只是为了放进报告里它的直接产出是特征工程决策。例如用箱线图发现thalach最大心率在无病组整体偏高说明它是有保护意义的特征不要因为相关系数不突出就丢掉再比如cp胸痛类型是定序变量取值1到4画countplot会发现类型2是患病率的分水岭可以考虑做一个哑变量cp_is_2。另一个常见的特征是slopeST段斜率它有0、1、2三个取值但样本分布很不平均。通过分组柱状图能看出取值2几乎都落在有病组这时可把它转成二元变量而不是保留三分类。这种从图里“看出”的特征构造方式比盲目用算法筛选更可控也更适合向别人解释你的分析系统为什么这样设计。4. 从逻辑回归到XGBoost心脏病预测模型的对比、调参与阈值选择4.1 分层交叉验证与基线模型先定及格线建模的第一步不是直接上XGBoost而是先训练一个简单模型当基线。UCI数据集只有303条样本随便切一次训练测试集结果都会因为噪声产生较大波动。正确的做法是使用分层K折交叉验证保证每一折里正负样本比例和全量一致否则某几折可能只分到两三个患病样本模型效果和评价指标都会失真。from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # 准备特征与标签 features [age, sex, cp, trestbps, thalach, exang, oldpeak, slope, ca, thal] X df[features] y df[target] # 逻辑回归放进Pipeline先做标准化再做训练 pipe_lr Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(max_iter1000, random_state42)) ]) cv StratifiedKFold(n_splits10, shuffleTrue, random_state42) scores cross_val_score(pipe_lr, X, y, cvcv, scoringroc_auc) print(fLogistic Regression AUC: {scores.mean():.3f} ± {scores.std():.3f})这段代码里有几个必须理解的细节。Pipeline把StandardScaler和LogisticRegression串成一步交叉验证时标准化只会用训练折的均值方差不会偷看验证折这是防止数据泄露的基本操作。StratifiedKFold的n_splits10配合shuffleTrue保证每一折样本的类别比例接近全量同时随机打乱降低排序带来的偏差。为什么用roc_auc而不是accuracy做评估因为患病样本占比只有大约31%如果只看准确率一个把所有样本都预测为无病的模型也能拿到69%的准确率极具欺骗性。AUC衡量的是模型把正例排到负例前面的能力不依赖阈值更适合类别不均衡的场景。基线逻辑回归的AUC一般在0.88到0.92之间如果你跑出来低于0.8先回去检查数据处理而不是调参。4.2 随机森林和XGBoost的三个必调参数拿到基线分数后再上树模型。随机森林在这种小数据集上很容易过拟合因为303条样本对树模型来说太少了。关键参数不是n_estimators多就好而是约束每棵树的复杂度。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth4, min_samples_leaf3, max_featuressqrt, random_state42 ) rf_scores cross_val_score(rf, X, y, cvcv, scoringroc_auc) print(fRandom Forest AUC: {rf_scores.mean():.3f} ± {rf_scores.std():.3f})这里三个参数是按重要性排的。max_depth限制树的深度小数据上设为4到6防止单棵树记住过多噪声min_samples_leaf要求叶子节点至少包含3个样本比直接限制最大深度更有效max_featuressqrt让每次分裂只考虑特征总数的平方根个特征增加树之间的多样性。如果还想上XGBoost思路类似但多一个关键参数scale_pos_weight它用来处理类别不均衡取值为负类样本数除以正类样本数。UCI案例里大约用2.2效果比直接调threshold更顺滑。from xgboost import XGBClassifier # 小数据集上控制学习率和树的复杂度别把迭代次数拉满 xgb XGBClassifier( n_estimators150, learning_rate0.05, max_depth3, subsample0.8, scale_pos_weight2.2, random_state42, eval_metricauc ) xgb_scores cross_val_score(xgb, X, y, cvcv, scoringroc_auc) print(fXGBoost AUC: {xgb_scores.mean():.3f} ± {xgb_scores.std():.3f})learning_rate0.05配合n_estimators150意思是用小步长多走几步比一次性放大学习率更稳。subsample0.8让每棵树只用80%的样本进一步降低过拟合。scale_pos_weight从模型层面调整了正负样本的权重它和下一节要说的阈值调整是两个维度的操作不冲突。4.3 阈值不只是0.5用Recall与Precision的平衡找最佳切分点模型输出的概率默认按0.5切分但在这个场景下0.5不一定合理。心脏病风险预测里漏掉一个真正的高危患者的代价远大于把一位普通人误判为高风险所以应该把阈值下调让模型更“愿意”预测为患病代价是假阳性增多。from sklearn.model_selection import train_test_split from sklearn.metrics import precision_recall_curve # 单独划分一次测试集避免在交叉验证里调阈值造成数据泄漏 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) pipe_lr.fit(X_train, y_train) proba pipe_lr.predict_proba(X_test)[:, 1] precision, recall, thresholds precision_recall_curve(y_test, proba) # 找到 recall 不低于0.85时precision最高的阈值 valid_mask recall 0.85 best_idx precision[valid_mask].argmax() best_threshold thresholds[valid_mask][best_idx] print(fSelected threshold: {best_threshold:.2f})这段代码的核心在于先固定recall下界再找precision最高的点。为什么不用默认0.5因为0.5对应的recall可能只有0.7左右也就是说差不多三成高危患者会被漏掉。把阈值降到0.35附近recall通常能提到0.85以上precision会从0.8掉到0.6左右但从业务角度这是可接受的。阈值调完之后要重新在测试集上计算混淆矩阵不要只报AUC。AUC是排序能力阈值是决策规则两者必须配合着看。树模型虽然支持predict_proba但XGBoost在类别不均衡时输出的概率往往会整体偏低这时更需要调阈值而不是直接用0.5。5. 项目常见问题排查五个让心脏病分析结果翻车的细节与解决办法5.1 列名缺失导致所有字段错位现象打印df.head()后age列里出现1.0、0.0sex列里出现male这类字符串整个表格字段对不上。原因processed.cleveland.data没有表头read_csv默认把第一行数据当成列名导致实际数据从第二行开始所有列名整体错位一行。解决读取时加上headerNone和namescolumns两个参数。如果已经读进去了用df.columns columns硬改列名再把第一行删掉。但最好还是重新按正确方式读取因为硬改列名后索引还是错的。5.2 缺失值以问号形式存成字符串警告信息刷屏现象调用df.corr()时报错说无法将字符串转换为浮点数或者只有age、chol等少数几列能参与计算。原因ca和thal列里的“?”被pandas识别为普通字符串整列类型变成object数值函数全部失效。解决先df.replace(?, np.nan, inplaceTrue)再对这两列执行pd.to_numeric(..., errorscoerce)。注意顺序不能反先替换后转换。处理完再检查dtypes确认已经是float64。5.3 用全量数据做特征选择后再切训练集造成数据泄露现象交叉验证AUC高达0.95但在新数据上只有0.7左右模型明显虚高。原因在划分训练测试集之前先用了全量数据算相关性、删特征、看可视化这些操作等于让测试集的信息参与到了特征选择过程中。测试集不再干净评估结果自然乐观。解决先把数据切分为train_test_split特征选择只在训练集上执行。更规范的做法是把特征选择器放进Pipeline里例如SelectKBest交叉验证时它会在每一折的训练子集上重新选择特征。5.4 类别不均衡让准确率虚高模型却漏掉真正的高危患者现象模型在测试集上准确率88%但看混淆矩阵患病组里有一半以上被分到无病组。原因正负样本比约31比69逻辑回归用默认阈值0.5时模型倾向输出负类因为把样本判为无病的整体损失更小。解决评价指标从accuracy换成AUC、Recall、Precision。训练层面用class_weightbalanced或scale_pos_weight2.2决策层面用precision_recall_curve重新找阈值两种手段叠加才能控制漏诊率。5.5 逻辑回归不做特征缩放系数和收敛速度都很不稳现象max_iter警告频繁出现训练时间偏长检查模型系数时发现某个特征的权重异常大但相关性分析里它并不重要。原因chol取值范围从120到400oldpeak只有0到6不缩放的逻辑回归梯度下降要在这种尺度差异极大的特征空间里找最优解收敛变慢系数解释性也失真。解决把StandardScaler放入Pipeline的第一步先对特征标准化再喂给模型。注意这条只影响线性模型随机森林和XGBoost不做特征缩放也能跑但放在Pipeline里统一处理没有坏处。6. 把分析包成可复用的流程预测函数、验证技巧与最终落地建议6.1 一个统一的predict入口训练、保存、预测三步走前面的分析和调参都是交互式操作真正要作为“系统”交付至少要封装成可重复执行、可对单条样本预测的代码。常见做法是把最终选定的模型组合成一个函数训练后保存到本地文件以后只加载不重训。import joblib def train_and_save(features, target, model, model_pathheart_model.pkl): 训练模型并保存完整Pipeline X df[features] y df[target] pipe Pipeline([ (scaler, StandardScaler()), (clf, model) ]) pipe.fit(X, y) joblib.dump(pipe, model_path) return pipe # 用一个样本测试保存后的模型 loaded_pipe joblib.load(heart_model.pkl) sample pd.DataFrame([{ age: 63, sex: 1, cp: 3, trestbps: 145, thalach: 150, exang: 0, oldpeak: 2.3, slope: 1, ca: 0, thal: 6 }]) prob loaded_pipe.predict_proba(sample)[0, 1] print(f患病概率: {prob:.2%})这段封装逻辑里需要注意的是保存时保存的是Pipeline而不是裸模型。因为预测时新样本也要走标准化如果不保存scalerpredict结果就是错的。joblib是sklearn官方推荐的持久化方式比pickle更稳对包含numpy数组的模型结构兼容性更好。6.2 用校准曲线和AUC验证模型的可信度除了交叉验证分数落地前还要看模型输出的概率是否可信。一个常见的反直觉情况是模型AUC很高但预测出的0.9概率实际只有0.7的患者真的患病说明概率被系统性高估了。这种情况在XGBoost等梯度提升模型上尤其常见。from sklearn.calibration import calibration_curve prob_test loaded_pipe.predict_proba(X_test)[:, 1] fraction_pos, mean_pred calibration_curve(y_test, prob_test, n_bins10) # 打印前几个箱子的校准结果 for i in range(len(mean_pred)): print(f预测概率 {mean_pred[i]:.2f} ~ {mean_pred[i]0.1:.2f}, 实际患病率 {fraction_pos[i]:.2f})校准曲线的读法是如果预测概率0.5的人群实际患病率也是0.5左右说明校准良好如果预测概率明显高于实际概率需要做概率修正或换用更保守的模型。这个步骤在竞赛里不常见但在真正做分析系统时非常有必要因为医生或业务方拿到的是具体概率值而不是排名。6.3 落地习惯UCI心脏病数据集这个案例最让我印象深刻的不是模型精度的提升而是同一套流程可以平移到其他临床预测项目上。拿到任何新数据先检查缺失标记格式再做分层划分最后统一放进Pipeline交叉验证这套顺序能把八成翻车原因提前排除。我最早做这个案例时第一版就是把准确率当KPI结果模型漏掉了将近一半的高危患者当时完全没意识到是阈值和评价指标的问题。后来养成的习惯是每次训练完先看混淆矩阵再针对漏诊那一格反推数据处理的漏洞。这个案例虽小但把pandas类型处理、交叉验证泄漏、类别不均衡这几个坑踩明白之后再做大规模医疗数据项目会省下很多返工时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表