Step 6(15%):一棵决策树是怎么长出来的
决策树在这门课里有特殊地位:SPSS Modeler 的招牌算法是 C5.0 和 CHAID,两者都是树;而它也是唯一一种你能亲眼看见它在想什么的算法——一棵树可以直接读成一串 if-else 规则交给业务方。这一章把一棵树从零长出来,每一刀的信息增益都是当场算的,然后用它演示这门课里最重要的一个建模现象:训练分涨、测试分跌。
树的算法只有三句话
- 挑一刀。在所有列、所有可能的切点里,找那一刀切下去之后「两边最纯」的那个。
- 切下去,两边各自递归。
- 什么时候停:到了最大深度、或者叶子里样本太少、或者再切也换不来更纯了。
唯一需要定义的是「纯」。两种常用判据,算出来的树略有不同:
| 判据 | 公式 | 纯的时候 | 谁在用 |
|---|---|---|---|
| Gini 不纯度 | 1 − Σ p² | 0 | CART、sklearn 默认、Spark MLlib 默认 |
| 信息熵 | −Σ p·log₂p | 0 | ID3、C4.5、SPSS 的 C5.0 |
「一刀有多好」用增益衡量:切之前的不纯度,减去切之后两边不纯度的加权平均。增益越大,这一刀越值。
亲手长一棵
下面这棵树是真的:判据、最大深度、叶子最小样本三个旋钮都会真的重新训练,树形、增益、准确率、特征重要度全部当场算:
默认设置(信息熵——和 SPSS C5.0 一致、深度 3、叶子最少 12 个样本)长出来的树,第一刀是:
人均 GDP <= 1182.15 (n=336, gain=0.456) ├─ 是 → 继续按人均 GDP 切 └─ 否 → 按疫苗覆盖率切
三件事值得注意:
- 第一刀的增益 0.456 远大于后面所有刀(第二刀 0.258,第三刀 0.056)。这说明问题的大部分结构就在第一刀里——和第 12 章 PCA 的结论(PC1 独占 70% 方差)完全一致。
- 特征重要度里
gdp_pc占 92.9%。好消息是结论简单;坏消息是这个模型几乎是个单变量规则——上线后那一列的口径一变,模型就废了。这句话要写进 8.4 的可靠性讨论。 - 把深度调到 3 以上,你会看到树开始劈
spend——那是第 8 章就点名过的纯噪声列。
过拟合的现场
把深度从 1 拉到 8,两个判据各扫一遍(Demo 里的说明会当场把整条扫描列出来):
| 最大深度 | Gini(CART/sklearn 默认) | 信息熵(C5.0) | ||||
|---|---|---|---|---|---|---|
| 叶子 | 训练 | 测试 | 叶子 | 训练 | 测试 | |
| 1 | 2 | 0.878 | 0.861 | 2 | 0.866 | 0.806 |
| 2 | 4 | 0.878 | 0.861 | 4 | 0.866 | 0.806 |
| 3 | 8 | 0.905 | 0.799 | 6 | 0.887 | 0.84 |
| 4 | 10 | 0.905 | 0.799 | 8 | 0.887 | 0.84 |
| 5–8 | 12 | 0.905 | 0.799 | 10 | 0.899 | 0.833 |
Gini 那半边的深度 3 就是过拟合的定义:训练准确率 +0.027,测试准确率 −0.062。模型多学到的东西不是规律,是这 336 行训练数据里的噪声——具体来说,是它拿 spend 那一列劈了一刀,而那一列与目标的相关系数只有 −0.096。
同一份数据、同样的深度设置,两个判据的结论完全不同:
- Gini:深度 1–2 最好(测试 0.861),深度 3 起下滑到 0.799。
- 信息熵:深度 1 只有 0.806,深度 3–4 上到 0.84,深度 5 之后回落到 0.833。
它们在同一批候选切点里挑了不同的那一刀,于是整棵树的走向就分岔了。这不是谁对谁错——它说明「判据」和「深度」必须一起扫,而不是固定一个判据只扫深度。
这一点直接对上 Step 6.3 的原文「build/select appropriate model(s) and choose relevant parameter(s)」:你要交的是一张二维扫描表(判据 × 深度),不是一个默认值。顺便说,这也解释了为什么 ISAS 那次同时跑 C5.0(熵)和 CHAID(卡方)是个便宜的加分动作——两个判据本身就是一次对比实验。
树的三个参数,各控制什么
| 参数 | sklearn | SPSS C5.0 / CHAID | Spark | 调它干什么 |
|---|---|---|---|---|
| 最大深度 | max_depth | C5.0 无直接项(用 pruning severity);CHAID 有 Maximum tree depth | maxDepth | 最有效的防过拟合旋钮。先调它 |
| 叶子最小样本 | min_samples_leaf | Minimum records per child branch | minInstancesPerNode | 防止树为了几个样本单独开一片叶子 |
| 最小增益 | min_impurity_decrease | C5.0 的 pruning severity | minInfoGain | 切不出收益就不切 |
| 判据 | criterion='gini'/'entropy' | C5.0 = 熵;CHAID = 卡方;CART = Gini | impurity='gini'/'entropy' | 影响切点选择 |
| 类别权重 | class_weight='balanced' | Misclassification costs 矩阵 | (无直接项,用重采样) | 处理类不平衡的首选(第 17 章) |
SPSS 的 CHAID 值得单独提一句:它用卡方检验决定分裂,可以做多路分裂(一个节点分成三支四支),而且天然处理类别变量。它的树往往比 C5.0 矮而宽,更容易给业务方看。ISAS 那次同时跑 C5.0 和 CHAID,是一个很省事的「比较了多个算法」证据。
把树读成规则:Step 8.3 的现成材料
树的最大优势是它能被翻译成人话。把上面那棵熵判据、深度 3 的树按叶子摊平(区间已合并,n 是训练集里落进该叶的地区数,p 是其中真正未达标的比例):
规则 1 人均 GDP ≤ 612 → 未达标 n=85 p=1.00
规则 2 612 < 人均 GDP ≤ 1 014 → 未达标 n=63 p=0.65
规则 3 1 014 < 人均 GDP ≤ 1 182 → 达标 n=21 p=0.33
规则 4 人均 GDP > 1 182,疫苗 ≤ 71.4% → 达标 n=17 p=0.29
规则 5 人均 GDP > 1 182,疫苗 > 71.4%,
卫生支出占比 ≤ 3.53% → 达标 n=51 p=0.08
规则 6 人均 GDP > 1 182,疫苗 > 71.4%,
卫生支出占比 > 3.53% → 达标 n=99 p=0.00
业务读法:人均 GDP 约 1 200 美元是这份数据里的一条分水岭。
线以下的地区几乎必然未达标,且越穷越确定(规则 1 的 85 个地区
无一达标);线以上,疫苗覆盖率是否达到 71% 成为主要区分因素。
含义是:对贫困地区需要更基础的投入,单靠提高疫苗覆盖率不足以
让它们达标;而对中等以上收入地区,疫苗服务的边际收益最高。
顺便看规则 5 和 6:树在 spend(卫生支出占比)上又劈了一刀,而两边的预测都是「达标」。这一刀没有改变任何判定,纯粹是在拟合训练集的噪声——spend 与目标的相关系数只有 −0.096。
但这里有个值得注意的细节:你不能靠「把深度设成 2」来砍掉它。因为第三层还有另一刀(规则 2 与 3 之间那个 1 014 美元的切点),那一刀是有用的——它把 21 个地区从「未达标」改判成「达标」,正是熵判据在深度 3 上把测试准确率从 0.806 抬到 0.84 的原因。把深度砍到 2,两刀一起没了,测试准确率反而掉回 0.806。
深度是一把粗刀,它按层砍,分不出哪一层有用。要精确砍掉没用的那一刀,得用另一个旋钮:最小增益(sklearn 的 min_impurity_decrease、Spark 的 minInfoGain、C5.0 的 pruning severity)。spend 那一刀的增益只有 0.043,把阈值设在 0.05 就能只砍掉它而保住 1 014 那一刀。这就是「剪枝」和「限深」的区别,也是 Step 6.3 里值得写一句的东西。
那段「业务读法」是 Step 8.3 的核心产出,而它是从树里直接读出来的。这就是为什么即使决策树的指标不是最好的(第 16 章会看到逻辑回归赢它 0.076 的准确率),也应该至少跑一棵树——它是唯一能给你这段话的算法。
「人均 GDP 1 182 美元是分水岭」是一句关于这份数据的陈述,不是一条自然规律。三个必须一起说的限制:
- 这个数字不稳定。换一个随机种子重新划分训练集,分裂点会变(可能是 1 050,也可能是 1 240)。想报这个数,就要报它在多次划分下的范围——第 17 章的 k 折可以顺手给你这个。
- 它不是「把 GDP 提到 1 200 就会达标」。GDP 只是发展水平的代理变量(第 12 章:PC1 占 70%)。第 20 章会用一个符号翻转的例子说明为什么这类因果解读很危险。
- 叶子里的比例不是概率。「65% 未达标」是训练集里 63 个样本的比例,样本越少越不可信。报告里给出每片叶子的 n,让读者自己判断。
# OSAS · scikit-learn
from sklearn.tree import DecisionTreeClassifier, export_text
clf = DecisionTreeClassifier(criterion='entropy', max_depth=3,
min_samples_leaf=12, random_state=42)
clf.fit(X_train, y_train)
print(export_text(clf, feature_names=list(X_train.columns))) # 规则文本
print(dict(zip(X_train.columns, clf.feature_importances_.round(3))))
# BDAS · PySpark MLlib
from pyspark.ml.feature import VectorAssembler, StringIndexer
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml import Pipeline
assembler = VectorAssembler(inputCols=num_cols + ohe_cols, outputCol='features')
dt = DecisionTreeClassifier(labelCol='label', featuresCol='features',
impurity='entropy', maxDepth=3,
minInstancesPerNode=12, seed=42)
model = Pipeline(stages=[assembler, dt]).fit(train)
print(model.stages[-1].toDebugString) # 树形文本,可直接贴进报告
print(model.stages[-1].featureImportances)
ISAS · SPSS Modeler:源节点 → Type 节点(把目标设为 Target、其余设 Input)→ Partition 节点 → C5.0 节点。双击 C5.0 可设 pruning severity 与 minimum records per child。建完模型会生成一个金色的模型块(nugget),双击它就能看到树形与规则集——那张图是 ISAS 报告里最好的一张截图。
三条线的一致性检查:同样的判据和深度,三条线的树应该结构相似但不完全相同(切点搜索策略、连续变量分箱方式有差异)。如果差异很大,先检查是不是划分种子不同或独热编码方式不同——这类排查过程写进 Step 8 的跨线对比,很有分量。
1. Step 6.1:探索性分析——相关矩阵(复用 2.3)+ 一次基线模型(深度 1 或全猜多数类)的指标,说明「问题的可分性如何」。
2. Step 6.3 的参数扫描表:判据 × 深度 × 训练准确率 × 测试准确率 × 叶子数。至少 6 行。然后一句「据此选定 X,因为再深只增加训练分不增加测试分」。
3. 一张树形图或规则文本(SPSS 的 nugget 截图 / export_text / toDebugString)。
4. 特征重要度表 + 一句风险提示(「单列占 92.9%,模型对该列的口径变化高度敏感」)。
5. 规则的业务读法(上面那段模板)——这一段会在 Step 8.3 再用一次。
这一章的一句话
树的全部算法就是「挑最能提纯的一刀、递归、到时候停」;而它最有价值的产出不是准确率,是可以读成人话的规则。参数要扫(判据 × 深度),因为深度 3 那一行会当场演示什么叫过拟合:训练 +0.027,测试 −0.062。
下一章把五个算法放到同一张测试集上比一遍——包括一个必须存在的对手:「全猜多数类」。它的准确率是 0.576,而这个数字会改变你读所有其他数字的方式。