ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

决策树分类实验(乳腺癌)WPBC数据清洗与参数调优全流程

2026/10/6 2:57:29 拓冰建站 浏览量
决策树分类实验(乳腺癌)WPBC数据清洗与参数调优全流程 简介一份面向机器学习初学者与医学数据分析场景的决策树分类实验素材基于 Wisconsin 乳腺癌数据集含 wpbc/wdbc 等版本实现肿瘤良恶性预测目标是帮助读者快速掌握决策树建模、评估与剪枝调优的完整流程。包内共 13 个文件整体约 572KB其中 .data 与 .names 为原始数据集及字段说明.csv 为整理后的实验数据.m 是 MATLAB 实现脚本.txt 为运行说明.png 则直观展示了模型评价良好时、剪枝前后的效果对比。实战价值方面资源覆盖数据预处理、训练集/测试集划分、决策树构建可对应 ID3/C4.5/CART 思路、准确率/召回率/F1 等指标评估以及剪枝前后过拟合对比便于按步骤复现并迁移到其他医疗诊断问题上。目前已有 1547 人学习下载尤其适合需要结合具体医学数据理解决策树原理、完成分类实验或课程设计的高校学生与入门分析师。1. 决策树分类实验乳腺癌先看清你手里这份 WPBC 是什么拿到这个 zip 包时大多数人会下意识以为里面是“乳腺癌良恶性诊断”的经典入门程序跑起来才发现标签是 R/N 而不是 M/B——这是完全不同的任务。这个决策树分类实验乳腺癌用的是 WPBC 数据集全称 Wisconsin Prognostic Breast Cancer来自 UCI 的预后数据集任务是预测患者术后是否会复发属于二分类问题。它和互联网上更常见的 WBCD 诊断数据集最大的差别是样本量小、类别不平衡、特征里有缺失值恰好是检验决策树基本功的好素材。这篇文章会把数据清洗、决策树分类程序实现、参数调优和踩坑点一条线讲完让新手能直接复现也让做过分类实验的熟手看到 WPBC 和常规 toy dataset 之间那些容易翻车的边界。2. WPBC 数据集与选型乳腺癌复发预测为什么适合决策树2.1 WPBC 和 WBCD 的差别同样是乳腺癌数据任务完全不同先明确一个经常被搞混的前提。UCI 上有两个名字接近的乳腺癌数据集WBCDWisconsin Breast Cancer Database和 WPBCWisconsin Prognostic Breast Cancer。WBCD 是诊断数据集标签是 Malignant/Benign要回答“这个肿瘤是良性还是恶性”WPBC 是预后数据集标签是 Recur/Non-recur要回答“患者术后一段时间内会不会复发”。这个 zipper 标题里明确写了 wpbc_dataset所以中间代码里如果复现者按 WBCD 的 569 条样本、32 列特征去套第一步就会对不上。WPBC 的公开档案记录是 198 条样本每条样本包含 ID、随访结果、随访时间以及一组细胞核形态测量特征。拿到手的数据通常是wpbc.data这种没有表头的 CSV 风格文本里面用逗号分隔缺失值用问号表示。因为样本只有 198 条比 WBCD 少了近三分之二所以训练时对“过拟合”和“评估稳定性”的敏感度会明显更高。维度WBCD诊断WPBC预后任务肿瘤良恶性二分类术后复发/未复发二分类样本量569198标签M / BR / N特征性质细胞核形态均值/最差细胞核形态 随访时间常见坑类别基本平衡坑在特征相关性小样本、不平衡、缺失值WPBC 里“随访时间”这一列很特殊。它表示从确诊到复发或末次随访的月数。它和标签有一定隐含关系但建模时不能把它当成普通特征丢进决策树——原因放在后面避坑章节细说。先把数据集的角色定位搞清楚这是一个复发预测任务不是一个诊断任务。2.2 决策树在这个任务里的三个选型理由为什么这个实验要选决策树而不是上来就逻辑回归、SVM有几个实际理由值得先说透。第一特征量纲差异极大。细胞核形态特征里有的特征是面积取值可以到几百上千有的特征是光滑度或分形维数取值在小数点后两三位。决策树做分裂时只比较特征值大小关系不依赖距离度量不需要做标准化或归一化。这对 WPBC 这种没经过统一 scale 的原始数据非常友好。第二小样本上可解释性就是生产力。198 条样本任何复杂模型都容易过拟合而决策树训练完可以画成树、导出文本规则直接放进实验报告或交给非技术背景的人评审。如果这个程序是课程实验或课题产出能一眼看懂“为什么被判为复发”比拿到一个黑匣子精确度更有说服力。第三细胞核特征与复发结果之间不是单纯的线性关系。比如某个特征单独看区分度低但和另一个特征组合后能划出清晰区域。决策树在构建时天然做多级条件组合相当于自动做了非线性特征交互不需要手工构造交叉项。2.3 标签分布与评估指标的起点先别急着算准确率踩过小样本数据坑的人都知道拿到数据第一件事不是训练是看标签分布。WPBC 的复发表签只占一小部分多数样本是未复发属于典型的不平衡二分类。在这种分布下直接看 accuracy 会被多数类带偏。举个极端情况模型把所有样本都判成不复发准确率也能有七成以上但临床上完全没有用。所以从实验一开始就要约定评估口径不仅看准确率还要看召回率、F1-score 和 AUC。咱们在划分数据集时也会用分层抽样这个词后面代码里会实现。2.4 实验环境的常见做法做这个实验最常见的方案是 Python pandas scikit-learn三个库覆盖数据处理、决策树训练和评估全流程。如果有可视化需求可以加 matplotlib 和 graphviz 相关工具包。scikit-learn 里DecisionTreeClassifier实现的是 CART 树支持gini和entropy两种分裂准则函数接口稳定适合复现也适合扩展。提示如果是为了复现课堂实验不建议自己从零写树的递归分裂代码优先用 sklearn 的成熟实现把精力放在数据处理和参数验证上。3. 用 pandas 清洗 WPBC从 zip 里的 raw 数据到可训练样本3.1 解压并读取wpbc.data文件没表头列名自己定zip 解压后一般会得到wpbc.data、wpbc.names、wpbc.test这样的文件组合。wpbc.data是主数据文件没有表头所有行以逗号分隔缺失值用?表示。读取时需要用headerNone并且手工指定列名。import pandas as pd import numpy as np # 假设列结构id, outcome, time, 后面是31个细胞核形态特征 columns [id, outcome, time] [ffeat_{i:02d} for i in range(31)] df pd.read_csv( wpbc.data, headerNone, namescolumns, na_values?, ) print(df.shape) print(df.head())代码逻辑说明headerNone告诉 pandas 文件里没有列名行names参数手动传入列名列表。这里没有硬背原始数据的特征名因为复现实验时特征列数可能有出入用feat_01这种编号命名通用性更强后面查看特征重要性时也不会乱。na_values?是关键一步把数据里的问号统一转换成 pandas 的NaN否则后面做数值计算时会报错或把缺失值当字符串处理。参数说明df.shape输出应该是(198, 34)左右如果你读出来的行数不是 198先检查是不是分隔符或者文件编码问题不要急着往下走。3.2 标签编码R/N 转为 0/1决策树分类器要求标签是数值类型。outcome列里R表示复发N表示未复发。这里不能用LabelEncoder无脑转否则字母顺序会把N编成 1、R编成 0后面报告指标时语义容易搞反。更稳妥的做法是显式映射。# 显式映射保证 1 表示复发(R)0 表示未复发(N) df[outcome] df[outcome].map({N: 0, R: 1}) # 检查映射后的标签分布 print(df[outcome].value_counts()) print(df[outcome].value_counts(normalizeTrue))代码逻辑说明map是逐值替换比replace更直观也方便一眼看出映射关系。输出类别分布的主要目的是确认不平衡程度如果1的比例低于 30%后面训练时就要考虑类别权重。3.3 缺失值填充median 是比 mean 更稳的默认选择WPBC 的特征列里存在缺失值。样本量只有 198 条不建议直接 drop 行否则本来就少的复发表本会更稀缺。常见做法是用中位数填充因为细胞核特征通常有偏态分布均值容易被极端值拉偏中位数更稳健。# 查看每列缺失数量 print(df.isna().sum().sort_values(ascendingFalse).head()) # 用中位数填充所有缺失值 df df.fillna(df.median()) # 再次检查 print(df.isna().sum().sum())代码逻辑说明df.isna().sum()可以快速定位哪些列有缺失、缺多少。fillna(df.median())是整表操作每一列的缺失值都填成该列中位数。如果某些列缺失比例特别高比如超过 30%就需要考虑是不是读取时列名错位导致不要直接填完不管。参数说明这一步是数据预处理里最容易偷懒的地方但也是后面所有结果的地基。填充完后数据里不应该再有任何NaN否则 sklearn 会直接报“Input contains NaN”错误。3.4 划分训练集用 stratify 保住少数类198 条样本本来就不多如果把复发样本全分到测试集训练集里就没有正样本了。因此划分时一定要用stratifyy让训练集和测试集的类别比例和全量数据保持一致。from sklearn.model_selection import train_test_split # 丢弃 id 和 time 列理由在避坑章节展开 X df.drop(columns[id, time]) y df[outcome] # 分层抽样测试集占 25% X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42, stratifyy, ) print(训练集正样本比例:, y_train.mean()) print(测试集正样本比例:, y_test.mean())代码逻辑说明drop(columns[id, time])把与细胞核形态无关的两列移除。id只是编号time是随访时长两者都不应该参与分类决策。stratifyy让划分时按 y 的类别比例抽样打印出的两个正样本比例应该非常接近。参数说明test_size0.25是常规选择但 WPBC 总共才 198 条测试集会只有约 50 条。如果实验报告需要更稳定的指标可以在后面改成交叉验证。random_state42固定随机种子保证每次运行结果一致这一点在复现别人的实验时尤其重要。提示如果你看到的wpbc.data里列数和这里不一致以解压后的wpbc.names文件描述为准调整 columns 列表即可。4. 决策树分类程序实现scikit-learn 建树与三个必调参数4.1 最小可跑分类器15 行代码先出一个 baseline数据清洗完成后决策树分类程序的主干就简单了。通过DecisionTreeClassifier定义模型、fit训练、score评估是 sklearn 的标准三连。from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import ( accuracy_score, classification_report, confusion_matrix, ) # 先不调参数跑一个 baseline clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(测试集准确率:, accuracy_score(y_test, y_pred)) print(混淆矩阵:\n, confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[Non-recur, Recur], zero_division0))代码逻辑说明这一步的意义是拿到一个未调参的基线。target_names里的顺序对应标签 0 和 1方便看每个类别的 precision/recall/f1。zero_division0防止某个类别没有预测出来时 sklearn 报警告。参数说明random_state42保证树的结构可复现。但基线跑完大概率能发现测试集准确率可能还行但复发类的召回率很低甚至全部被吞掉。这是类别不平衡导致的下一小节直接进入调参。4.2 三个必调参数criterion、max_depth、min_samples_leaf决策树训练真正需要花时间的地方是这三个参数。criterion是分裂准则可选gini或entropy。两者在绝大多数场景下结果差异很小但entropy对多类别分支更敏感信息增益的解释也更直观。在 WPBC 这种二分类小样本上以entropy为准同时跑一次对比就可以不必纠结。max_depth是树的最大深度不设置时 sklearn 会一直扩展直到所有叶节点纯净这在小样本上几乎必然过拟合。WPBC 特征多、样本少深度控制到 4 到 6 是常见范围具体值用验证集或交叉验证来选。min_samples_leaf是叶节点最少包含的样本数。设置成 5意味着每个叶节点至少有 5 个样本相当于强制剪枝对抑制噪声和类别不平衡都有帮助。clf DecisionTreeClassifier( criterionentropy, max_depth5, min_samples_leaf5, random_state42, ) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(训练集准确率:, accuracy_score(y_train, clf.predict(X_train))) print(测试集准确率:, accuracy_score(y_test, y_pred)) print(混淆矩阵:\n, confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[Non-recur, Recur], zero_division0))代码逻辑说明训练集和测试集准确率都打印出来是为了对比过拟合程度。如果训练集接近 100%、测试集还在 70% 以下说明深度仍然过大或叶子过细。max_depth5配合min_samples_leaf5是相对保守的组合适合 WPBC 这种 100 多条训练样本的数据。调参后重点看 Recur 类的 recall这个指标反映“真正复发的患者里模型抓住了多少”。参数说明class_weight参数可以继续加比如class_weightbalanced让 sklearn 根据类别频率自动给少数类更高权重。加了之后准确率有可能略降但 Recur 类的 recall 通常会提升——在小样本、不平衡数据上这是比无脑拉参数更直接的干预手段。4.3 画树与特征重要性让结果不再是一个黑匣子决策树最大的卖点是可解释性。sklearn 提供了export_text直接把树结构打印成纯文本规则完全不依赖 graphviz 系统库。另一个实用输出是feature_importances_能告诉你模型实际靠哪些特征分裂。from sklearn.tree import export_text # 纯文本输出树结构最不容易出环境问题的方法 tree_rules export_text( clf, feature_nameslist(X.columns), max_depth5, ) print(tree_rules) # 特征重要性排序 importance pd.Series(clf.feature_importances_, indexX.columns) importance importance.sort_values(ascendingFalse) print(importance.head(10))代码逻辑说明export_text输出的形式是一组缩进条件比如feat_05 0.012配合 class 比例和样本数能直接放到实验报告里当规则证据。feature_importances_的含义是每个特征在整棵树分裂中带来的不纯度减少总量越靠前说明对分类的贡献越大。参数说明max_depth5限制导出树规则的长度防止输出过长。如果某个特征名排在第一位且是feat_xx回到原始列名表里找出它对应的是哪个测量量。正常来说排名靠前的应该是和细胞核形态相关的特征比如面积或周长方向。提示如果export_tree想要图形格式可以用plot_tree它不需要系统安装 graphviz直接基于 matplotlib 渲染适合放 PPT。5. WPBC 决策树训练避坑4 个翻车现场与排查办法5.1 现象训练集准确率 100%测试集掉到 60% 出头这是决策树实验里最经典的翻车现场尤其是 WPBC 这种只有 198 条样本的数据。原因很简单不限制max_depth时DecisionTreeClassifier默认生长到所有叶节点完全纯净树把训练样本的每个角落都记住了泛化能力被牺牲干净。你在屏幕前看到训练集满分时不要高兴那不是模型强是树在背答案。解决设置max_depth在 4 到 6并且加min_samples_leaf。如果带参数的版本在验证集上仍然波动大继续用后面的交叉验证来选参数而不是手动一点点碰运气。另外也可以试试ccp_alpha剪枝它是 sklearn 里代价复杂度剪枝的实现能自动剪掉对泛化贡献小的子树。5.2 现象准确率报告有 80%但 Recur 类一个都没召回这类问题在小样本不平衡数据上特别隐蔽。WPBC 里未复发样本占大头如果只看 accuracy模型只要把所有样本都判成 N 就能拿一个不错的分数。但分类报告里 Recur 类的 recall 是 0说明这个分类程序在临床上毫无价值——所有真正复发的患者都会被漏掉。原因有两层。第一是数据本身不平衡第二是默认的决策树分裂规则没有对少数类做补偿。解决方式是加class_weightbalanced让少数类的分裂错误代价更高。同时评估指标从 accuracy 切换到混淆矩阵、F1-score 和 AUC并且明确说出自己要优化的指标是 Recur 类的 recall 还是整体 F1。5.3 现象Time 字段被当成特征后“复发时间”成了最重要特征第一次做 WPBC 很容易顺手把time列一起喂给模型因为它也是数值。但time是随访时长和标签存在时间维度的关联比如随访满 5 年还没复发的人大概率会被标为 N。把它加进特征树会过度依赖这个时间信息而真实应用中预测时根本拿不到患者未来的随访时间这就构成了数据泄露。解决在划分 X 和 y 之前就把time和id一起 drop 掉和第三节代码保持一致。如果是做数据探索可以把time单独做 Kaplan-Meier 之类的生存分析但不要混进决策树分类器的特征矩阵。5.4 现象想画图时Graphviz 报“dot 不在 PATH 中”或者图片空白很多人在可视化决策树这一步被环境折腾到怀疑人生。sklearn.tree.export_graphviz生成的是.dot文本后续渲染依赖系统安装 graphviz 的二进制程序pip 安装pydotplus解决不了系统依赖。这不是模型问题是环境问题。解决优先用export_text做实验记录零依赖、不翻车。如果一定要图用plot_tree它直接基于 matplotlib 渲染不用 graphviz。需要高清图时给plot_tree传figsize加大画布或者用matplotlib保存成 PNG注意fontsize调小一点防止文字重叠。5.5 现象每次换 random_state指标忽高忽低结论不稳定在小样本上单次划分训练/测试集的随机性会被放大。random_state42跑一次测试准确率 80%换成 2024 就变成 65%这不是模型写错了是测试集只有约 50 条样本时换几十条样本就足以改变判定结果。评估稳定性比单次分数更重要。解决不再依赖单次划分改用StratifiedKFold做交叉验证把多次结果取均值和标准差调参过程也用交叉验证的均值得分去选参数而不是盯某一次测试集的分数。对应的代码在下一章给出。6. 交叉验证与剪枝调参让 WPBC 决策树不再靠随机种子活着6.1 用 StratifiedKFold 替换单次留出法WPBC 样本量小最有效的评估方式是把数据切成 5 折每折都当一次测试集最终报告 5 次结果的均值和标准差。from sklearn.model_selection import StratifiedKFold, cross_val_score cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score( clf, X, y, cvcv, scoringroc_auc, ) print(AUC 均值: %.3f (±%.3f) % (scores.mean(), scores.std()))代码逻辑说明StratifiedKFold保证每一折里的 R/N 比例与全量基本一致cross_val_score自动完成拟合和评估循环。用scoringroc_auc是因为 AUC 对不平衡数据比 accuracy 稳健得多能反映模型对少数类的排序能力。6.2 用小范围 GridSearchCV 锁定参数调参不必手动一个个试。网格搜索配合交叉验证是最省事、也最容易写进实验报告的方案。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 4, 5, 6], min_samples_leaf: [3, 5, 8], criterion: [gini, entropy], class_weight: [None, balanced], } grid GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cvStratifiedKFold(n_splits5, shuffleTrue, random_state42), scoringroc_auc, n_jobs-1, ) grid.fit(X, y) print(最优参数:, grid.best_params_) print(最优 AUC:, grid.best_score_)代码逻辑说明网格搜索会在参数组合空间里逐一做交叉验证并返回均值最高的一组。搜索结束后用grid.best_estimator_就可以拿最优模型继续预测和画特征重要性。要注意这里是在全量 X、y 上搜索最终报告直接用grid.best_score_不要再去全量数据上重新打分否则会虚高。6.3 一个小技巧把树规则和特征重要性直接导出进报告实验做完后把export_text的输出和特征重要性存成文本放进实验报告附录比贴一张模糊的树图更有说服力。我自己现在拿到任何小样本数据集第一件事都是先看标签分布、再决定评估口径和是否加类别权重这个习惯就是从 WPBC 上养成的。决策树看起来是机器学习里最好上手的模型但真正把它调得能上台面靠的不是玄学而是把数据泄露、评估偏差和随机波动一条条堵死。希望今天的这几条踩坑记录对你有帮助。本文还有配套的精品资源点击获取