简介这份资源是面向机器学习初学者与医学数据分析爱好者的决策树分类实验包围绕wpbcWisconsin乳腺癌数据集展开用于完成良恶性肿瘤的预测建模与性能评估。压缩包共13个文件约572KB包含data、names、csv等数据与说明文件png结果图以及m脚本和txt文本覆盖数据读取、模型训练、剪枝对比与评价指标输出等环节。实验完整呈现了从缺失值处理、特征划分到准确率、召回率、F1分数评估的流程并附有剪枝前后对比图便于直观理解决策树过拟合与欠拟合的平衡。目前已有1546人学习下载适合希望掌握ID3、C4.5或CART等算法实践、并借鉴医疗诊断分类思路的读者参考复用。1. 决策树分类实验从 wpbc 数据集到可复现的乳腺癌二分类拿到一份乳腺癌数据集想用决策树跑一个二分类实验结果发现标签不是 0/1缺失值还带问号训练集准确率 98% 但测试集只有 70%——这是很多人做决策树分类实验时真实翻车的起点。wpbc 数据集Wisconsin Prognostic Breast Cancer和更常见的 wdbc/breast_cancer 数据集不一样它的标签是复发时间需要先做二值化才能变成分类任务。这篇笔记就围绕「决策树分类程序使用乳腺癌数据集」这个方向把数据加载、标签构造、预处理、建模、调参、评估这条链路完整走一遍每一步都给可抄的代码和参数说明。适合已经会写 Python、想拿一个真实医学数据集练手分类流程的人也适合正在做课程实验、需要一份能跑通且能解释清楚每一步为什么这么做的参考。2. wpbc 数据集到底长什么样字段、标签与二值化改造2.1 先搞清楚 wpbc 和 breast_cancer 的区别很多人搜「乳腺癌 决策树」时默认用的是 sklearn 自带的load_breast_cancer那个数据集是 569 个样本、30 个特征、标签直接是 0/1 的良性/恶性二分类拿来就能训。但标题里明确写了 wpbc 数据集这是 UCI 上的另一个数据集全称是 Breast Cancer Wisconsin (Prognostic)198 个样本每个样本有 33 个字段去掉 ID 后 32 个标签是Outcome取值是N非复发和R复发而且还有一个Time字段表示复发时间。wpbc 的定位是预后预测不是诊断所以它的任务本质是「预测会不会复发」而不是「预测是不是恶性」。这个区别直接决定了你的实验设计。如果你拿 wpbc 当 breast_cancer 用把Outcome直接当标签那没问题N/R 映射成 0/1 就是二分类。但如果你想把Time也用上那就变成了生存分析不是决策树分类的范畴了。所以第一步要明确这个实验做的是二分类标签用OutcomeTime字段要么丢掉要么只用来做分层抽样。wpbc 的字段结构大致是第 1 列是 ID第 2 列是 Outcome第 3 列是 Time后面 30 列是特征包括半径、纹理、周长、面积、光滑度、紧凑度、凹陷度、凹点数、对称性、分形维数这 10 个属性的均值、标准差、最差值。和 breast_cancer 的 30 个特征命名逻辑一致但数值分布不同因为样本量小很多。2.2 加载 wpbc 并构造二分类标签wpbc 原始文件是wpbc.data逗号分隔缺失值用?表示。下面这段代码把数据读进来做标签映射并把?转成 NaN。import pandas as pd import numpy as np # wpbc.data 没有表头列名按 UCI 官方文档手动指定 columns [ id, outcome, time, radius_mean, texture_mean, perimeter_mean, area_mean, smoothness_mean, compactness_mean, concavity_mean, concave_points_mean, symmetry_mean, fractal_dimension_mean, radius_se, texture_se, perimeter_se, area_se, smoothness_se, compactness_se, concavity_se, concave_points_se, symmetry_se, fractal_dimension_se, radius_worst, texture_worst, perimeter_worst, area_worst, smoothness_worst, compactness_worst, concavity_worst, concave_points_worst, symmetry_worst, fractal_dimension_worst ] df pd.read_csv(wpbc.data, headerNone, namescolumns, na_values?) # 标签映射N - 0非复发R - 1复发 df[label] df[outcome].map({N: 0, R: 1}) # 检查标签分布 print(df[label].value_counts()) print(缺失值统计) print(df.isnull().sum().sum(), 个缺失值)这段代码的关键点有三个。第一列名必须手动指定因为原始文件没有表头如果你直接read_csv不加names第一行数据会被当成表头后面全乱。第二na_values?让 pandas 自动把问号识别为 NaN省得后面再手动替换。第三标签映射用mapN 对应 0R 对应 1这样决策树才能处理。跑完之后你会看到标签分布大概是 N 占 151 个R 占 47 个属于典型的不平衡二分类比例大约 3:1。这个不平衡会在后面评估时带来麻烦因为如果模型全预测 N准确率也有 76%所以不能只看准确率。2.3 缺失值处理wpbc 的 4 个字段有坑wpbc 的缺失值集中在concavity_mean、concave_points_mean、concavity_worst、concave_points_worst这几个字段总共 4 个字段有缺失缺失样本数不多大概十几个。处理方式有三种直接删行、均值填充、中位数填充。对于决策树来说中位数填充比均值更稳因为决策树对异常值不敏感但均值容易被极端值拉偏。from sklearn.impute import SimpleImputer feature_cols [c for c in df.columns if c not in [id, outcome, time, label]] X df[feature_cols].copy() y df[label].copy() # 中位数填充决策树对缺失值敏感必须先补 imputer SimpleImputer(strategymedian) X_imputed pd.DataFrame(imputer.fit_transform(X), columnsfeature_cols) print(填充后缺失值, X_imputed.isnull().sum().sum())这里用SimpleImputer而不是手动fillna好处是填充策略可以统一管理后面如果换数据集改一个参数就行。strategymedian是中位数也可以换成mean或most_frequent但对连续特征来说中位数最稳。注意填充必须在划分训练集和测试集之前做还是之后做严格来说应该先在训练集上 fit再 transform 测试集避免数据泄漏。但 wpbc 样本量小缺失值少实际影响不大。如果你要做严谨实验用Pipeline把 imputer 和模型串起来交叉验证时自动处理。3. 决策树分类器怎么搭从默认参数到可解释的树3.1 基线模型先跑通再调参搭决策树分类器最忌讳一上来就调参应该先用默认参数跑一个基线看看数据本身能到什么水平。sklearn 的DecisionTreeClassifier默认用 gini 系数不限制深度所以树会一直长到叶子节点纯净为止训练集准确率通常很高但测试集可能一般。from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report # 分层抽样保证训练集和测试集标签比例一致 X_train, X_test, y_train, y_test train_test_split( X_imputed, y, test_size0.3, random_state42, stratifyy ) # 默认参数基线 clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(测试集准确率, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[非复发, 复发]))stratifyy是关键因为 wpbc 标签不平衡如果不分层可能测试集里复发样本只有几个评估结果波动很大。random_state42保证每次划分一致方便复现。跑完基线你会看到测试集准确率大概在 0.70 到 0.78 之间但classification_report里复发类别的 recall 可能只有 0.3 到 0.5意思是大部分复发样本被漏掉了。这就是不平衡数据 默认决策树的典型问题树倾向于预测多数类。3.2 三个必调参数max_depth、min_samples_leaf、class_weight决策树的可调参数很多但对 wpbc 这种小样本不平衡数据真正影响大的就三个。max_depth控制树的最大深度。默认是 None树会无限长训练集准确率能到 1.0但测试集过拟合。wpbc 样本只有 198 个特征 30 个深度建议控制在 3 到 6 之间。深度 3 的树大概 7 个叶子节点可解释性最好深度 6 能到 20 多个叶子拟合能力更强但容易过拟合。min_samples_leaf控制叶子节点最少样本数。默认是 1意味着每个叶子可以只有一个样本这在小数据集上就是过拟合的根源。建议设成 5 到 10让每个叶子至少有 5 个样本树会更稳。class_weight处理不平衡。默认 None所有样本权重一样。设成balanced后sklearn 会自动按标签频率反比给权重复发类样本权重会更高recall 会明显提升。# 调参后的决策树 clf_tuned DecisionTreeClassifier( max_depth4, min_samples_leaf5, class_weightbalanced, random_state42 ) clf_tuned.fit(X_train, y_train) y_pred_tuned clf_tuned.predict(X_test) print(调参后准确率, accuracy_score(y_test, y_pred_tuned)) print(classification_report(y_test, y_pred_tuned, target_names[非复发, 复发]))跑完对比一下准确率可能从 0.75 降到 0.72但复发类别的 recall 会从 0.4 升到 0.7 以上。这就是class_weightbalanced的作用牺牲一点整体准确率换少数类的识别能力。在医学场景里漏掉一个复发样本的代价远大于误判一个非复发样本所以这个取舍是值得的。3.3 用 GridSearchCV 找参数组合手动调参靠经验但如果你想系统性地找最优组合用GridSearchCV。注意评分指标不能只用 accuracy不平衡数据要用f1或recall。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 4, 5, 6, 8], min_samples_leaf: [1, 3, 5, 8, 10], class_weight: [None, balanced] } grid GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringf1, n_jobs-1 ) grid.fit(X_train, y_train) print(最优参数, grid.best_params_) print(最优 F1, grid.best_score_)cv5是 5 折交叉验证scoringf1用 F1 分数而不是准确率因为 F1 同时考虑 precision 和 recall。n_jobs-1用满 CPU 核数加速。跑完你会得到一组最优参数通常max_depth在 4 到 6 之间min_samples_leaf在 3 到 8 之间class_weight大概率是balanced。提示GridSearchCV 的best_score_是交叉验证的平均 F1不是测试集 F1。测试集要单独用grid.best_estimator_.predict(X_test)评估两者有差距是正常的。4. 评估与可视化准确率之外还要看什么4.1 混淆矩阵和 ROC 曲线准确率在 3:1 不平衡数据上会骗人。一个全预测非复发的模型准确率 76%但它没有任何临床价值。所以必须看混淆矩阵和 ROC 曲线。from sklearn.metrics import confusion_matrix, roc_curve, auc import matplotlib.pyplot as plt # 混淆矩阵 cm confusion_matrix(y_test, y_pred_tuned) print(混淆矩阵) print(cm) # ROC 曲线 y_prob clf_tuned.predict_proba(X_test)[:, 1] fpr, tpr, thresholds roc_curve(y_test, y_prob) roc_auc auc(fpr, tpr) plt.figure(figsize(6, 5)) plt.plot(fpr, tpr, labelfAUC {roc_auc:.3f}) plt.plot([0, 1], [0, 1], k--) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(wpbc 决策树 ROC 曲线) plt.legend() plt.tight_layout() plt.savefig(roc_wpbc.png, dpi150)混淆矩阵的四个格子分别代表真阴性非复发预测对、假阳性非复发误判为复发、假阴性复发漏判、真阳性复发预测对。在医学场景里假阴性最危险因为漏掉一个复发患者。所以你要重点看假阴性的数量如果超过 5 个说明模型还需要调。ROC 曲线的 AUC 值衡量模型整体排序能力0.5 是随机猜1.0 是完美。wpbc 上决策树的 AUC 通常在 0.70 到 0.80 之间不算高因为样本量小、特征噪声大。如果 AUC 低于 0.65说明模型基本没学到东西要检查数据预处理是不是出了问题。4.2 特征重要性决策树的可解释性优势决策树相比 SVM、神经网络最大的优势是可解释性。feature_importances_属性直接给出每个特征的重要性分数加起来等于 1。import pandas as pd importances pd.DataFrame({ feature: feature_cols, importance: clf_tuned.feature_importances_ }).sort_values(importance, ascendingFalse) print(importances.head(10))跑完你会看到 wpbc 上最重要的特征通常是concave_points_worst、perimeter_worst、area_worst这几个和 breast_cancer 上的结论类似。这说明最差值的形态特征对复发预测最有区分度。如果你发现某个特征重要性特别高比如 0.5 以上要警惕是不是数据泄漏比如 ID 字段没删干净。4.3 可视化决策树把树画出来决策树可以导出成图形直观看到每个节点用什么特征、什么阈值分裂。from sklearn.tree import plot_tree plt.figure(figsize(20, 10)) plot_tree( clf_tuned, feature_namesfeature_cols, class_names[非复发, 复发], filledTrue, roundedTrue, fontsize8 ) plt.tight_layout() plt.savefig(tree_wpbc.png, dpi150, bbox_inchestight)filledTrue让节点颜色按类别纯度填充颜色越深纯度越高。roundedTrue圆角框好看一点。fontsize8防止文字重叠。画出来的树如果深度是 4大概有 15 个节点能看清每个分裂条件。你可以顺着一条路径读比如「concave_points_worst 0.15 且 area_worst 800 则预测非复发」这就是决策树的规则提取能力。注意如果树太深画出来会糊成一团。建议先用max_depth3画一棵小树看结构再用完整树看细节。5. 避坑与排查wpbc 决策树实验的 5 个血泪教训5.1 现象测试集准确率 0.95但复发 recall 只有 0.2原因没有设class_weight决策树默认把所有样本等权多数类非复发主导了分裂方向少数类被牺牲。解决加class_weightbalanced或者用sample_weight手动给少数类加权。如果还不行用 SMOTE 过采样少数类但要注意 SMOTE 只能在训练集上做不能碰测试集。5.2 现象交叉验证 F1 很高但测试集 F1 很低原因填充缺失值的时候在全集上做了fit_transform测试集的信息泄漏到了训练过程。或者用train_test_split之前就做了标准化。解决把所有预处理步骤放进Pipeline让交叉验证自动在每折训练集上 fit测试集只 transform。from sklearn.pipeline import Pipeline pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (clf, DecisionTreeClassifier(max_depth4, class_weightbalanced, random_state42)) ]) pipe.fit(X_train, y_train)5.3 现象特征重要性里 ID 字段排第一原因加载数据时没把id列排除ID 是递增数字和标签有偶然相关性决策树把它当成了强特征。解决feature_cols里明确排除id、outcome、time、label。任何和标签无关的元数据列都要删掉。5.4 现象predict_proba输出的概率只有 0 和 1原因决策树默认min_samples_leaf1叶子节点只有一个样本概率就是 0 或 1没有中间值。解决增大min_samples_leaf到 5 以上叶子节点有多个样本后概率就会变成 0.2、0.6 这种连续值。如果要做概率校准用CalibratedClassifierCV包一层。5.5 现象换random_state后结果波动很大原因wpbc 只有 198 个样本测试集 30% 只有 59 个样本随机划分的波动被放大。不同随机种子可能让准确率差 5 到 10 个百分点。解决用StratifiedKFold做 10 折交叉验证报告平均值和标准差而不是单次划分的结果。如果标准差超过 0.08说明模型不稳定要简化树结构或增加正则化。6. 把决策树推到边界从单棵树到集成与校准单棵决策树在 wpbc 上的 AUC 大概 0.75不算强。如果你想再往上推有两个方向集成和校准。集成方面随机森林和梯度提升比如 XGBoost是自然延伸。随机森林用 bagging 降低方差XGBoost 用 boosting 降低偏差。在 wpbc 这种小样本上随机森林通常比单棵树稳AUC 能到 0.80 左右XGBoost 如果调参得当能到 0.82 到 0.85但小样本上容易过拟合max_depth要压到 3 以内learning_rate设 0.05 到 0.1。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth5, min_samples_leaf3, class_weightbalanced, random_state42 ) rf.fit(X_train, y_train) y_prob_rf rf.predict_proba(X_test)[:, 1] print(随机森林 AUC, auc(*roc_curve(y_test, y_prob_rf)[:2]))校准方面决策树的predict_proba不是真正的概率因为叶子节点纯度不代表置信度。用CalibratedClassifierCV做 Platt 缩放或 isotonic 回归能让概率更可靠。在医学场景里如果要把概率输出给医生参考校准这一步不能省。from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV( DecisionTreeClassifier(max_depth4, min_samples_leaf5, class_weightbalanced, random_state42), methodsigmoid, cv5 ) calibrated.fit(X_train, y_train) y_prob_cal calibrated.predict_proba(X_test)[:, 1] print(校准后 AUC, auc(*roc_curve(y_test, y_prob_cal)[:2]))验证方法上我一般会做三件事一是用StratifiedKFold跑 10 折看 F1 的均值和标准差二是画学习曲线看训练集和验证集分数随样本量增加的变化判断是过拟合还是欠拟合三是用permutation_importance做特征重要性置换检验确认重要特征不是偶然。from sklearn.inspection import permutation_importance perm permutation_importance( clf_tuned, X_test, y_test, n_repeats30, random_state42, scoringf1 ) for i in perm.importances_mean.argsort()[::-1][:5]: print(f{feature_cols[i]}: {perm.importances_mean[i]:.4f} ± {perm.importances_std[i]:.4f})置换重要性比内置的feature_importances_更可靠因为它是在测试集上打乱特征值看性能下降多少能反映特征的真实贡献。如果某个特征置换后 F1 几乎不变说明它不重要可以考虑删掉简化模型。我自己做这类实验的习惯是先跑通基线再调三个核心参数然后看混淆矩阵和 AUC最后用置换重要性验证特征。每一步都留random_state每一步都记录参数和分数。wpbc 数据集小跑一次不到 10 秒但坑不少尤其是标签二值化和缺失值处理这两步翻车的人最多。希望帮到你。本文还有配套的精品资源点击获取
