简介:这份资源是面向计算机、数学、电子信息等专业学生的数据科学课程设计参考资料,以UCI Heart Disease数据集为核心,完整呈现心脏病数据分析项目的全流程,适合作为期末大作业、毕设或算法练习的借鉴模板。压缩包共70个文件,约23.22MB,包含4个Python源码文件、2份CSV数据集、1份PDF分析报告、1份PPT展示文稿,以及53张PNG与8张JPG图表,覆盖数据预处理、可视化、建模与模型选择等环节。代码部分按预处理、可视化、建模、模型选择分模块组织,报告与PPT可直接用于成果展示,图片素材则包含混淆矩阵、ROC曲线、特征重要性分布等关键分析结果。目前已有187人学习下载,读者可据此掌握从数据清洗到模型评估的完整分析思路,并对照示例图表理解分类模型的表现评价方法,快速搭建起结构清晰、内容完整的数据分析项目框架。
1. 心脏病数据分析项目到底在分析什么:从 UCI 数据集到可复现结论
拿到「基于UCI Heart Disease数据集的心脏病分析python源码+数据集+介绍PPT+分析报告+示例图片.zip」这类资源包,很多人第一反应是解压、打开 notebook、从头跑到尾,然后看着准确率数字发个朋友圈就结束了。但真正做过医疗数据分析的人知道,这个数据集的价值不在「跑出一个模型」,而在于它是一套完整的、可复现的分析链路:从字段含义理解、缺失值处理、特征工程,到模型对比、结果解释、报告输出。UCI Heart Disease 数据集(常被称为 Cleveland Heart Disease 数据集)包含 14 个核心字段,303 条样本(Cleveland 子集),目标变量是是否患有心脏病。它小、干净、字段语义清晰,是入门结构化医疗数据分析最合适的练手材料之一。这篇文章面向的是想把这个资源包真正用起来的人——不只是跑通代码,而是理解每一步为什么这么做、参数怎么调、哪里容易翻车,最终能自己独立完成一份可信的分析报告。
2. 先搞清楚数据长什么样:字段含义与加载后的第一轮体检
2.1 14 个字段的医学含义与类型划分
UCI Heart Disease 数据集的字段设计非常经典,理解每个字段的医学背景是后续特征工程的前提。常见做法是先做一张字段说明表,把类型、取值范围、缺失情况和医学含义对齐。
| 字段名 | 含义 | 类型 | 常见取值/范围 | 缺失情况 |
|---|---|---|---|---|
| age | 年龄 | 数值 | 29–77 | 无 |
| sex | 性别 | 类别 | 1=男, 0=女 | 无 |
| cp | 胸痛类型 | 类别 | 1–4 | 无 |
| trestbps | 静息血压 | 数值 | 94–200 | 无 |
| chol | 血清胆固醇 | 数值 | 126–564 | 无 |
| fbs | 空腹血糖>120mg/dl | 类别 | 1=是, 0=否 | 无 |
| restecg | 静息心电图结果 | 类别 | 0–2 | 无 |
| thalach | 最大心率 | 数值 | 71–202 | 无 |
| exang | 运动诱发心绞痛 | 类别 | 1=是, 0=否 | 无 |
| oldpeak | 运动相对静息ST段下降 | 数值 | 0–6.2 | 无 |
| slope | ST段斜率 | 类别 | 1–3 | 无 |
| ca | 主要血管数 | 数值/类别 | 0–3 | 有少量缺失 |
| thal | 地中海贫血类型 | 类别 | 3,6,7 | 有少量缺失 |
| target/num | 是否患病 | 类别 | 0=无, 1–4=有 | 无 |
Cleveland 子集里 ca 和 thal 有少量缺失值,这是第一个需要处理的地方。很多教程直接 dropna 了事,但 303 条样本丢几条就少几个百分点,更稳妥的做法是单独分析缺失比例再决定填充策略。
2.2 用 pandas 加载并做第一轮体检
拿到 csv 后不要急着画图,先做一轮结构化体检:形状、类型、缺失、分布、重复值。
import pandas as pd import numpy as np # 加载数据,注意原始文件可能是空格分隔或逗号分隔 df = pd.read_csv("heart.csv") # 第一轮体检:形状、类型、缺失 print("shape:", df.shape) print("\ndtypes:\n", df.dtypes) print("\nmissing:\n", df.isnull().sum()) print("\nduplicated:", df.duplicated().sum()) # 目标变量分布,确认是否类别不平衡 print("\ntarget distribution:\n", df["target"].value_counts(normalize=True)) # 数值字段描述统计,重点看范围和异常 print("\ndescribe:\n", df.describe().T[["min", "max", "mean", "std"]])这段代码做了四件事:确认样本量和字段数、检查类型是否正确(有些版本 target 是 0–4 多分类,需要先二值化)、统计缺失和重复、看目标分布是否均衡。如果 target 是多分类(0–4),需要先转成二分类:
# 如果 target 是 0-4,转成二分类:0 为无病,1-4 为有病 if df["target"].nunique() > 2: df["target"] = (df["target"] > 0).astype(int)参数说明:normalize=True输出比例而非计数,方便判断类别是否均衡;describe().T转置后按字段看更直观。这一步看起来简单,但很多人跳过,后面模型效果差的时候回头查才发现是 target 编码搞错了。
2.3 缺失值处理:为什么不能无脑 dropna
ca 和 thal 的缺失比例通常在 1%–2% 左右。直接 dropna 会丢掉几条样本,在 303 条的数据集上影响不大,但如果后续要做交叉验证,样本越少方差越大。我一般会先看缺失是否随机:
# 查看缺失行的分布,判断是否随机缺失 missing_rows = df[df.isnull().any(axis=1)] print("缺失样本数:", len(missing_rows)) print(missing_rows[["age", "sex", "target"]].describe()) # 对类别字段用众数填充,数值字段用中位数填充 df["ca"] = df["ca"].fillna(df["ca"].mode()[0]) df["thal"] = df["thal"].fillna(df["thal"].mode()[0]) # 再次确认无缺失 assert df.isnull().sum().sum() == 0, "仍有缺失值"逻辑说明:ca 和 thal 都是类别属性,用众数填充比均值更合理,因为它们的取值是离散的整数编码,均值填充会产生 1.5 这种无意义的值。填充后加一个 assert 做防御性检查,避免后续建模时才发现问题。
3. 特征工程与可视化:让报告有说服力的关键步骤
3.1 连续变量分箱与类别变量编码
原始字段里 age、trestbps、chol、thalach、oldpeak 是连续数值,直接扔进模型不是不行,但做分析报告时,分箱后的分组对比更有解释力。常见做法是把年龄按十年分段,血压和胆固醇按临床常用阈值分段。
# 年龄分箱:30以下、30-40、40-50、50-60、60以上 df["age_group"] = pd.cut(df["age"], bins=[0, 30, 40, 50, 60, 100], labels=["<30", "30-40", "40-50", "50-60", "60+"]) # 血压分箱:按临床常用阈值 120/140 df["bp_group"] = pd.cut(df["trestbps"], bins=[0, 120, 140, 200], labels=["normal", "elevated", "high"]) # 类别变量做 one-hot,注意 drop_first 避免共线性 cat_cols = ["cp", "restecg", "slope", "thal", "age_group", "bp_group"] df_encoded = pd.get_dummies(df, columns=cat_cols, drop_first=True) print("编码后字段数:", df_encoded.shape[1])参数说明:pd.cut的 bins 是左开右闭区间,labels 长度要比 bins 少 1;drop_first=True在独热编码时丢掉第一个类别作为基准,避免虚拟变量陷阱。如果后续用树模型,其实不需要 drop_first,但做逻辑回归时必须处理。
3.2 用分组统计和图表支撑分析结论
分析报告里最有说服力的不是模型准确率,而是分组对比。比如「患病组最大心率均值明显低于非患病组」这种结论,比「模型准确率 85%」更能让非技术读者理解。
import matplotlib.pyplot as plt import seaborn as sns # 分组对比:患病 vs 非患病在关键数值字段上的差异 key_numeric = ["age", "trestbps", "chol", "thalach", "oldpeak"] group_stats = df.groupby("target")[key_numeric].mean().T group_stats.columns = ["no_disease", "disease"] group_stats["diff_pct"] = ((group_stats["disease"] - group_stats["no_disease"]) / group_stats["no_disease"] * 100).round(1) print(group_stats) # 画一张相关性热力图,看字段间关系 plt.figure(figsize=(10, 8)) sns.heatmap(df[key_numeric + ["target"]].corr(), annot=True, fmt=".2f", cmap="RdBu_r", center=0) plt.title("Key Numeric Features Correlation") plt.tight_layout() plt.savefig("correlation_heatmap.png", dpi=150)逻辑说明:groupby("target").mean()按是否患病分组求均值,转置后每个字段一行,方便对比;diff_pct计算两组差异百分比,绝对值大的字段通常是强区分特征。热力图用RdBu_r发散色系,中心为 0,正相关偏红、负相关偏蓝,比默认色系更容易看出方向。保存图片时 dpi 设 150 以上,放进 PPT 不会糊。
3.3 特征筛选:不是字段越多越好
编码后字段数会膨胀到 20 多个,但 303 条样本下字段太多容易过拟合。常见做法是用卡方检验或互信息做一轮筛选,保留 Top 10–15 个字段。
from sklearn.feature_selection import chi2, SelectKBest # 分离特征和目标 X = df_encoded.drop("target", axis=1) y = df_encoded["target"] # 卡方检验筛选,注意只能用于非负特征 X_nonneg = X - X.min() # 平移保证非负 selector = SelectKBest(chi2, k=12) X_selected = selector.fit_transform(X_nonneg, y) # 输出被选中的字段名 selected_mask = selector.get_support() selected_features = X.columns[selected_mask].tolist() print("选中字段:", selected_features)参数说明:k=12是保留字段数,可以按样本量调整,一般控制在样本量的 1/20 以内;卡方检验要求特征非负,所以先做平移。如果不想平移,可以改用mutual_info_classif,它对负值不敏感。筛选后建议保存一份字段列表,后续建模统一用这套字段,避免每次跑结果不一致。
4. 建模与评估:从逻辑回归到树模型的最小对比框架
4.1 为什么先跑逻辑回归再跑树模型
逻辑回归在这个数据集上通常能到 80%–85% 的准确率,且系数可解释,适合写进报告。树模型(随机森林、XGBoost)可能高 2–3 个百分点,但解释性差一些。我一般两个都跑,报告里以逻辑回归为主、树模型做对比,这样既有可解释性又有性能上限的参考。
from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score # 划分训练测试集,stratify 保证类别比例一致 X_train, X_test, y_train, y_test = train_test_split( X_selected, y, test_size=0.2, random_state=42, stratify=y) # 逻辑回归需要标准化 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 逻辑回归 lr = LogisticRegression(max_iter=1000, C=1.0, random_state=42) lr.fit(X_train_scaled, y_train) lr_pred = lr.predict(X_test_scaled) lr_prob = lr.predict_proba(X_test_scaled)[:, 1] # 随机森林 rf = RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42) rf.fit(X_train, y_train) rf_pred = rf.predict(X_test) rf_prob = rf.predict_proba(X_test)[:, 1] # 对比输出 for name, pred, prob in [("LR", lr_pred, lr_prob), ("RF", rf_pred, rf_prob)]: print(f"\n=== {name} ===") print(classification_report(y_test, pred)) print("AUC:", round(roc_auc_score(y_test, prob), 3))参数说明:stratify=y保证训练测试集里患病比例一致,小数据集必须加;max_iter=1000防止逻辑回归不收敛;C=1.0是正则强度倒数,调小可以增强正则;随机森林max_depth=5限制深度防过拟合,n_estimators=200在 303 条数据上足够稳定。评估不能只看准确率,classification_report给出精确率、召回率、F1,AUC 衡量排序能力,医疗场景下召回率往往比精确率更重要。
4.2 交叉验证:小数据集不能只信一次划分
303 条样本下,单次 train_test_split 的结果波动可能有好几个百分点。必须做交叉验证才能得到稳定估计。
from sklearn.model_selection import StratifiedKFold # 5 折分层交叉验证 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) lr_cv = cross_val_score(lr, scaler.fit_transform(X_selected), y, cv=cv, scoring="roc_auc") rf_cv = cross_val_score(rf, X_selected, y, cv=cv, scoring="roc_auc") print("LR AUC: %.3f (+/- %.3f)" % (lr_cv.mean(), lr_cv.std())) print("RF AUC: %.3f (+/- %.3f)" % (rf_cv.mean(), rf_cv.std()))逻辑说明:StratifiedKFold每折都保持类别比例,比普通 KFold 更适合分类问题;scoring="roc_auc"用 AUC 而非准确率,因为 AUC 对阈值不敏感。输出均值加减标准差,标准差超过 0.05 说明模型不稳定,需要检查特征或增加正则。注意逻辑回归在交叉验证里也要先标准化,这里用fit_transform在每折内部做,避免数据泄漏。
4.3 特征重要性:报告里最值得写的一段
无论用逻辑回归还是随机森林,都可以输出特征重要性,这是分析报告的核心内容之一。
# 随机森林特征重要性 import pandas as pd feat_imp = pd.Series(rf.feature_importances_, index=X_selected.columns if hasattr(X_selected, "columns") else [f"f{i}" for i in range(X_selected.shape[1])]) feat_imp = feat_imp.sort_values(ascending=False) print(feat_imp.head(10)) # 逻辑回归系数(标准化后可比) coef = pd.Series(lr.coef_[0], index=feat_imp.index[:len(lr.coef_[0])]) print("\nLR coefficients:\n", coef.sort_values(key=abs, ascending=False).head(10))参数说明:随机森林的feature_importances_是基尼重要性,偏向高基数特征;逻辑回归系数在标准化后可以直接比较绝对值大小,正负号表示方向。两个结果对照看,如果都指向同一批字段(比如 cp、thalach、oldpeak、ca),那这些字段的结论就比较可靠。报告里可以写「最大心率每降低一个标准差,患病风险显著上升」这类可解释的结论。
5. 避坑与排查:这个数据集上最容易翻车的 5 个地方
5.1 坑一:target 编码搞错导致模型学反
现象:模型准确率异常高(95%+)或异常低(50% 左右),混淆矩阵里预测全是一类。原因:不同来源的 heart.csv 里 target 编码不一致,有的 0 表示患病、1 表示健康,有的反过来;有的 target 是 0–4 多分类没转二分类。解决:加载后第一件事就是打印df["target"].value_counts()和字段说明对照,确认 0/1 含义,多分类先二值化。我一般会在代码开头写死注释说明编码方向,避免后面自己都忘了。
5.2 坑二:标准化在划分之前做导致数据泄漏
现象:交叉验证 AUC 比测试集高很多,或者测试集效果虚高。原因:先对全量数据做 StandardScaler 再划分,测试集的均值和方差信息泄漏到了训练过程。解决:标准化必须在训练集上 fit、在测试集上 transform,交叉验证时用 Pipeline 把标准化和模型串起来。
from sklearn.pipeline import Pipeline pipe = Pipeline([("scaler", StandardScaler()), ("lr", LogisticRegression(max_iter=1000))]) scores = cross_val_score(pipe, X_selected, y, cv=cv, scoring="roc_auc")5.3 坑三:把 ca 和 thal 当连续变量直接回归
现象:逻辑回归系数解释不通,或者树模型重要性排序奇怪。原因:ca(主要血管数)和 thal(地中海贫血类型)虽然用数字编码,但本质是类别,直接当数值会引入虚假的序关系。解决:做 one-hot 编码,或者至少在报告里说明它们是类别字段。如果字段取值少(ca 只有 0–3),one-hot 后维度增加不多,值得做。
5.4 坑四:忽略类别不平衡直接看准确率
现象:准确率 85% 看起来不错,但召回率只有 60%,漏掉了很多患病样本。原因:数据集中患病和健康比例大约接近 1:1,但测试集划分后可能有波动,且医疗场景下漏诊代价高。解决:评估时重点看召回率和 AUC,必要时用class_weight="balanced"让模型更关注少数类。
lr_balanced = LogisticRegression(max_iter=1000, class_weight="balanced")5.5 坑五:报告里只写准确率不写置信区间
现象:报告结论「模型准确率 85%」被质疑不可靠。原因:303 条样本下,85% 的准确率置信区间可能宽达 ±8%。解决:报告里给出交叉验证均值±标准差,或者用 bootstrap 估计置信区间。常见做法是跑 1000 次 bootstrap,每次重采样算 AUC,取 2.5% 和 97.5% 分位数作为区间。
from sklearn.utils import resample boot_aucs = [] for _ in range(1000): X_b, y_b = resample(X_selected, y, random_state=None) boot_aucs.append(cross_val_score(pipe, X_b, y_b, cv=3, scoring="roc_auc").mean()) print("95%% CI: %.3f - %.3f" % (np.percentile(boot_aucs, 2.5), np.percentile(boot_aucs, 97.5)))6. 把分析变成可交付物:报告结构、PPT 要点与复现检查清单
6.1 分析报告的推荐结构
一份能拿得出手的心脏病分析报告,我一般按这个顺序组织:数据来源与字段说明(含缺失处理)、探索性分析(分组对比 + 相关性热力图)、特征工程(编码与筛选逻辑)、模型对比(逻辑回归 vs 随机森林,含交叉验证)、特征重要性解释、结论与局限。局限部分一定要写,比如样本量小、单中心数据、不能直接用于临床诊断,这既是严谨也是保护自己。
6.2 PPT 只放四类图
介绍 PPT 不需要堆代码,放四类图就够了:字段分布对比图(患病 vs 非患病)、相关性热力图、模型 ROC 曲线对比、特征重要性排序图。每张图配一句话结论,比如「最大心率在患病组平均低 15 bpm」。示例图片建议统一用 150 dpi 以上、白底、字号不小于 12,投影时不至于看不清。
6.3 复现检查清单
跑完整个流程后,用这份清单自查一遍:target 编码是否确认;缺失值处理是否记录;标准化是否只在训练集 fit;交叉验证是否用了分层;评估是否包含召回率和 AUC;特征重要性是否两个模型对照;报告是否写了局限。这七条都过了,这份分析基本就站得住。
6.4 一个具体技巧:用 SHAP 做单样本解释
如果想让报告更有说服力,可以加一个单样本解释。SHAP 能告诉你「这个样本为什么被预测为患病」,比全局特征重要性更直观。
import shap # 用随机森林做 SHAP 解释 explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test) # 对第一个测试样本画力图 shap.initjs() shap.force_plot(explainer.expected_value[1], shap_values[1][0], X_test.iloc[0])参数说明:TreeExplainer对树模型效率高;shap_values[1]取正类(患病)的解释;force_plot展示每个字段把预测值往哪个方向推。注意 SHAP 在旧版本和新版本 API 有差异,如果报错先检查版本,常见做法是固定一个能跑通的版本写进 requirements。这个技巧我一般放在报告附录,不放正文,避免非技术读者看不懂。
最后说个血泪经验:这个数据集我前后跑过不下十次,每次翻车都不是模型本身的问题,而是数据加载和编码环节的疏忽。后来我养成了一个习惯,任何分析项目第一步先写一个check_data.py,把形状、类型、缺失、目标分布、重复值全打出来,确认无误再往下走。这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取