ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WPBC乳腺癌数据决策树分类实战:预处理、调参与避坑指南

WPBC乳腺癌数据决策树分类实战:预处理、调参与避坑指南 简介一份基于WPBC威斯康星乳腺癌数据集的决策树分类实验包面向机器学习初学者、数据挖掘课程学员及医学数据分析人员演示如何用决策树算法完成乳腺肿瘤良恶性的分类预测。资源共13个文件包含data/csv格式的原始数据与处理结果、names/txt格式的数据字典与运行说明、m格式的MATLAB实现程序以及png格式的剪枝前后对比和模型评价图片整体仅572KB结构清晰便于按需查看。实验完整覆盖缺失值处理、特征归一化等数据预处理环节并采用ID3、C4.5或CART等常见决策树策略构建模型通过准确率、召回率、F1分数等指标评估性能同时展示剪枝对过拟合的影响与训练集/测试集划分思路有助于理解特征划分、模型调优和可解释性分析的核心思想。已有1547人学习适合需要以真实医学数据集复现分类流程、完成课程作业或系统掌握决策树机理的开发者随附的可运行程序与对比图表可直接用于实验验证。1. 用WPBC乳腺癌数据跑决策树分类实验这是一道送分题但很多人挂在预处理上很多人拿到“决策树分类实验乳腺癌.zip”时以为只要把数据塞进DecisionTreeClassifier就能交差结果一看测试集准确率只有70%。这个实验的核心并不在决策树算法本身而在于WPBC数据集威斯康星预后乳腺癌数据的预处理和参数控制。它和常见的WDBC诊断数据集不同标签是复发R与非复发N预测目标变成了“患者会不会复发”这让数据清洗、特征筛选和评估方式都跟着变了。本文按真实复现顺序讲清楚WPBC数据怎么读、缺失值怎么处理、决策树分类程序怎么写、参数怎么调以及我在小样本数据上踩过的几个坑。适合刚入门机器学习、想拿真实医学数据练手的从业者。2. 预处理WPBC数据集特征筛选与标签处理中的三个决策拿到wpbc数据集后直接跑决策树的人十有八九会在后面返工。原因很简单原始文件不是能直接喂给sklearn的形态。它既有字符串标签又有缺失值还附带着一个和标签强相关的时间列。这三个问题如果没处理干净决策树再优秀出来的也是垃圾结果。预处理不是走流程而是要把“模型能看到什么”牢牢控制在手里。2.1 先分清WPBC和WDBC乳腺癌数据里最容易被搞混的标签网上搜“乳腺癌数据集”时经常会同时搜到WPBC和WDBC两个名字。它们都来自同一批乳腺肿块细针穿刺图像但任务完全不同WDBC是诊断判断肿块是良性B还是恶性MWPBC是预后预测复发R和非复发N。我们这个实验的标题里明确写着wpbc数据集标签是R/N不是M/B。如果拿错数据集后面的模型再正确也和实验要求对不上。我一般会先用pandas把数据读出来确认前几列和标签分布import pandas as pd # 常见情况压缩包解压后是 wpbc.data没有表头 cols [id, outcome, time] [ffeature_{i} for i in range(1, 31)] df pd.read_csv(wpbc.data, headerNone, namescols) print(df.head()) print(df[outcome].value_counts())这段代码的关键在于names参数。UCI原始格式是逗号分隔但没有列名如果漏掉headerNone第一行数据会被当成表头后面所有特征列都错位。输出outcome列应该能看到R和N两个值。如果你的解压文件是带表头的csv可以直接用pd.read_csv(wpbc.csv)但需要先确认是否有id、time这些列。我只遇到过少数几个版本会把outcome写成R/N之外的形式比如1和0所以先统计一下类别分布避免后面建模时才发现标签反了。2.2 去掉ID和时间列别让标签的影子混进特征WPBC原始表里除了ID、outcome还有一个time列。很多新手以为时间和特征一样也是帮助预测的好材料这个念头非常危险。time列的含义是如果样本是R记录复发时间如果是N记录未复发的随访时间。它本身就是outcome的“结果”相当于直接把答案摆在特征里。模型只要看到time几乎不用学任何医学特征就能猜中标签。所以剔除ID和time是决策树分类实验的第一步。处理代码很短X df.drop(columns[id, outcome, time]) y df[outcome].map({R: 1, N: 0}) print(X.shape) print(y.value_counts())drop后剩下的30列特征就是细胞核半径、纹理、周长、面积等统计量。map把字符串标签转成0/1决策树分类器只接受数值标签。这里要养成一个好习惯转换后立刻用value_counts()看一眼确认N转成0、R转成1的比例没有因为映射错误而缺失。小样本数据最怕标签不均衡后面要据此决定评估指标。另外检查缺失值也要趁现在做missing X.isna().sum() print(missing[missing 0])wpbc这类真实医学数据里个别特征列出现缺失很常见。决策树库大多数不支持直接处理NaN所以需要填充或删除。我的经验是先看缺失比例如果某个特征缺失不到5%可直接删除样本如果在20%以内用中位数填充如果超过30%这个特征本身的可靠性就要打个问号。这个规则比单纯“全部删除”更稳。2.3 切分、填充与特征选择为什么必须先划分再fit在动手填充和特征选择之前必须先做训练集与测试集划分。这是医学数据分析里最容易踩的数据泄漏坑。如果先把整份数据填好、选好特征再随机切分测试集在无形中已经看到了训练集统计出的中位数、特征重要性等信息最后评估出来的准确率会虚高上线后打回原形。正确顺序是先切分再在训练集上fit填充器最后用同一个填充器transform测试集。代码这样写from sklearn.model_selection import train_test_split from sklearn.impute import SimpleImputer # 划分时用 stratify保持 R/N 在训练集和测试集中的比例一致 X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 中位数填充器只 fit 训练集 imp SimpleImputer(strategymedian) X_tr pd.DataFrame(imp.fit_transform(X_tr), columnsX_tr.columns) X_te pd.DataFrame(imp.transform(X_te), columnsX_te.columns)test_size0.2意味着198个样本里约40个进入测试集对这个小数据集来说可以接受。stratifyy是必须的如果不按标签分层极端情况下测试集可能全是N导致模型没机会展示真实能力。random_state42固定切分顺序让实验可复现。imp.fit_transform(X_tr)只学习训练集中位数imp.transform(X_te)直接套用两个矩阵各自独立测试集信息没有被污染。同样的原则也适用于特征选择。如果用SelectKBest或RFE必须在训练集上fit然后只transform测试集。这个习惯一旦养成能避开绝大多数数据泄漏引发的“虚假高准确率”。处理完的X_tr、X_te、y_tr、y_te就是后面所有实验的输入。3. 实现决策树分类程序信息增益、Gini不纯度与递归分裂很多人会用框架但说不清决策树为什么选择某个特征作为根结点。这一章先用原理把“根结点的估值”这件事讲透再带着你写一个最小可运行的决策树分类程序最后说明为什么实际项目里不手写。3.1 决策树分类器的工作原理根结点选什么特征取决于不纯度下降最大决策树分类器的核心是递归划分在每一个节点寻找一个特征和一个切分阈值让分裂后左右两个子节点的“不纯度”总和尽可能低。最常用的不纯度指标是Gini不纯度公式含义是“从节点中随机抽两个样本它们标签不一样的概率”。如果节点里全是同一类别Gini为0类别越乱Gini越高。所以根结点的估值也就是用哪个特征、在什么阈值上做第一次切分是由“谁让不纯度下降最多”决定的。比如WPBC数据里有一个特征是细胞核半径若在半径小于某个值时复发R概率明显偏低这个切分就是好切分。算法会遍历所有特征的候选阈值挑出加权Gini最小的那一组。这个过程的数学细节不复杂但写代码时很考验对数组切片的掌控。理解这一点比会调用fit更重要因为后面调max_depth、看feature_importances_时都必须回到这个逻辑。3.2 用Python手写一个最简决策树分类程序为了说明“决策树分类程序”并不神秘我给你一份能直接跑通的简化实现。它对连续特征采用二分每次分裂前遍历每个特征的所有取值作为阈值用Gini不纯度选择最优切分。import numpy as np class SimpleDecisionTree: def __init__(self, max_depth3, min_samples_leaf1): self.max_depth max_depth self.min_samples_leaf min_samples_leaf def fit(self, X, y): self.tree self._grow(X, y, depth0) def _gini(self, y): _, counts np.unique(y, return_countsTrue) p counts / counts.sum() return 1 - (p ** 2).sum() def _best_split(self, X, y): n len(y) parent_gini self._gini(y) best_feat, best_thr, best_score None, None, float(inf) for f in range(X.shape[1]): for thr in np.unique(X[:, f]): left_mask X[:, f] thr right_mask ~left_mask if left_mask.sum() self.min_samples_leaf or right_mask.sum() self.min_samples_leaf: continue score (left_mask.sum() / n) * self._gini(y[left_mask]) \ (right_mask.sum() / n) * self._gini(y[right_mask]) if score best_score: best_score score best_feat, best_thr f, thr if best_score is None or best_score parent_gini - 1e-9: return None return best_feat, best_thr def _grow(self, X, y, depth): if depth self.max_depth or len(np.unique(y)) 1: vals, counts np.unique(y, return_countsTrue) return {leaf: True, pred: vals[np.argmax(counts)]} split self._best_split(X, y) if split is None: vals, counts np.unique(y, return_countsTrue) return {leaf: True, pred: vals[np.argmax(counts)]} f, thr split left_mask X[:, f] thr return { leaf: False, feature: f, threshold: thr, left: self._grow(X[left_mask], y[left_mask], depth 1), right: self._grow(X[~left_mask], y[~left_mask], depth 1) } def _predict_one(self, row, node): if node[leaf]: return node[pred] if row[node[feature]] node[threshold]: return self._predict_one(row, node[left]) return self._predict_one(row, node[right]) def predict(self, X): return np.array([self._predict_one(row, self.tree) for row in np.asarray(X)])调用方式如下注意特征矩阵和标签都用NumPy数组# X_tr 和 y_tr 来自第 2 章预处理结果 model SimpleDecisionTree(max_depth3, min_samples_leaf4) model.fit(X_tr.values, y_tr.values.astype(int)) y_pred model.predict(X_te.values) print(自定义决策树在测试集上的准确率:, (y_pred y_te.values.astype(int)).mean())这段代码里的_best_split用双重循环暴力寻找最优切分每次分裂的复杂度是“特征数 × 特征取值数”对WPBC这种198个样本的数据完全够用。min_samples_leaf是预剪枝的关键参数它让每个叶子至少保留4个样本避免模型只因一个异常点就分裂。如果你把min_samples_leaf改成1几乎100%会把训练集背下来测试集上立刻翻车。这也是后面调参最基本的手感来源。3.3 实际决策树分类实验为什么很少手写这种程序手写版本能帮你理解算法但我不建议在正式实验里用它。最直接的原因是没有做后剪枝也没有处理特征缺失。scikit-learn的DecisionTreeClassifier底层用了CART优化算法支持预剪枝参数、代价复杂度剪枝ccp_alpha、类别权重class_weight和随机性控制这些功能自己手写要折腾很久而且容易写出隐藏bug。另一个原因是医学小样本实验里决策树的结果需要高度可复现。手写代码里对并列最优特征的处理往往不明确同样的数据跑两次可能得出不同树。而成熟的库可以固定random_state保证每一次训练的结构一致。所以下面章节我们回到scikit-learn这个更主流的选择把手写代码作为“原理验证”把库函数作为“正式实验工具”。4. 用scikit-learn复现并调优决策树分类实验的参数地图现在进入正式实验环节。使用DecisionTreeClassifier后实验的难度从“写算法”变成“调参数”。这一章只讲真正需要调整的5个参数再给出一个可复用的网格搜索流程最后说明为什么不能只用准确率评估。4.1 DecisionTreeClassifier的15个参数里真正要调的是这5个scikit-learn的决策树分类器参数看着很多但大部分用默认值即可。真正影响WPBC实验结果的参数只有这些参数默认值本实验建议作用criterionginigini或entropy分裂时用的不纯度指标差异在小数据集上不明显max_depthNone35限制树的最大层数防止无限生长min_samples_leaf126叶子节点最少样本数比max_depth更平滑地控制过拟合class_weightNoneNone或balanced处理R/N类别不平衡random_stateNone42固定随机种子保证结果可复现max_depthNone时树可以一直长到每个叶子都纯净这在198个样本的小数据集上等于背题。min_samples_leaf设置成1同样危险。把max_depth限制在35同时让每个叶子至少有46个样本是我做这个实验的起点。不是说默认值一定差是不设置的话你很难判断模型学到的是规律还是噪声。class_weight先不设如果测试集上少数类R的召回率特别低再考虑balanced。4.2 用GridSearchCV搜索最优参数从max_depth到min_samples_leaf手动一个个试参数很慢我一般直接用GridSearchCV把参数组合扫一遍。决策树训练速度快WPBC数据量小哪怕网格大一点几秒也能跑完。from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import GridSearchCV param_grid { max_depth: [2, 3, 4, 5, 6], min_samples_leaf: [1, 2, 4, 6], criterion: [gini, entropy] } dt DecisionTreeClassifier(random_state42) gs GridSearchCV( dt, param_grid, cv5, scoringroc_auc, n_jobs-1 ) gs.fit(X_tr, y_tr) print(最佳参数:, gs.best_params_) print(交叉验证平均AUC:, gs.best_score_)这里有几个参数要解释。cv5表示在训练集内部再切成5折每折轮流当验证集最终分数是5次平均值比单次随机切分稳定得多。scoringroc_auc是个关键选择不用准确率因为WPBC的R/N比例天然不平衡准确率会被多数类N拉高模型可能“什么都不预测也能拿高分”。n_jobs-1让并行跑满CPU小数据集无所谓但习惯可以保留。网格搜索完成后不要直接认为best_score_就是模型的真实能力。它来自训练集内部的交叉验证不是独立测试集。我习惯把它当作“参数之间的相对比较”最终成绩一定要用第2章留出的X_te来算。4.3 模型评估准召率、混淆矩阵和ROC曲线别只看一个准确率决策树分类实验报告里最常见的错误是只输出一个accuracy。在医学小样本上这远远不够。我建议至少打印三样东西classification_report、混淆矩阵、AUC。from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score best_tree gs.best_estimator_ y_pred best_tree.predict(X_te) y_proba best_tree.predict_proba(X_te)[:, 1] print(classification_report(y_te, y_pred)) print(confusion_matrix(y_te, y_pred)) print(测试集AUC:, roc_auc_score(y_te, y_proba))classification_report里重点关注少数类R的recall它告诉你“真正复发的人里模型能找出来多少”。confusion_matrix能看出错在哪里比如R被预测成N的数量。roc_auc_score接受的是predict_proba输出的正类概率而不是预测标签因为它要衡量排序能力。这三个指标放在一起才能判断这棵决策树到底值不值得上会评审。调参到这里很多人以为实验就结束了。但还有几个反复出现的坑下面单列一章专门说。5. 决策树在乳腺癌数据上的避坑记录过拟合、不平衡与不可复现这一章是我做类似实验时踩过、也帮同事排查过的真实问题。每一条都按“现象 → 原因 → 解决”来写希望能帮你少走几步弯路。5.1 现象训练集准确率高到100%测试集却掉到72%这是决策树分类实验最经典的翻车画面。明明训练时准确率接近满分测试集一测就露馅。原因很简单默认参数下max_depthNone决策树会一直分裂到每个叶子都只含一个样本把训练数据里的随机噪声当成规律背了下来。小样本尤其容易这样因为样本少一两棵异常子树就足以主导预测。解决方法是给树加限制。我通常先设max_depth4、min_samples_leaf4看测试集分数是否回升再用网格搜索精细调整。如果限制后训练集和测试集分数都偏低说明是欠拟合如果训练集高但测试集低继续加大min_samples_leaf。决策树的过拟合不像深度学习那样需要复杂正则化这两三个参数就能按住。5.2 现象同一条数据两次得到完全不同的决策树有一次我把实验结果保存后第二天重新跑同一个脚本发现画出来的决策树结构变了准确率也变了零点几个百分点。原因不是数据变了而是DecisionTreeClassifier在特征切分收益并列时会用随机数打破平局。不加random_state每次训练的树都不完全一样这在小样本上尤其明显。解决方法是固定所有随机种子。数据切分时设了random_state42模型也要设random_state42。严格一点的话网格搜索里的每个DecisionTreeClassifier都带上这个参数。写实验报告时把random_state写进参数说明别人复现时才能得到相同的树结构。5.3 现象把time列当成特征后测试集AUC直接飙到0.99然后被人一眼识破这是我见过最多的高分假象。有人会把原始表里的time列留在特征里训练完发现AUC异常高以为模型找到了某种“玄学规律”。实际上time是标签的结果变量R样本记录复发时间N样本记录未复发随访时间。模型只要按照time大小切一刀就能几乎完美区分R和N这是典型的数据泄漏。解决方法只有一条在特征矩阵里显式删掉id和time。更严谨一点可以做完训练后查看feature_importances_如果某个特征重要性超过0.5回去检查它是不是泄漏变量。在医学数据里“异常高的分数”往往不是好事而是数据问题。5.4 现象GridSearchCV的best_score_高于测试集分数以为模型“稳了”结果上线翻车网格搜索跑完best_score_常常比测试集分数高出一截。这不是bug而是因为它是在训练集内部交叉验证得到的分数本质上仍然属于“训练过程”。数据量只有一百多个样本时交叉验证组内相似度高分数容易被高估。把best_score_写进最终报告会给人一种虚假的安心。解决方法是坚持用独立的测试集做最终评估。只有测试集分数才是模型对未见过数据的真实表现。如果样本实在太小可以在网格搜索外面再套一层交叉验证也就是嵌套交叉验证但那样做复杂度高对这个实验来说可以直接用test_size0.2留出测试集。关键是不要混淆“调参分数”和“泛化分数”。5.5 现象少数类R的召回率只有0.2网格搜索却告诉我这是最优模型AUC高不代表少数类找得准。WPBC中R样本本来就不多如果模型把所有样本都预测成N准确率可能不低但R的召回率接近0。网格搜索用roc_auc作为评分AUC对类别不平衡相对鲁棒但仍有偏科的可能。解决方法是把class_weightbalanced加入网格搜索或者训练后单独看混淆矩阵。如果R召回率太低可以手动降低正类的判定阈值比如把predict_proba中0.5改成0.3。这个操作放在第6章具体讲。总之模型评估一定要看少数类不能只看总分。6. 让决策树分类结果更可信交叉验证、特征重要性与阈值移动实验基本跑通后还有三个小技巧能让结果更可信也更好向上汇报。它们不改变核心算法但能显著提升结论的说服力。6.1 用分层K折交叉验证替代单一测试集结论如果样本量小单次随机划分的运气成分很大。我习惯再用StratifiedKFold把完整数据交叉验证一遍交叉验证的平均分比单次测试集更稳定。代码很短from sklearn.model_selection import StratifiedKFold, cross_validate cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_validate( best_tree, X, y, cvcv, scoring[accuracy, roc_auc] ) print(5折AUC均值:, scores[test_roc_auc].mean())注意这里的X和y是填充完整后的全部数据而不是训练集。交叉验证会重新划分给出模型在不同数据折上的一致性。这个数字比单次测试集分数更能说明模型是否稳定。6.2 用feature_importances_验证模型有没有作弊训练完一定要打印特征重要性它既是业务可解释性的来源也是数据泄漏的照妖镜。import pandas as pd importance pd.Series( best_tree.feature_importances_, indexX_tr.columns ).sort_values(ascendingFalse) print(importance.head(10))如果前几名是细胞核半径、纹理、面积等医学上站得住的特征说明模型学到了合理规律。如果第一名是id或time回到第2章重新检查预处理。特征重要性还能帮你在答辩时回答“决策树凭什么做出判断”这是黑匣子问题最好的预防针。6.3 阈值移动不要等类别不平衡毁掉召回率再后悔当测试集上少数类R的召回率不达标时一个快速有效的做法是降低预测阈值。决策树输出的是概率默认以0.5作为判断边界但WPBC中R比例本来就低0.5可能太苛刻。proba best_tree.predict_proba(X_te)[:, 1] threshold 0.3 y_final (proba threshold).astype(int)把阈值从0.5降到0.3相当于更积极地标记复发风险。这会提高R的召回率但可能牺牲N的精确率。具体降到多少可以遍历几个阈值画出F1曲线选一个业务上能接受的平衡点。这个动作在医学筛查场景里非常实用宁可多复查不愿漏诊。我做这个实验时最深的教训是不要把决策树当成一个“fit完就交差”的万能黑匣子。它的强项是透明、可解释、适合小样本它的脆弱点全在数据入口和参数边界上。把第2章的预处理顺序牢牢记住把random_state、stratify、交叉验证写进每个实验过程你会发现在乳腺癌数据上跑出一棵稳定的决策树并不难。希望帮到你。本文还有配套的精品资源点击获取
返回列表