简介这份资源是面向机器学习初学者与医学数据分析爱好者的决策树分类实验包围绕wpbcWisconsin Breast Cancer乳腺癌数据集展开帮助读者理解如何用决策树完成良恶性肿瘤的预测任务。压缩包共13个文件约572KB包含data、csv等数据文件names与txt说明文档m脚本以及png结果图覆盖数据读取、模型构建到剪枝前后对比的完整流程。已有1546人学习下载说明该实验在入门分类算法时具有较高参考价值。读者可借助其中的脚本与数据动手复现决策树训练与测试过程观察剪枝对过拟合的影响并通过准确率、召回率等指标评估模型表现从而掌握特征划分、树的深度调优等关键思路为医疗诊断类分类问题提供可借鉴的实践模板。1. 决策树分类实验从 wpbc 数据集到可复现的乳腺癌二分类拿到「决策树分类实验乳腺癌.zip」这类标题的人通常不是想听算法史而是手里已经有一份 wpbc 数据集想跑出一个能解释、能复现、指标不虚高的二分类结果。wpbc 全称 Wisconsin Prognostic Breast Cancer和更常见的 wdbc 不同它多了肿瘤大小、淋巴结状态、复发时间这类预后字段样本量只有 198 条正负样本还偏斜所以它天然是个「小样本 类别不平衡」的决策树练兵场。这篇文章就围绕这份数据把决策树分类器从加载、切分、调参到评估整条链路走一遍重点讲清楚为什么在小样本上决策树容易过拟合、参数该怎么压、评估该看哪几个数。适合已经会调 sklearn、但一遇到医学小数据集就指标飘忽的从业者也适合想拿一个完整二分类案例练手的新手。下面所有步骤都能直接抄数据集字段名以 wpbc 常见版本为准若你的 csv 列名不同改一下映射即可。2. wpbc 数据集长什么样字段、标签与不平衡陷阱2.1 先认清 wpbc 和 wdbc 的区别别拿错数据很多人搜「乳腺癌数据」时默认拿到的是 wdbcDiagnostic569 条样本、30 个特征、标签是恶性/良性任务干净漂亮。而 wpbc 是 Prognostic 版本198 条样本标签通常是「复发 / 未复发」特征里除了细胞核形态还混入了肿瘤直径、淋巴结阳性数、复发时间这些预后变量。这意味着两件事第一样本更少决策树的分裂点更容易被个别样本带偏第二标签分布更不均衡复发类往往只占两三成。如果你直接套用网上 wdbc 的调参经验准确率可能看着还行但召回率会很难看。常见做法是先把数据读进来打印 shape、标签分布和缺失情况再决定要不要做重采样。import pandas as pd import numpy as np # 读取 wpbc 数据集假设文件为 wpbc.csv第一列是样本 ID df pd.read_csv(wpbc.csv) print(数据形状:, df.shape) print(列名:, df.columns.tolist()) # 常见 wpbc 标签列名为 OutcomeR 表示复发N 表示未复发 print(标签分布:\n, df[Outcome].value_counts()) print(缺失值统计:\n, df.isnull().sum().sum())这段代码的作用是先做数据体检。shape告诉你样本和特征规模value_counts让你看清不平衡程度isnull().sum()判断要不要插补。wpbc 里Lymph node status有时会以?形式出现读进来会变成字符串后面建模前必须处理。参数上没什么可调的但这一步不能省否则你后面调半天参数问题其实出在标签编码上。2.2 标签编码与特征筛选把预后字段用起来wpbc 的标签是字符型决策树虽然也能处理字符串标签但为了统一评估和后续画 ROC建议映射成 0/1。同时像「复发时间」这种字段在真实预测场景里属于事后信息如果它和标签高度相关模型会学到一个「作弊」特征交叉验证分数虚高。我的习惯是先把明显泄漏的列去掉再保留细胞核形态和肿瘤大小这类术前可得特征。下面这段做标签映射和列筛选。# 标签映射复发为 1未复发为 0 df[label] df[Outcome].map({R: 1, N: 0}) # 去掉 ID 和可能造成标签泄漏的复发时间字段 drop_cols [ID, Outcome, Time] # 若存在 ? 占位先替换为 NaN 再决定是否删除 df df.replace(?, np.nan) df df.drop(columns[c for c in drop_cols if c in df.columns]) # 简单处理缺失数值列用中位数填充 for col in df.columns: if df[col].isnull().any(): df[col] df[col].fillna(df[col].median()) print(处理后形状:, df.shape) print(正样本比例:, df[label].mean().round(3))逻辑说明map把字符标签转成数值drop去掉泄漏列replace把问号统一成 NaN再用中位数填充。参数上中位数比均值更抗偏态适合医学指标。正样本比例打印出来如果低于 0.35就要在建模时考虑class_weight或分层抽样。这一步做完数据才算真正可喂给模型。3. 决策树分类器怎么搭从默认参数到小样本调参3.1 用 sklearn 跑通第一个决策树基线先别急着调参用默认参数跑一个基线看看模型在 wpbc 上到底什么水平。默认的DecisionTreeClassifier不限制深度在 198 条样本上几乎必然过拟合训练集准确率能到 1.0测试集却可能掉到 0.7 以下。这个反差本身就是重要信息说明必须剪枝。下面代码用分层切分保证训练测试集标签比例一致。from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import classification_report, confusion_matrix X df.drop(columns[label]) y df[label] # 分层切分保证训练集和测试集正负比例接近 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 默认参数基线 clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(训练集准确率:, clf.score(X_train, y_train).round(3)) print(测试集准确率:, clf.score(X_test, y_test).round(3)) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))逻辑说明stratifyy是关键参数小样本下不分层会导致某一折里正样本极少。random_state固定后结果可复现。输出里重点看训练和测试准确率的差距以及classification_report里少数类的 recall。如果少数类 recall 低于 0.5说明模型基本在偏向多数类后面要靠class_weight和剪枝一起救。3.2 三个必调参数max_depth、min_samples_leaf、class_weight决策树在小样本上的核心矛盾是「分裂太细」。控制复杂度主要靠三个参数max_depth限制树深min_samples_leaf要求叶子节点最少样本数class_weight给少数类加权。我的经验是 wpbc 这种量级max_depth放在 3 到 5 之间min_samples_leaf放在 5 到 10 之间再配class_weightbalanced往往比默认参数稳得多。下面用网格搜索一次性比较。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 4, 5, 6], min_samples_leaf: [3, 5, 8, 10], class_weight: [None, balanced], criterion: [gini, entropy] } grid GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringf1, # 不平衡数据看 f1 比 accuracy 靠谱 n_jobs-1 ) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(最佳交叉验证 f1:, grid.best_score_.round(3)) best_clf grid.best_estimator_ print(测试集 f1:, best_clf.score(X_test, y_test).round(3))逻辑说明scoringf1是因为 wpbc 不平衡准确率会骗人。cv5在 198 条样本上每折约 40 条已经是能接受的下限再少就不稳定。class_weightbalanced会自动按类别频率反比加权通常能把少数类 recall 拉上来。参数含义上max_depth越小越保守min_samples_leaf越大越平滑两者要配合调单独调一个容易顾此失彼。3.3 用交叉验证曲线判断有没有过拟合网格搜索给的是平均分但你还想知道模型稳不稳。画一条max_depth对应的训练和验证曲线能直观看到从哪一层开始验证分掉头向下。下面代码手动循环不同深度记录两组分数。import matplotlib.pyplot as plt from sklearn.model_selection import cross_val_score depths range(1, 11) train_scores, val_scores [], [] for d in depths: clf DecisionTreeClassifier( max_depthd, min_samples_leaf5, class_weightbalanced, random_state42 ) clf.fit(X_train, y_train) train_scores.append(clf.score(X_train, y_train)) val_scores.append(cross_val_score(clf, X_train, y_train, cv5, scoringf1).mean()) plt.plot(depths, train_scores, labeltrain) plt.plot(depths, val_scores, labelcv f1) plt.xlabel(max_depth) plt.legend() plt.show()逻辑说明训练分随深度单调上升验证分先升后降拐点就是相对合适的深度。wpbc 上常见拐点在 3 到 5 之间。如果验证分曲线抖动很大说明样本太少交叉验证折数可以降到 3或者改用重复分层交叉验证。这一步不是必须画图但它是判断「玄学调参」还是「有依据调参」的分水岭。4. 评估与解释别只看准确率把树画出来4.1 不平衡二分类该看哪几个指标wpbc 上准确率 0.75 可能意味着模型把所有样本都判成未复发因为未复发占多数。所以必须看混淆矩阵、少数类 recall、F1 和 AUC。下面代码一次性输出这些指标并画出 ROC 曲线。from sklearn.metrics import roc_auc_score, roc_curve y_prob best_clf.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_prob) print(AUC:, round(auc, 3)) fpr, tpr, _ roc_curve(y_test, y_prob) plt.plot(fpr, tpr, labelfAUC{auc:.3f}) plt.plot([0, 1], [0, 1], --, colorgray) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend() plt.show()逻辑说明predict_proba取正类概率roc_auc_score衡量排序能力不受阈值影响。AUC 在 0.7 以上算可用0.8 以上算不错但 wpbc 样本少别指望太高。混淆矩阵里重点看左下和右上也就是漏报和误报。医学场景通常更怕漏报所以阈值可以适当下调牺牲一点精确率换召回。4.2 把决策树画出来验证它学到的规则是否合理决策树最大的优势是可解释。用plot_tree把树结构画出来看看根节点和主要分裂特征是否符合医学常识。如果根节点是一个明显泄漏的字段说明前面筛选没做干净。from sklearn.tree import plot_tree plt.figure(figsize(18, 10)) plot_tree( best_clf, feature_namesX.columns, class_names[N, R], filledTrue, roundedTrue, fontsize8 ) plt.show()逻辑说明feature_names传入列名class_names对应标签filledTrue按类别着色。看树时关注三点根节点特征是否合理、树的深度是否和设定一致、叶子节点的样本数是否过少。如果某个叶子只有一两个样本说明min_samples_leaf还可以再调大。这一步是决策树相比随机森林、XGBoost 的独特价值别浪费。5. 避坑与排查wpbc 决策树实验里最容易翻车的五件事5.1 现象交叉验证分数很高测试集一塌糊涂原因多半是特征泄漏比如把复发时间或 ID 类字段留在了特征里模型在训练时记住了样本。解决建模前逐列检查凡是预测时点拿不到的字段一律删掉再用train_test_split之前就完成筛选不要等切分后再删。5.2 现象少数类 recall 始终为 0原因类别不平衡加上默认阈值 0.5模型全判多数类。解决加class_weightbalanced并把评估指标从 accuracy 换成 f1 或 recall必要时手动下调预测阈值。5.3 现象每次跑出来结果都不一样原因random_state没固定或者交叉验证折数太少导致方差大。解决所有涉及随机的环节都设random_state交叉验证改用StratifiedKFold并固定种子。5.4 现象树深调到 10 以上训练分 1.0 但验证分不升原因小样本下树越深越容易记住噪声这是决策树的血泪经验。解决把max_depth压回 3 到 5配合min_samples_leaf至少 5宁可欠拟合也不要过拟合。5.5 现象plot_tree报错或中文乱码原因特征名里有非 ASCII 字符或 matplotlib 字体没配。解决统一用英文列名或在绘图前设置支持中文的字体class_names用短字符串别塞长句。6. 进阶技巧用代价敏感与阈值移动把召回再拉一档如果前面几步做完少数类 recall 还是卡在 0.6 左右可以试两个进阶手段。第一个是代价敏感学习不只用balanced而是手动指定class_weight{0:1, 1:3}这类比例让模型更怕漏报。第二个是阈值移动决策树默认 0.5 切分你可以遍历 0.2 到 0.6找使 F1 或 recall 最优的阈值。下面给一个阈值扫描的写法。thresholds np.arange(0.2, 0.65, 0.05) best_thr, best_f1 0.5, 0 for thr in thresholds: y_pred_thr (y_prob thr).astype(int) f1 f1_score(y_test, y_pred_thr) if f1 best_f1: best_f1, best_thr f1, thr print(最佳阈值:, round(best_thr, 2), 对应 F1:, round(best_f1, 3))逻辑说明y_prob是正类概率遍历阈值相当于在 ROC 曲线上找最优点。参数上步长 0.05 够用太细容易过拟合测试集。注意阈值要在验证集上选别直接在测试集上挑否则指标会虚高。这个技巧在医学二分类里很实用因为漏报代价通常高于误报。另外如果你想把 wpbc 上的经验迁移到更大数据可以对比随机森林和决策树的区别随机森林靠 bagging 降方差小样本上往往比单棵树稳但可解释性差。我的习惯是先用决策树把规则讲清楚再用随机森林或 XGBoost 二分类模型做性能上限参考两者差距不大就优先交付决策树因为业务方看得懂。最后说个我自己的教训早期做 wpbc 时我迷信准确率调出一棵 0.82 的树就交差结果复盘发现少数类几乎没抓到后来强制自己每次先打印混淆矩阵再看其他指标才没再翻车。希望帮到你。本文还有配套的精品资源点击获取
