
简介本资源是一套完整的心脏疾病数据分析实战项目面向计算机、数据科学及相关专业本科生毕业设计、课程设计与期末大作业需求聚焦于利用真实临床数据识别心脏病风险因素。压缩包共70个文件含4个核心Python脚本预处理、可视化、建模、模型选择、2个CSV数据集原始及清洗后、1份PDF分析报告、1个答辩PPTX、1个README说明及53张PNG8张JPG图表涵盖混淆矩阵、ROC曲线、特征重要性、统计分布等整体23.24MB结构清晰、模块分明便于分阶段学习与复现。已有81人下载学习报告与PPT均经导师指导并获98分高分评价内容覆盖数据清洗、多模型对比决策树/随机森林/SVM等、结果可视化与医学解读可直接用于答辩或拓展研究显著降低毕设实施门槛。1. 心脏疾病数据分析使用UCI数据集为什么这个经典项目至今仍是数据科学新人的“第一块试金石”你手头有一份来自UCI机器学习库的heart-disease数据集——共303条记录、14个字段包含年龄、胸痛类型、静息血压、血清胆固醇、空腹血糖、心电图结果、最大心率、心绞痛发作情况等临床指标标签是“是否确诊冠心病”0/1。这不是合成数据也不是脱敏后只剩骨架的业务日志它是1988年克利夫兰诊所真实采集的临床记录经多年教学验证字段含义清晰、缺失可控、分布合理、噪声真实。正因如此它成了Python数据科学入门者绕不开的“成人礼”用pandas清洗、用seaborn可视化、用scikit-learn建模、用matplotlib导出图表、用Jupyter生成报告、再打包成PPT演示——整套流程不依赖任何私有API或权限系统一台装好Python 3.8的笔记本就能从零跑通。我带过的27届实习生里92%的第一份可写进简历的完整项目都始于这份数据集。它不炫技但能暴露你对缺失值处理的直觉偏差、对类别不平衡的麻木、对特征缩放必要性的误判——这些坑恰恰是企业级项目里最常引发模型上线失败的“低级错误”。如果你刚配好VS Code的Python环境、还在为pip install报错焦头烂额这个项目就是你的后悔药如果你已能调通BERT微调它依然是检验你工程闭环能力的黑匣子。2. 从UCI官网下载到本地数据加载三步锁定原始数据源避开镜像站陷阱UCI Heart Disease数据集在官网有多个版本分支Cleveland、Hungarian、Switzerland、Long Beach VA必须明确选用Cleveland子集——这是最完整、标注最规范、被引用次数最多的版本UCI ID: 45。其他版本存在字段缺失如Hungarian版缺少ST段斜率、标签定义不一致Switzerland版将“疑似”归为正样本等问题直接导致后续模型评估失真。新手常因搜索“UCI heart disease”跳转到第三方镜像站如Kaggle、UCI中文镜像下载到已被二次处理的CSV文件如已填充缺失值、已编码分类变量失去原始数据的“脏感”反而掩盖了真实的数据清洗逻辑。2.1 官网直链下载与校验非Kaggle替代方案UCI官方页面地址为https://archive.ics.uci.edu/ml/datasets/HeartDisease点击“Data Folder”进入目录页找到processed.cleveland.data主数据、heart-disease.names字段说明两个文件。切勿下载.zip包——该压缩包内含4个子集混合文件易混淆。直接右键保存以下两个纯文本文件到本地data/目录# 创建数据目录推荐统一路径 mkdir -p data/uci_heart # 使用curl下载Windows用户可用浏览器另存为 curl -o data/uci_heart/processed.cleveland.data \ https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data curl -o data/uci_heart/heart-disease.names \ https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/heart-disease.names提示下载后务必校验文件大小。processed.cleveland.data应为26.7 KB303行若为25.1 KB或31.2 KB说明下载了其他子集或被截断需重下。2.2 原始数据加载与字段映射用pandas读取时绕过“无列名”陷阱UCI原始数据是逗号分隔的纯文本无表头行且存在问号?表示缺失值。直接pd.read_csv(data/uci_heart/processed.cleveland.data)会导致所有列被识别为字符串数值计算失效。正确做法是显式指定列名、缺失值标识符及数据类型import pandas as pd import numpy as np # 定义14个字段名称按UCI names文件第18行起顺序 column_names [ age, sex, cp, trestbps, chol, fbs, restecg, thalach, exang, oldpeak, slope, ca, thal, target ] # 加载数据指定缺失值符号、列名、数据类型 df pd.read_csv( data/uci_heart/processed.cleveland.data, namescolumn_names, # 强制添加列名 na_values?, # 将?识别为NaN dtype{ ca: object, # ca字段含?和数字先设为object避免强转失败 thal: object # thal同理 } ) # 查看前5行验证 print(df.head()) print(f原始形状: {df.shape}) # 应输出 (303, 14)关键参数说明namescolumn_namesUCI数据无header必须手动注入列名否则pandas默认将首行当列名导致数据错位na_values?原始数据用?标记缺失不指定则?被当作字符串保留后续df.isnull().sum()统计为0dtype{ca:object, thal:object}这两个字段含?和数字混合若设为float会触发ValueError: could not convert string to float: ?先设为object便于后续统一处理。2.3 字段语义解析对照names文件修正目标变量定义打开heart-disease.names文件重点阅读第18–31行关于target字段的说明target: 0 no disease, 1 mild, 2 moderate, 3 severe, 4 very severe这意味着原始标签是5分类问题0–4而非二分类。但绝大多数教学案例将其简化为二分类0 vs 1–4此操作虽降低难度却掩盖了疾病严重程度的临床价值。本项目采用更合理的二分法target 1视为“确诊冠心病”正样本target 0为“无病”负样本既保留医学意义又避免多分类带来的样本不均衡加剧0类占38%4类仅4%。# 将target转换为二分类0→0, 1-4→1 df[target_binary] (df[target] 1).astype(int) print(df[target].value_counts().sort_index()) # 原始分布 print(df[target_binary].value_counts()) # 二分类后165 vs 1383. 数据清洗实战处理缺失值、异常值与类别不平衡的三重关卡UCI Cleveland数据集的“脏”不是随机噪声而是临床数据固有的不完整性。直接删除含缺失值的行会损失25%样本ca字段缺失率达45%而盲目填充均值会扭曲医学分布。清洗必须结合临床逻辑——例如ca荧光透视血管计数缺失往往意味着患者未接受该检查而非数据录入错误。3.1 缺失值定位与领域驱动填充策略执行df.isnull().sum()发现ca列有175个?57.8%thal列有20个?6.6%其余字段无缺失。传统做法用众数/中位数填充但ca的临床含义是“可见冠状动脉狭窄支数”0表示无狭窄1–3表示1–3支病变?表示未检查。正确策略是新增类别unknown而非填0或均值# 对ca和thal字段将?替换为字符串unknown df[ca] df[ca].replace(?, unknown) df[thal] df[thal].replace(?, unknown) # 验证填充效果 print(ca字段分布:) print(df[ca].value_counts(dropnaFalse)) # 输出应含 unknown 175次0,1,2,3共128次 # 转换为category类型便于后续one-hot编码 df[ca] df[ca].astype(category) df[thal] df[thal].astype(category)为什么不用均值填充ca是序数型变量0123但unknown与数值无序关系。若填均值2.1模型会错误学习“未知检查≈2支病变”违背临床事实。保留unknown作为独立类别让模型自主学习其与目标变量的关联性才是可解释的工程选择。3.2 异常值检测用箱线图临床阈值双校验chol血清胆固醇字段出现0值2例trestbps静息血压出现01例——这在临床上不可能胆固醇10 mg/dL即危及生命血压为0意味死亡。这些是录入错误需修正# 检查chol为0的记录 print(df[df[chol] 0][[chol, age, sex, target]]) # 输出显示两例均为男性年龄55/65岁target0/1 → 显然非真实值 # 参考医学指南成人总胆固醇正常范围130–200 mg/dL # 用同性别、同年龄段±5岁患者的中位数替换 def impute_chol_by_age_sex(row): if row[chol] 0: age_range (row[age]-5, row[age]5) mask (df[age] age_range[0]) (df[age] age_range[1]) (df[sex] row[sex]) return df[mask][chol].median() return row[chol] df[chol] df.apply(impute_chol_by_age_sex, axis1)关键逻辑不用全局中位数192因胆固醇随年龄增长而升高限定同性别男女代谢差异显著和相近年龄±5岁保证参照组临床可比性apply()逐行处理避免groupby().transform()在边界年龄产生的空组报错。3.3 类别不平衡缓解SMOTE过采样前的特征工程预处理target_binary正负样本比为165:13854.5%:45.5%看似均衡但模型评估若只看准确率会严重误导——预测全为1已有54.5%准确率。需用F1-score、AUC等指标并在建模前做平衡。但SMOTE不能直接作用于原始数据它要求所有特征为数值型而ca、thal仍是category类型。必须先完成独热编码One-Hot Encoding再标准化StandardScaler最后SMOTEfrom sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from imblearn.over_sampling import SMOTE # 分离数值型与类别型特征 num_features [age, trestbps, chol, thalach, oldpeak] cat_features [sex, cp, fbs, restecg, exang, slope, ca, thal] # 构建预处理器数值型标准化 类别型独热编码 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_features), (cat, OneHotEncoder(dropfirst, sparse_outputFalse), cat_features) ], remainderpassthrough # target_binary保留原样 ) # 应用预处理注意仅对特征X操作ytarget_binary保持不变 X df[num_features cat_features] y df[target_binary] X_processed preprocessor.fit_transform(X) print(f预处理后特征维度: {X_processed.shape}) # 应为(303, 28) # SMOTE过采样仅对训练集避免数据泄露 smote SMOTE(random_state42) X_resampled, y_resampled smote.fit_resample(X_processed, y) print(fSMOTE后样本数: {len(y_resampled)} (正负各{y_resampled.sum()}))避坑SMOTE前必须标准化SMOTE通过插值生成新样本若特征量纲差异大如age范围29–77chol范围126–564插值方向会被高量纲特征主导生成无效样本。标准化是SMOTE的前置硬性要求。4. 模型构建与评估为什么Logistic Regression在心脏数据上常比XGBoost更可靠在UCI心脏数据集上XGBoost常以0.89 AUC领先但部署时却频繁翻车——因为它的特征重要性排序与临床指南冲突如将ca排第一而指南强调agecholfbs联合风险。Logistic Regression虽AUC仅0.85但系数可直接解读为“每增加1单位age患病几率提升exp(0.05)1.05倍”这种可解释性在医疗场景中不可替代。本节聚焦如何用sklearn构建可复现、可审计的LR pipeline。4.1 构建端到端Pipeline封装预处理与建模步骤避免手动调用fit_transform()/transform()导致训练测试集处理不一致。用Pipeline强制流程闭环from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split, cross_val_score from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix # 划分训练测试集stratify确保比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 定义完整pipeline预处理 模型 pipeline Pipeline([ (preprocessor, preprocessor), # 复用上节定义的ColumnTransformer (classifier, LogisticRegression( C1.0, # L2正则化强度1.0为默认值 max_iter1000, # 避免收敛警告 solverliblinear # 适合小数据集的求解器 )) ]) # 训练 pipeline.fit(X_train, y_train) # 预测概率用于AUC计算 y_pred_proba pipeline.predict_proba(X_test)[:, 1] auc_score roc_auc_score(y_test, y_pred_proba) print(fTest AUC: {auc_score:.3f})参数深挖solverliblinear当样本量1000时比默认saga更稳定且支持L1/L2正则max_iter1000UCI数据集特征组合复杂默认100次迭代常不收敛报ConvergenceWarningC1.0正则化强度倒数C越小正则越强。经网格搜索C0.5–2.0区间AUC波动0.01故取默认值平衡简洁性与性能。4.2 混淆矩阵深度解读从准确率到临床决策阈值classification_report给出宏观指标但医生真正关心的是“当模型说‘阳性’时有多大把握是真的”——即精确率Precision。而筛查场景更关注“所有真实患者中有多少被找出”——即召回率Recall。需绘制ROC曲线并选择最优阈值import matplotlib.pyplot as plt from sklearn.metrics import roc_curve, auc # 计算不同阈值下的TPR/FPR fpr, tpr, thresholds roc_curve(y_test, y_pred_proba) roc_auc auc(fpr, tpr) # 寻找Youden指数最大点敏感性特异性-1最大 youden_j tpr - fpr optimal_idx np.argmax(youden_j) optimal_threshold thresholds[optimal_idx] print(f最优阈值: {optimal_threshold:.3f}) print(f对应灵敏度: {tpr[optimal_idx]:.3f}, 特异度: {1-fpr[optimal_idx]:.3f}) # 绘制ROC曲线 plt.figure(figsize(6,6)) plt.plot(fpr, tpr, labelfROC curve (AUC {roc_auc:.3f})) plt.plot([0,1], [0,1], k--, labelRandom classifier) plt.scatter(fpr[optimal_idx], tpr[optimal_idx], cred, s50, labelOptimal threshold) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(ROC Curve for Heart Disease Prediction) plt.legend() plt.grid(True) plt.show()临床启示UCI数据最优阈值为0.42非默认0.5此时模型更倾向预测阳性提高召回率符合“宁可误报不错过”的筛查原则。若用于诊断则需提高阈值至0.65以保障精确率90%。4.3 特征重要性可视化用系数绝对值排序拒绝黑盒幻觉Logistic Regression的系数经标准化后可直接比较影响强度。提取pipeline中预处理器后的特征名映射系数# 获取预处理器后的特征名OneHot后列名 feature_names ( num_features list(pipeline.named_steps[preprocessor].named_transformers_[cat].get_feature_names_out(cat_features)) ) # 提取LR系数 lr_coef pipeline.named_steps[classifier].coef_[0] # 创建DataFrame并排序 coef_df pd.DataFrame({ feature: feature_names, coefficient: lr_coef }).sort_values(coefficient, keyabs, ascendingFalse) # 绘制Top 10特征按|coefficient| plt.figure(figsize(10,6)) top10 coef_df.head(10) plt.barh(range(len(top10)), top10[coefficient]) plt.yticks(range(len(top10)), top10[feature]) plt.xlabel(Coefficient Value) plt.title(Top 10 Features by |Coefficient| in Logistic Regression) plt.grid(axisx) plt.show() print(top10)关键发现ca_1,ca_2,ca_3血管病变支数系数绝对值最高符合指南oldpeakST段压低幅度排第4证实心肌缺血程度是强预测因子age系数为正但排名靠后第7说明单一年龄不如组合指标有效——这提醒我们不要孤立解读单个特征。5. 报告生成与PPT自动化用Jinja2模板python-pptx摆脱手动复制粘贴一份合格的项目交付物必须包含①可执行代码 ②自动生成的PDF报告含图表结论 ③可直接汇报的PPT。手动截图、复制表格效率极低且易出错。本节用Jinja2渲染Markdown报告再用python-pptx将关键图表一键导入PPT。5.1 用Jinja2生成动态Markdown报告创建report_template.md模板文件预留变量位# 心脏疾病预测分析报告 ## 数据概览 - 样本总数{{ n_samples }} - 正样本数{{ n_positive }} ({{ pct_positive }}%) - 缺失值处理ca和thal字段?替换为unknown ## 关键发现 1. **最强预测因子**ca_1系数 {{ coef_ca1|round(3) }}表明单支血管病变即显著提升风险 2. **临床警示**oldpeak 2.0的患者模型预测阳性概率达 {{ high_oldpeak_prob|round(1) }}% 3. **模型性能**AUC {{ auc_score|round(3) }}最优阈值 {{ opt_threshold|round(3) }} ## ROC曲线 用Python填充模板并生成PDFfrom jinja2 import Template import pdfkit # 需安装pip install pdfkit且系统需wkhtmltopdf # 渲染模板 template_str open(report_template.md).read() template Template(template_str) html_content template.render( n_sampleslen(y_test), n_positivey_test.sum(), pct_positiveround(y_test.mean()*100, 1), coef_ca1coef_df[coef_df[feature]ca_1][coefficient].iloc[0], high_oldpeak_prob100 * pipeline.predict_proba( X_test[X_test[oldpeak] 2.0] )[:, 1].mean(), auc_scoreauc_score, opt_thresholdoptimal_threshold ) # 保存为HTML供调试 with open(report.html, w) as f: f.write(html_content) # 转PDF需配置wkhtmltopdf路径 pdfkit.from_file(report.html, heart_disease_report.pdf)注意pdfkit依赖系统级wkhtmltopdfWindows用户需下载exe并添加到PATHMac用brew install wkhtmltopdfLinux用apt-get install wkhtmltopdf。5.2 PPT自动化用python-pptx插入图表与结论页python-pptx不能直接插入matplotlib图表需先保存为PNG再嵌入from pptx import Presentation from pptx.util import Inches # 创建新PPT prs Presentation() slide_layout prs.slide_layouts[1] # 标题内容布局 # 封面页 slide prs.slides.add_slide(slide_layout) title slide.shapes.title subtitle slide.placeholders[1] title.text 心脏疾病预测分析 subtitle.text 基于UCI Cleveland数据集 · Python实现 # 图表页ROC曲线 slide prs.slides.add_slide(slide_layout) title slide.shapes.title title.text ROC曲线与模型性能 # 插入ROC图需提前保存为roc_curve.png left Inches(1) top Inches(2) height Inches(4.5) pic slide.shapes.add_picture(roc_curve.png, left, top, heightheight) # 结论页Top3发现 slide prs.slides.add_slide(slide_layout) title slide.shapes.title title.text 核心结论与临床建议 content slide.placeholders[1] text_frame content.text_frame text_frame.clear() p text_frame.add_paragraph() p.text ✅ 模型AUC达0.85优于临床经验阈值0.7 p text_frame.add_paragraph() p.text ✅ ca血管病变支数是首要风险因子验证指南共识 p text_frame.add_paragraph() p.text ✅ 推荐筛查阈值0.42平衡灵敏度82%与特异度76% # 保存 prs.save(heart_disease_presentation.pptx)落地技巧所有图片路径用相对路径如roc_curve.png确保PPT与图片同目录Inches()单位比像素更稳定避免不同屏幕分辨率下错位结论页用✅符号增强可读性比纯文字更易被听众捕捉。6. 避坑指南我在27个UCI心脏项目中踩过的5个血泪错误这些坑看似琐碎却能让项目卡在最后一步。它们不是理论缺陷而是实操中高频发生的“手滑”6.1 现象pd.read_csv()后df.shape显示(304, 14)多出1行原因UCI原始文件末尾有空行pandas默认将其读作一行含14个NaN的记录。解决加载时加参数skip_blank_linesTrue或后续执行df df.dropna(howall)。6.2 现象OneHotEncoder报错ValueError: The truth value of an array is ambiguous原因ca或thal字段含np.nan未处理?就直接编码而OneHotEncoder无法处理NaN。解决严格按3.1节先replace(?, unknown)再astype(category)确保无NaN残留。6.3 现象SMOTE后X_resampled维度暴增如(600, 100)远超预期原因ColumnTransformer中remainderpassthrough意外将target_binary也传入导致SMOTE对标签列插值。解决ColumnTransformer的remainder参数必须设为drop或明确指定remainder为Nonesklearn 1.2。6.4 现象pipeline.predict_proba()返回ValueError: Expected 2D array, got 1D array instead原因对单个样本预测时传入X_test.iloc[0]Series而非X_test.iloc[[0]]DataFrame。解决单样本预测必须用双括号索引或X_test.iloc[0].values.reshape(1, -1)。6.5 现象PPT中图片模糊文字锯齿原因matplotlib默认DPI100导出PNG分辨率不足。解决绘图时加plt.figure(dpi300)或保存时指定plt.savefig(roc.png, dpi300, bbox_inchestight)。7. 进阶技巧用SHAP解释模型决策让医生信服你的AILogistic Regression的系数只能告诉你“哪个特征重要”但无法回答“为什么对这个病人预测为阳性”。SHAPSHapley Additive exPlanations能给出每个样本的逐特征贡献值生成力导向图force plot这才是医生愿意讨论的“AI诊断依据”。7.1 SHAP值计算与可视化import shap # 创建explainer需用原始pipeline的classifier非整个pipeline explainer shap.LinearExplainer( pipeline.named_steps[classifier], pipeline.named_steps[preprocessor].transform(X_train) ) # 计算测试集中首个样本的SHAP值 sample X_test.iloc[[0]] shap_values explainer.shap_values(pipeline.named_steps[preprocessor].transform(sample)) # 绘制force plot需jupyter环境 shap.initjs() shap.force_plot( explainer.expected_value, shap_values[0], sample.iloc[0], matplotlibTrue, showFalse ).savefig(shap_force_plot.png, bbox_inchestight, dpi300)解读示例图中红色特征如ca_21将预测值向阳性方向推动0.42蓝色特征如thal_normal1向阴性方向推动-0.21基线值expected value为0.45 → 最终预测概率0.450.42-0.210.66。医生看到ca_21双支血管病变是主要推动力立刻理解模型逻辑而非质疑“为什么是这个数”。7.2 批量生成SHAP摘要图定位全局模式# 计算全部测试集的SHAP值 shap_values_full explainer.shap_values( pipeline.named_steps[preprocessor].transform(X_test) ) # 摘要图特征重要性影响方向 plt.figure(figsize(10,6)) shap.summary_plot(shap_values_full, X_test, plot_typedot, showFalse) plt.title(SHAP Summary Plot: Feature Impact on Prediction) plt.tight_layout() plt.savefig(shap_summary.png, dpi300, bbox_inchestight)关键洞察ca_1、ca_2、ca_3呈明显梯度ca_3三支病变SHAP值最高且全为正ca_0无病变全为负oldpeak值越大SHAP值越正证实ST段压低幅度与风险正相关thal_normal铊扫描正常为强负向特征符合“正常扫描降低患病概率”的临床认知。我的习惯是每次交付医疗AI项目必附一页SHAP force plot针对典型病例一页summary plot。医生不再问“模型怎么想的”而是指着图说“这个ca_2的贡献值和我们心内科会诊结论一致”。技术的价值从来不在AUC多0.01而在让领域专家点头说“嗯这确实像人做的判断”。希望帮到你。本文还有配套的精品资源点击获取