从零开始:Python实战UCI心脏病数据集全流程解析
当我在医学院第一次接触机器学习项目时,UCI心脏病数据集就像一位严厉的老师——它看似简单却暗藏玄机。这个经典数据集特别适合初学者理解真实世界数据的复杂性,今天我就带大家完整走一遍从数据清洗到建模准备的全过程,分享那些只有踩过坑才知道的实战经验。
1. 环境准备与数据加载
工欲善其事,必先利其器。我们先配置好Python环境,这里推荐使用Anaconda创建独立环境:
conda create -n heart_disease python=3.8 conda activate heart_disease pip install pandas numpy matplotlib seaborn scikit-learn数据集可以从UCI官网直接下载,但更便捷的方式是通过Kaggle获取预处理版本。加载数据时有个小技巧——指定na_values参数可以自动识别缺失值标记:
import pandas as pd # 注意原始数据中的?会被识别为缺失值 df = pd.read_csv('processed.cleveland.csv', na_values=['?', ' ', 'NA']) print(f"原始数据形状: {df.shape}")常见问题排查清单:
- 文件编码问题:尝试
encoding='latin1' - 路径错误:使用绝对路径或确认工作目录
- 列名不一致:检查原始数据的header参数
2. 数据探索与缺失值处理
第一次看到这个数据集时,我惊讶地发现看似完整的表格里藏着6个"数据黑洞"。特别是ca和thal两列,它们的缺失值就像心电图上的异常波动——不能简单忽略。
2.1 缺失值可视化分析
用热力图直观展示缺失值分布:
import seaborn as sns import matplotlib.pyplot as plt plt.figure(figsize=(10,6)) sns.heatmap(df.isnull(), cbar=False, cmap='viridis') plt.title('缺失值分布热力图') plt.show()缺失值统计表:
| 列名 | 缺失数量 | 缺失比例 | 处理建议 |
|---|---|---|---|
| ca | 4 | 1.32% | 众数填充 |
| thal | 2 | 0.66% | 众数填充 |
2.2 智能填充策略
比起直接删除,我更喜欢用上下文感知的填充方式。对于分类变量,采用众数填充更合理:
from sklearn.impute import SimpleImputer # 针对分类变量的填充器 cat_imputer = SimpleImputer(strategy='most_frequent') df[['ca', 'thal']] = cat_imputer.fit_transform(df[['ca', 'thal']]) # 验证填充结果 print(f"填充后缺失值统计:\n{df.isnull().sum()}")注意:虽然均值填充适用于连续变量,但对于
ca(血管数量)这种离散特征,众数填充更能保持数据分布特性。
3. 特征工程实战技巧
原始数据中的特征就像未切割的钻石,需要经过精心打磨才能展现价值。这里分享三个特征处理的关键步骤。
3.1 目标变量二值化
原始target列取值0-4,我们需要转换为二分类问题:
# 将1-4合并为1(患病),0保持为0(健康) df['target'] = df['target'].apply(lambda x: 1 if x > 0 else 0) # 检查类别分布 print(df['target'].value_counts(normalize=True))类别分布参考值:
- 健康(0):约45%
- 患病(1):约55%
3.2 分类变量编码
像cp(胸痛类型)这样的有序分类变量,直接使用原始数值可能导致模型误解其含义:
# 对有序分类变量创建映射字典 cp_mapping = { 1: 'typical angina', 2: 'atypical angina', 3: 'non-anginal pain', 4: 'asymptomatic' } df['cp_desc'] = df['cp'].map(cp_mapping) # 使用独热编码转换 df = pd.get_dummies(df, columns=['cp'], prefix='cp')3.3 连续变量标准化
年龄、胆固醇等变量的量纲差异很大,需要进行标准化:
from sklearn.preprocessing import StandardScaler cont_features = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak'] scaler = StandardScaler() df[cont_features] = scaler.fit_transform(df[cont_features])特征处理前后对比表:
| 处理类型 | 涉及特征 | 方法 | 注意事项 |
|---|---|---|---|
| 二值化 | target | 阈值转换 | 保留原始列备份 |
| 独热编码 | cp, slope | get_dummies | 避免虚拟变量陷阱 |
| 标准化 | 连续变量 | StandardScaler | 保存scaler对象 |
4. 探索性数据分析(EDA)进阶
EDA不是简单的画图,而是要像侦探一样发现数据背后的故事。以下是几个必看的分析视角。
4.1 年龄与患病率的关系
使用分段分析揭示非线性关系:
# 创建年龄分段 df['age_group'] = pd.cut(df['age'], bins=[0, 40, 50, 60, 70, 100], labels=['<40', '40-50', '50-60', '60-70', '70+']) # 计算各年龄段患病率 age_risk = df.groupby('age_group')['target'].mean() plt.bar(age_risk.index, age_risk.values) plt.title('不同年龄段患病风险') plt.ylabel('患病概率') plt.show()4.2 特征相关性分析
不要只看简单的相关系数矩阵,试试更高级的关联分析:
# 计算点二列相关(适用于二分类变量) from scipy.stats import pointbiserialr correlations = [] for col in df.select_dtypes(include=['float64', 'int64']).columns: if col != 'target': corr, _ = pointbiserialr(df[col], df['target']) correlations.append((col, abs(corr))) # 按相关性排序 sorted(correlations, key=lambda x: x[1], reverse=True)[:5]Top5相关特征:
- thal (0.52)
- ca (0.49)
- cp (0.43)
- oldpeak (0.41)
- exang (0.39)
5. 建模准备与数据分割
经过前面的步骤,我们的数据已经脱胎换骨。现在需要为机器学习模型做好最后准备。
5.1 特征选择与数据集构建
删除中间过程创建的辅助列,构建最终特征集:
X = df.drop(['target', 'age_group', 'cp_desc'], axis=1) y = df['target'] # 检查特征矩阵 print(f"最终特征形状: {X.shape}")5.2 分层抽样分割
使用分层抽样保持类别比例:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) print(f"训练集类别比例: {y_train.mean():.2f}") print(f"测试集类别比例: {y_test.mean():.2f}")提示:设置random_state保证结果可复现,这在学术研究中尤为重要
5.3 数据保存与复用
将处理好的数据保存为多个版本:
# 保存完整处理流程数据 df.to_csv('heart_disease_processed.csv', index=False) # 单独保存特征矩阵和标签 X_train.to_csv('X_train.csv', index=False) X_test.to_csv('X_test.csv', index=False) y_train.to_csv('y_train.csv', index=False) y_test.to_csv('y_test.csv', index=False)版本控制建议:
- raw_data:原始数据备份
- v1_initial_clean:基础清洗版本
- v2_feature_engineered:完整特征工程版本
- final_model_ready:建模就绪版本