
简介一套基于Python的心脏病数据分析与预测项目资料面向计算机相关专业学生适用于毕业设计、期末大作业或项目实训参考。项目依托UCI心脏病数据集完整覆盖数据预处理、特征工程、模型构建与结果评估等技术环节。整套资料共84个文件、约23.8MB主要包含Python源码数据预处理、建模、模型选择、可视化、CSV标准数据集、PDF分析报告、答辩PPT以及大量结果图表如混淆矩阵、ROC曲线、特征重要性分布等。目前已有56人学习/下载。除可直接运行的代码与数据外还提供了多种机器学习模型决策树、随机森林、支持向量机、神经网络等的对比评估过程与可视化图表便于理解模型调参和性能验证方法文档与PPT结构完整可作为毕业设计或课程综合项目的整体参考范例。1. 心脏病数据分析系统不是玄学先从UCI数据集看清边界一套能跑通、能交差、能写进简历的 «基于Python的心脏病数据分析系统»听起来像是一个毕业设计题目的标准答案但实际上它有非常具体的技术边界数据来自UCI Machine Learning Repository公开的心脏病数据集最常用的是Cleveland子集共303条样本、14个字段。这份资源的价值不在于“机器学习高大上”而在于它把一条完整的数据分析链路——从字段清洗、特征工程、模型训练到可视化展示——封装成可以直接改、直接跑的Python代码。它适合谁两类人。一类是正在做期末大作业或毕业设计的学生需要一份能讲清楚“每一步为什么这么做”的参考实现另一类是刚接触Python数据分析的从业者想在一个小数据集上完整走一遍pandas、scikit-learn和可视化工具链。先说结论这套系统最大的难点不在算法选型而在数据本身的坑——UCI这份数据里有缺失值、有语义模糊的分类字段、有不平衡的标签分布这些才是真正决定项目质量的地方。2. UCI心脏病数据集拆解字段语义、分布规律与清洗策略2.1 十四列字段逐一过一遍别把它们都当成数值UCI心脏病数据集是从克利夫兰诊所的冠心病研究数据整理而来常见文件名是heart.csv也有版本叫processed.cleveland.data。数据列分别为age年龄、sex性别1男0女、cp胸痛类型、trestbps静息血压、chol血清胆固醇、fbs空腹血糖是否大于120mg/dl、restecg静息心电图结果、thalach最大心率、exang运动诱发心绞痛、oldpeakST段压低值、slopeST段峰值斜率、ca主要血管数、thal地中海贫血类型、target是否患有心脏病1有0无。这份数据里最容易被当成连续数值乱用的字段是cp、restecg、slope、thal和ca。它们是医学语义上的分类编码比如cp的取值1到4分别代表典型心绞痛、非典型心绞痛、非心源性疼痛、无症状数字之间没有“越大越严重”的递进关系。如果直接把原始编码丢给线性模型模型会强行学习到数字大小的单调关系这会导致病理性错误。我一般会在建模前单独处理这些字段要么做独热编码要么按医学常识重新映射成有序等级。import pandas as pd df pd.read_csv(heart.csv) # 先看一眼缺失值和数据类型 print(df.info()) print(缺失值统计) print(df.isnull().sum()) # ca 和 thal 在原始文件中存在无效占位符读取后需要排查 # 常见做法把无法解析的字符替换成 NaN df[ca] pd.to_numeric(df[ca], errorscoerce) df[thal] pd.to_numeric(df[thal], errorscoerce) print(清洗后缺失值) print(df.isnull().sum())这段代码解决的是数据入场后的第一道坎。pd.to_numeric加errorscoerce会把无法解析的字符串变成NaN避免后续建模时类型报错。我用df.info()先确认每列的数据类型和内存占用再集中统计缺失值这一步在UCI原始数据上必须做——Cleveland版本的数据文件里ca和thal列有少量以?表示的缺失值直接用read_csv读进来会得到 object 类型后面做标准化时就会翻车。2.2 缺失值处理方式删行还是补值取决于缺失率和业务含义清洗完缺失值统计之后下一步是决定哪些缺失值该删、哪些该补。这里没有统一标准我的习惯是先看缺失比例如果某列缺失超过30%直接考虑删列如果是个别样本的缺失优先删行如果删除会损失太多样本再用中位数或众数补值。Cleveland数据集本身只有303条样本扣掉缺失后剩余可用样本通常不到300条这意味着每删一条都会直接影响训练集大小。所以我倾向于对ca和thal这两列做众数填充而不是粗暴删行。# 用众数填充缺失列避免有效样本被浪费 for col in [ca, thal]: df[col] df[col].fillna(df[col].mode()[0]) # 填充完成后再次确认 print(df.isnull().sum().sum())填充之后不要急着建模先检查分类字段的取值分布是否正常。UCI数据集中thal的有效取值是3、6、7分别对应正常、固定缺陷和可逆缺陷但有些版本的数据里会出现数值0的无效样本——这是我实际复现时踩到的第一个坑。无效取值不属于缺失值isnull()查不出来但它会污染模型训练。排查方式很简单用value_counts()看每个分类字段的分布发现异常取值再单独处理。2.3 数据分布的可视化检查先从散点和直方图里发现规律数据清洗不是闷头写代码可视化能更快暴露问题。我习惯先做一个全局的分布矩阵图重点观察标签列target的分布是否均衡、关键特征thalach与target之间的关系、以及是否存在明显的离群点。import matplotlib.pyplot as plt import seaborn as sns sns.set_theme(stylewhitegrid) fig, axes plt.subplots(2, 2, figsize(12, 8)) sns.histplot(datadf, xage, huetarget, multiplestack, axaxes[0, 0]) sns.boxplot(datadf, xtarget, ythalach, axaxes[0, 1]) sns.countplot(datadf, xcp, huetarget, axaxes[1, 0]) sns.scatterplot(datadf, xoldpeak, ythalach, huetarget, axaxes[1, 1]) plt.tight_layout() plt.savefig(data_distribution.png, dpi120)这张图能同时看出三个信号第一target的两类样本数量是否接近如果悬殊太大后面必须考虑重采样或调整评价指标第二thalach在两类人群中的分布是否明显不同这决定了它作为特征的区分度第三cp的不同取值下心脏病患者的占比差异通常cp4无症状组的心肌梗死比例会异常高。3. 建模主线特征工程、三类模型对比与指标选择3.1 特征工程分类字段独热编码数值字段标准化建模前的最后一步是特征工程。age、trestbps、chol、thalach、oldpeak这些数值型特征量纲不同直接丢给模型会让chol这种数值大的字段天然获得更高权重必须做标准化或归一化。分类字段按上一章的分析做独热编码这样逻辑回归能真正学习到每个类别的独立效应。from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer cat_cols [cp, restecg, slope, thal, sex, fbs, exang] num_cols [age, trestbps, chol, thalach, oldpeak, ca] preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols) ] )ColumnTransformer的好处在于它能把预处理流程打包成一个对象在交叉验证里随模型一起训练避免数据泄露。注意ca虽然数值不大但它本质上是“血管数量”的计数这里直接放进数值列做标准化属于可接受的折中方案。如果你追求更严谨的写法应该把它也转成有序分类变量。3.2 三类模型做横向对比逻辑回归、随机森林、XGBoost心脏病的二分类问题不需要复杂的深度学习传统机器学习的三件套就够用。我对比的是逻辑回归、随机森林和XGBoost理由很直接逻辑回归适合做基线和可解释性分析随机森林抗过拟合能力强、对分类特征不敏感XGBoost在表格数据上通常能拿到更高的AUC。三个模型共用同一套预处理管线把训练集和测试集同时过一遍结果才具备可比性。import xgboost as xgb from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier models { logistic: Pipeline([ (pre, preprocessor), (clf, LogisticRegression(max_iter1000, random_state42)) ]), rf: Pipeline([ (pre, preprocessor), (clf, RandomForestClassifier(n_estimators200, max_depth6, random_state42)) ]), xgb: Pipeline([ (pre, preprocessor), (clf, xgb.XGBClassifier(n_estimators200, max_depth4, learning_rate0.05, eval_metricauc, random_state42)) ]) }这里的参数不是随手填的都是我在这个数据集上试过之后留下的合理默认值。max_iter1000是因为独热编码后特征维度变多逻辑回归收敛需要更多轮次max_depth控制在4到6是为了防止树模型在小样本上过拟合learning_rate调低到0.05搭配200棵树通常比默认值更稳。你可以先跑一遍再用网格搜索微调但不要一上来就上复杂调参小数据集更看重稳定性和可解释性。3.3 评估指标别只看准确率ROC曲线才能说明问题UCI心脏病数据集的类别分布大致呈40%比60%样本总量不到300条这时候accuracy很容易给出虚假的安全感。比如模型把所有样本都预测成“无心脏病”准确率也有接近六成但这个模型毫无价值。我评估时核心看三个指标roc_auc、recall和f1-score。from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score X df.drop(target, axis1) y df[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_test) y_prob model.predict_proba(X_test)[:, 1] print(f{name}: acc{accuracy_score(y_test, y_pred):.3f}, frecall{recall_score(y_test, y_pred):.3f}, ff1{f1_score(y_test, y_pred):.3f}, fauc{roc_auc_score(y_test, y_prob):.3f})注意train_test_split里加了stratifyy这是为了在切分时保证训练集和测试集中正负样本的比例与原始数据一致。这在小数据集上尤其重要否则运气不好切分出来的测试集可能全是某一类样本导致评估指标剧烈波动。执行完这段代码你会看到三个模型的四项指标对比——在多数种子下XGBoost的AUC能跑到0.92左右随机森林略低逻辑回归最低但胜在系数可以直接解读。这份资源的核心交付物之一就是这套对比结果和画出ROC曲线的完整代码。4. 把分析系统落成Python应用训练、预测与可视化闭环4.1 保存模型和预处理管线预测时不能再重复训练数据分析项目做到模型训练只是前半段后半段是把模型落地成“输入一堆指标、输出风险概率”的系统。常见的做法是用joblib把训练好的Pipeline整个保存成文件而不是单独保存模型和预处理对象。为什么要整个打包因为Pipeline里包含了独热编码的列名映射和标准化参数的均值方差这些元信息在预测阶段必须和训练时保持一致分开保存容易在不同环境间出现列顺序错乱的问题。import joblib from sklearn.model_selection import cross_val_score from sklearn.metrics import roc_curve # 挑AUC最优的模型做最终训练 best_model models[xgb] best_model.fit(X_train, y_train) joblib.dump(best_model, heart_model.joblib) print(模型已保存文件大小) import os print(f{os.path.getsize(heart_model.joblib) / 1024:.1f} KB)保存之后最好立即做一次反序列化验证确认加载出来的模型能用同样的输入做预测不然部署到另一台机器上发现模型文件损坏或者环境依赖版本不对排查起来非常耗时。这个习惯我后来一直在用每次保存模型前都强制走一遍“保存 → 加载 → 预测 → 比对结果”的闭环。4.2 用Flask写一个最小可用的预测接口预测接口是整个“系统”中最容易让初学者卡住的部分。不需要复杂的前端框架Flask写三四十行代码就能提供一个支持POST请求的JSON接口。接收的参数是患者指标返回的是心脏病风险概率和风险等级同时把模型的决策依据回传给调用方。from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) model joblib.load(heart_model.joblib) feature_order [age, sex, cp, trestbps, chol, fbs, restecg, thalach, exang, oldpeak, slope, ca, thal] app.route(/predict, methods[POST]) def predict(): try: data request.get_json() features [float(data[col]) for col in feature_order] features np.array(features).reshape(1, -1) prob model.predict_proba(features)[0][1] risk_level 高风险 if prob 0.5 else 低风险 return jsonify({risk_probability: round(prob, 4), risk_level: risk_level}) except KeyError as e: return jsonify({error: f缺少字段 {e}}), 400 if __name__ __main__: app.run(host127.0.0.1, port5000, debugFalse)接口中feature_order列表的顺序必须和训练时X_train的列顺序完全一致这是最容易翻车的地方。如果训练时df.drop(target, axis1)的列顺序是英文表头的排列顺序而预测请求按别的顺序传参模型输出的概率就是错的——但程序不会报错因为特征个数相同。这一类“静默错误”在部署阶段最有迷惑性需要额外的断言来兜底。4.3 前端可视化交互式Web界面让分析结果直观可见除了JSON接口系统里通常还配一个简单的交互页面。用Flask的render_template渲染一个HTML表单用户填写13项身体指标点击提交后前端用fetch把数据POST到/predict接口拿到结果后在前端展示风险概率和风险等级。这种架构的好处是后端逻辑和前端展示完全解耦后续换成Vue或者React只改前端不动模型代码。# templates/index.html 关键部分 form idheartForm label年龄/label input typenumber nameage required label胸痛类型 (1-4)/label select namecp option value1典型心绞痛/option option value2非典型心绞痛/option option value3非心源性疼痛/option option value4无症状/option /select button typesubmit预测风险/button /form div idresult/div script document.getElementById(heartForm).addEventListener(submit, async (e) { e.preventDefault(); const formData new FormData(e.target); const payload Object.fromEntries(formData.entries()); const resp await fetch(/predict, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify(payload) }); const result await resp.json(); document.getElementById(result).innerHTML 风险概率: ${result.risk_probability}等级: ${result.risk_level}; }); /script到这里整条链路已经走通了数据清洗 → 特征工程 → 模型训练 → 接口封装 → 前端展示。这套系统交付后你可以直接运行python app.py然后在浏览器里操作整个预测流程。5. 避坑与排查复现这套系统最容易翻车的五个地方5.1 现象read_csv读出来的ca列全是对象类型模型无法训练原因UCI原始文件里缺失值用?表示Pandas默认不识别它为空值会把它当成普通字符串读入整列的dtype就变成了object。后续进行数值运算时抛出 TypeError。解决读取时直接指定na_values[?]这样?会按缺失值处理再走填充逻辑。不要等到建模前才处理数据读入阶段就应完成这一步。df pd.read_csv(heart.csv, na_values[?])5.2 现象模型AUC只有0.75怎么调都上不去原因ca列里存在无意义取值比如数值0之外的异常值。UCI文档对ca的定义是0到3之间的整数但部分版本的数据文件里混入了其他字符或异常编码。这些异常样本干扰了特征分布。解决建模前检查df[ca].value_counts()把取值不符合文档定义的样本单独标记或剔除。我当时的处理是把ca取值范围限制在0到3之间范围外的统一按缺失值填充。5.3 现象训练集AUC接近1.0测试集只有0.7差距非常大原因在训练之前先把整个数据集做了标准化包括测试集的数据。另一种可能是独热编码在训练集和测试集上分别执行导致某些类别只在测试集中出现产生维度不匹配。解决用Pipeline把预处理和训练打包成一个整体在交叉验证中对训练集做fit_transform对测试集只做transform。这能从根本上杜绝数据泄露。如果发现类别不匹配在OneHotEncoder里设handle_unknownignore。5.4 现象模型预测结果全是“无心脏病”查代码逻辑没有明显错误原因target列中的正样本有心脏病占比较低模型在默认阈值0.5下偏向预测多数类。这不是代码bug而是类别不平衡问题。解决两个方案。一是训练时给少数类加权重比如class_weightbalanced二是根据临床场景调整阈值因为漏诊心脏病假阴性的代价远比误报假阳性高。把阈值从0.5降到0.3让模型更敏感同时用ROC曲线找到约登指数最优的切点。5.5 现象Flask接口部署到服务器上后预测速度很慢原因每次请求都重新加载一次joblib模型文件没有把模型对象放到全局变量或缓存中。模型文件虽然不大但在高并发场景下重复读取磁盘会影响响应时间。解决模型加载放到模块的顶层作用域在应用启动时加载一次后续请求直接复用。测试时直接把模型对象定义为全局变量然后循环调用100次对比响应时间你会看到显著的差距。6. 结果验证与进阶交叉验证和SHAP值让模型不再黑匣子模型选型和接口封装都完成后还有一步很多人会忽略——用交叉验证和特征重要性解释把结论夯实。单次train_test_split的结果具有随机性可能是某一组划分恰好运气好。我通常会在项目收尾阶段用5折分层交叉验证重新评估模型得到每折AUC的均值和标准差这样写进报告里的数字才足够硬气。from sklearn.model_selection import StratifiedKFold, cross_val_score cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) cv_scores cross_val_score(best_model, X, y, cvcv, scoringroc_auc) print(f5折AUC: {cv_scores.mean():.4f} ± {cv_scores.std():.4f})交叉验证之后再用SHAP值分析每个特征对预测结果的贡献方向。SHAP能告诉我们是thalach升高会降低风险还是oldpeak升高会显著提升风险这类结论比一个裸的AUC数字更有说服力尤其适合写进毕业设计的论文里。import shap explainer shap.TreeExplainer(best_model.named_steps[clf]) X_processed best_model.named_steps[pre].transform(X_test) shap_values explainer.shap_values(X_processed) shap.summary_plot(shap_values, X_processed, feature_namesfeature_order)SHAP的summary plot能按重要度排序展示所有特征的影响方向。我在这个数据集上跑出来的结果thalach最大心率和oldpeakST段压低通常排在前两位这跟心内科的临床常识一致——运动负荷试验中的ST段压低幅度是判断心肌缺血的核心指标之一。从那以后我每次做完一个数据分析项目都会强制走一遍交叉验证加SHAP解释的流程即使时间紧也要跑一个5折的AUC均值。原因很简单单次划分的测试集结果没法说服别人但交叉验证的均值和可视化解释能。如果你照着这份资源复现建议也把这个习惯保留下来你的系统会多一份可信度希望帮到你。本文还有配套的精品资源点击获取