☰
决策树分类实验:wpbc乳腺癌数据集二分类调参与评估
2026/9/26 23:44:15 网站建设 项目流程

简介:这份资源是面向机器学习初学者与医学数据分析爱好者的决策树分类实验包,围绕wpbc(Wisconsin 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 的区别,别拿错数据

很多人搜「乳腺癌数据」时,默认拿到的是 wdbc(Diagnostic),569 条样本、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 标签列名为 'Outcome',R 表示复发,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_size=0.3, random_state=42, stratify=y ) # 默认参数基线 clf = DecisionTreeClassifier(random_state=42) 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))

逻辑说明:stratify=y是关键参数,小样本下不分层会导致某一折里正样本极少。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_weight="balanced",往往比默认参数稳得多。下面用网格搜索一次性比较。

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_state=42), param_grid, cv=5, scoring="f1", # 不平衡数据看 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))

逻辑说明:scoring="f1"是因为 wpbc 不平衡,准确率会骗人。cv=5在 198 条样本上每折约 40 条,已经是能接受的下限,再少就不稳定。class_weight="balanced"会自动按类别频率反比加权,通常能把少数类 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_depth=d, min_samples_leaf=5, class_weight="balanced", random_state=42 ) 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, cv=5, scoring="f1").mean()) plt.plot(depths, train_scores, label="train") plt.plot(depths, val_scores, label="cv 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, label=f"AUC={auc:.3f}") plt.plot([0, 1], [0, 1], "--", color="gray") 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_names=X.columns, class_names=["N", "R"], filled=True, rounded=True, fontsize=8 ) plt.show()

逻辑说明:feature_names传入列名,class_names对应标签,filled=True按类别着色。看树时关注三点:根节点特征是否合理、树的深度是否和设定一致、叶子节点的样本数是否过少。如果某个叶子只有一两个样本,说明min_samples_leaf还可以再调大。这一步是决策树相比随机森林、XGBoost 的独特价值,别浪费。

5. 避坑与排查:wpbc 决策树实验里最容易翻车的五件事

5.1 现象:交叉验证分数很高,测试集一塌糊涂

原因:多半是特征泄漏,比如把复发时间或 ID 类字段留在了特征里,模型在训练时记住了样本。解决:建模前逐列检查,凡是预测时点拿不到的字段一律删掉,再用train_test_split之前就完成筛选,不要等切分后再删。

5.2 现象:少数类 recall 始终为 0

原因:类别不平衡加上默认阈值 0.5,模型全判多数类。解决:加class_weight="balanced",并把评估指标从 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 的树就交差,结果复盘发现少数类几乎没抓到,后来强制自己每次先打印混淆矩阵再看其他指标,才没再翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询