08 决策树 (Decision Trees)

课程路线图:我们今天的旅程

课程路线图 展示本章四个主要部分的流程图:核心思想、回归树、分类树、实践。 1. 核心思想 与动机 2. 回归树 (Regression) 3. 分类树 (Classification) 4. Python实践 (Implementation)

本章学习目标:解锁决策树的四大关键

  1. 理论层面:理解树状模型的基本思想,它与我们熟悉的线性模型有何根本不同?
  2. 应用层面:如何利用决策树对复杂的经济金融数据进行分类(如判断客户是否违约)和回归(如预测股票收益率)?
  3. 技术层面:决策树模型是如何从数据中“学习”到决策规则的?其背后的训练算法是什么?
  4. 实践层面:如何使用 Python 从零开始构建、训练并评估一个决策树模型?

温故知新:线性模型的世界观

我们之前学习的线性模型(如OLS)非常强大,它们用一个统一的、全局的公式来描述世界。

\[ \large{y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \epsilon} \]

这个公式假设特征和结果之间是线性可加的关系。

动机:线性模型的局限性

然而,在真实的经济世界中,许多关系是非线性的交互的

  • 非线性: 一个人的收入对信贷风险的影响,在高收入区间和低收入区间可能是完全不同的。
  • 交互作用: 年龄对风险的影响,可能取决于他/她是否有房产。

线性模型很难直接捕捉这种复杂的结构。

可视化:线性模型的“边界”

线性模型(如逻辑回归)只能画出直线来分割数据。如果数据的真实边界是弯曲的,线性模型就会犯很多错误。

线性模型的局限性 一个线性模型试图用直线分割非线性数据,导致分类错误。 模型预测为 A 类 模型预测为 B 类 分类错误!

核心思想的转变:从“一个公式”到“一套规则”

模型类型 核心思想 例子
线性模型 用一个单一的、全局的数学公式来描述 xy 的关系。 \(y = \beta_0 + \beta_1 x_1 + \epsilon\)
树模型 找到一套分层的、局部的决策规则来细分样本空间。 如果收入 > 5000,则…

决策树的思路更接近人类的决策过程,因此具有很强的可解释性

决策树的类比:20个问题游戏

想象一下经典的“20个问题”游戏。你通过一系列“是/否”问题来缩小可能性范围,最终猜出答案。

决策树就是机器在玩这个游戏。

  • 人类问题: “它比面包盒大吗?”
  • 决策树节点: 尺寸 > 面包盒?

每问一个问题,就离答案更近一步。

第一部分:理解树的基本概念

什么是“树”?一种抽象的层次结构

在深入决策树之前,我们先理解什么是抽象意义上的“树”。

  • 它是一种用来表示节点 (Node) 与节点之间层次关系的数据结构。
  • 它看起来像一个倒挂的真实树木,根在上,叶在下。

可视化:一棵抽象的树

抽象的树结构 一个树形结构图,包含根节点、内部节点和叶节点。 根节点 内部节点 A 内部节点 B 叶节点 叶节点 叶节点

树的核心组成元素

一棵树由几种关键部分构成,我们来逐一认识它们。

  1. 根节点 (Root Node)
  2. 内部节点 (Internal Node)
  3. 叶节点 (Leaf / Terminal Node)
  4. 边 (Edge / Branch)

核心元素 (1): 根节点 (Root Node)

树的最顶层节点,唯一的,没有父节点。 它是所有决策的起点,包含了整个数据集。

根节点示意图 一棵树的结构,高亮显示其顶部的根节点。 根节点 内部节点 内部节点 叶节点 叶节点 叶节点

核心元素 (2): 内部节点 (Internal Node)

非叶子节点,它们既有父节点,也有子节点。 每个内部节点都代表一个决策点或一个问题

内部节点示意图 一棵树的结构,高亮显示其中间的内部节点。 根节点 内部节点 内部节点 叶节点 叶节点 叶节点

核心元素 (3): 叶节点 (Leaf Node)

树的末端节点,没有子节点。 它们代表了最终的预测结果决策结论

叶节点示意图 一棵树的结构,高亮显示其底部的叶节点。 根节点 内部节点 内部节点 叶节点 叶节点 叶节点

关键度量:树的深度 (Depth)

深度 (Depth) 是衡量树结构复杂性的一个重要指标。

  • 定义: 从根节点到最远的叶节点所需要经过的的数量。
  • 直观理解: 一个深度为 d 的树,最多需要 d 个问题就能得到最终答案。

深度越大的树,模型越复杂,决策规则越多。

可视化:树的深度

树的深度示意图 一棵深度为2的树,标注了各个层级和测量深度的路径。 ... ... 深度 = 0 深度 = 1 深度 = 2 这棵树的深度 = 2

从抽象树到决策树:赋予节点经济含义

现在,我们将抽象概念赋予经济含义。

决策树 (Decision Tree) 是一种机器学习方法,其中:

  • 每个内部节点 代表一个对特征的测试(一个分割规则)。
  • 每个分支 代表一个测试输出(决策路径)。
  • 每个叶节点 代表一个最终的决策或预测值(例如“高风险”或预测收益率0.05)。

实例:用决策树进行信用风险评估

这是一个经典的信用违约数据分类例子。我们的目标是根据客户信息,判断一笔贷款是“高风险”还是“低风险”。

我们的(模拟)数据集

假设我们有以下客户数据:

客户ID 收入 (千元) 拥有房产 年龄 风险等级
1 4.5 28
2 8.0 45
3 6.2 35
4 3.1 50
5 7.5 29

第一步:寻找最佳的第一个问题

算法会检查所有特征,找到那个最有区分度的特征来进行第一次划分。

假设模型发现收入是最重要的特征。

  • 规则: 收入 > 5.0 千元?

这个规则将所有申请人分成了两组,构成了我们决策树的根节点

信用风险评估:根节点分裂

信用风险决策树的根节点 决策树的第一个分裂,基于收入是否大于5000元。 收入 > 5.0? ... ...

可视化:第一次数据分割

基于收入的数据分割 散点图显示数据点根据收入被一条垂直线分割成两部分。 收入 (千元) 年龄 收入=5.0 收入3.1, 高风险 收入4.5, 高风险 收入6.2, 低风险 收入7.5, 低风险 收入8.0, 低风险

第二步:递归分裂 (Recursive Splitting)

现在我们对每个分支进行递归处理。

  • 对于收入 <= 5.0 的群体,模型发现他们的违约风险普遍很高,可以直接判定为高风险。这是一个叶节点
  • 对于收入 > 5.0 的群体,风险尚不明确。我们需要寻找下一个最有区分度的特征,比如是否拥有房产
  • 新规则: 拥有房产? (是/否)

信用风险评估:最终的决策树

最终,我们得到了一棵完整的决策树,它是一套清晰、可执行的规则。

完整的信用风险决策树 一个三层决策树,用于判断信用风险是高还是低。 收入 > 5.0? 高风险 有房产? 高风险 低风险

解读这棵树:将规则翻译成语言

这棵树告诉我们一个清晰的决策流程:

  1. 首先,检查客户的收入是否大于5000元。
  2. 如果不是,直接判定为“高风险”。
  3. 如果是,则接着检查客户是否拥有房产。
  4. 如果没有房产,判定为“高风险”。
  5. 如果有房产,判定为“低风险”。

决策树的强大优势

  1. 极强的可解释性 (Interpretability): 我们可以清晰地追踪模型的决策路径,理解为什么模型会做出这样的预测。这在金融风控、医疗诊断等领域至关重要。

  2. 高度的灵活性 (Flexibility): 决策树不要求特征与目标变量之间存在线性关系。它可以捕捉复杂的非线性和交互作用。

  3. 应用广泛: 决策树既可以用于回归问题 (Regression Tree),也可以用于分类问题 (Classification Tree)

第二部分:回归树 (Regression Tree)

回归树的目标:预测一个连续值

当我们想要预测一个连续的目标变量 y 时(例如股票收益率、房价、公司利润),我们使用回归树。

  • 核心问题: 如何构建一棵树,使其对 y 的预测最准确?
  • 关键: 我们需要一个评价标准来衡量预测的准确性。

我们需要一个代价函数 (Cost Function)

和线性回归一样,我们需要一个代价函数来量化模型的预测误差。我们的目标是找到一棵能够最小化这个代价函数的树。

  • 在线性回归中,我们使用的代价函数是均方误差 (Mean Squared Error, MSE)残差平方和 (Residual Sum of Squares, RSS)
  • 这个思想可以被完美地沿用到回归树中。

回归树的代价函数:残差平方和 (RSS)

对于一棵给定的树,它将样本空间划分为 M 个互不重叠的区域(叶节点) \(R_1, R_2, ..., R_M\)

对于任何落入区域 \(R_m\) 的观测值,我们的预测值都是该区域内所有训练样本目标值的平均值,记为 \(\hat{y}_{R_m}\)

\[ \large{\hat{y}_{R_m} = \frac{1}{N_m} \sum_{x_i \in R_m} y_i} \]

我们的目标是最小化总的 RSS:

\[ \large{RSS = \sum_{m=1}^{M} \sum_{x_i \in R_m} (y_i - \hat{y}_{R_m})^2} \]

可视化理解 RSS

RSS 就是所有数据点到其所在区域预测值(水平线)的垂直距离的平方和。我们的目标是调整分割线,让这些红色虚线的总长度(的平方)最小。

残差平方和 (RSS) 的可视化 散点图显示了数据点和两个区域的平均值线,以及它们之间的残差。 特征 X 目标 Y ŷ_R1 ŷ_R2

挑战:如何找到最优的树?

寻找能够最小化全局 RSS 的最优树是一个NP-hard问题。

为什么?因为可能的树结构数量是指数级的。我们不可能检查每一棵可能的树。这是一个组合爆炸问题,在计算上是不可行的。

可视化:组合爆炸

即使只有少数几个数据点,构建树的方式也多得惊人。

树结构的组合爆炸 从一个根节点出发,可以生成多种不同结构的树,数量迅速增加,形成混乱的组合爆炸。 起始数据 无数种可能的树...

解决方案:贪心算法 (Greedy Algorithm)

既然找不到全局最优解,我们就退而求其次,寻找一个足够好的局部最优解。

我们使用一种自顶向下 (top-down)贪心 (greedy) 策略来构建决策树,这个过程也叫做递归二元切分 (Recursive Binary Splitting)

  • 贪心的意思是:在每一步,我们只做出当前看起来最好的决策,而不考虑这个决策对未来的影响。

贪心算法的类比:最速下降

就像一个徒步者在山上,每一步都选择最陡峭的下山路径,而不考虑这是否会将他带入一个无法走出的山谷。

贪心算法类比:最速下降 一个人在山顶选择最陡的路径下山,而忽略了可能通往全局最低点的另一条路径。 你在这里 贪心选择 (当前最陡) 全局最优路径 局部最优 全局最优

递归二元切分:算法步骤

算法从根节点开始,包含所有数据:

  1. 遍历所有特征: 对每一个特征 j
  2. 遍历所有可能的分裂点: 对特征 j 的每一个可能的分割点 s
  3. 计算分裂增益: 计算如果按照 (j, s) 这个规则进行分裂,会导致 RSS 减少多少。
  4. 选择最佳分裂: 选取那个能使 RSS 减少得最多的特征 j 和分割点 s
  5. 执行分裂: 用选出的最佳规则 (j, s) 将当前节点分裂成两个子节点。
  6. 递归: 对每个新的子节点,重复步骤 1-5,直到满足某个停止条件

关键计算:如何衡量一次分裂的好坏?

假设我们正在考虑用特征 j 和分割点 s 来分裂一个节点。这个分裂会产生两个区域: * \(R_1(j,s) = \{x | x_j < s\}\) * \(R_2(j,s) = \{x | x_j \ge s\}\)

这次分裂带来的 RSS 减少量 \(\Delta RSS\) 为:

\[ \large{\Delta RSS_{j,s} = RSS_{parent} - (RSS_{R_1} + RSS_{R_2})} \]

我们的目标就是在所有可能的 js 中,找到能使 \(\Delta RSS_{j,s}\) 最大化的那个组合。

一个数值例子:理解分裂过程

假设我们有一个节点,里面有5个样本,我们想基于特征 x 对它进行分裂。

样本 特征 x 目标 y
1 2.2 10
2 3.5 12
3 6.0 25
4 9.8 80
5 15.0 90

问题: 最佳的分割点在哪里?

可能的分割点

特征 x 的值是 [2.2, 3.5, 6.0, 9.8, 15.0]

我们可以在任意两个相邻点的中点进行分割。所以我们有4个可能的分割点: 1. s1 = (2.2 + 3.5) / 2 = 2.85 2. s2 = (3.5 + 6.0) / 2 = 4.75 3. s3 = (6.0 + 9.8) / 2 = 7.9 4. s4 = (9.8 + 15.0) / 2 = 12.4

可视化:可能的分割点

可能的分割点 一条数轴上标出了5个数据点和4个可能的分割点。 x 0 5 10 15 20 2.2 3.5 6.0 9.8 15.0 s1=2.85 s2=4.75 s3=7.9 s4=12.4

检验分割点 1:s = 2.85

  • 规则: x < 2.85
  • 左子节点 R1: {样本1 (y=10)}
    • 预测值 \(\hat{y}_{R1} = 10\)
    • \(RSS_{R1} = (10-10)^2 = 0\)
  • 右子节点 R2: {样本2,3,4,5 (y=12,25,80,90)}
    • 预测值 \(\hat{y}_{R2} = (12+25+80+90)/4 = 51.75\)
    • \(RSS_{R2} = (12-51.75)^2 + ... + (90-51.75)^2 = 4668.75\)
  • 分裂后总 RSS: \(0 + 4668.75 = 4668.75\)

检验分割点 3:s = 7.9

  • 规则: x < 7.9
  • 左子节点 R1: {样本1,2,3 (y=10,12,25)}
    • 预测值 \(\hat{y}_{R1} = (10+12+25)/3 \approx 15.67\)
    • \(RSS_{R1} = (10-15.67)^2 + ... + (25-15.67)^2 \approx 134.67\)
  • 右子节点 R2: {样本4,5 (y=80,90)}
    • 预测值 \(\hat{y}_{R2} = (80+90)/2 = 85\)
    • \(RSS_{R2} = (80-85)^2 + (90-85)^2 = 50\)
  • 分裂后总 RSS: \(134.67 + 50 = 184.67\)

可视化检验分割点 3

分割点 s=7.9 的可视化 散点图显示在 x=7.9 处分割数据,以及两个区域的平均值线。 特征 X 目标 Y x=2.2, y=10 x=3.5, y=12 x=6.0, y=25 x=9.8, y=80 x=15.0, y=90 x=7.9 ŷ_R1=15.67 ŷ_R2=85

比较所有分割点

分割点 s 分割后的总 RSS
2.85 4668.75
4.75 3050.00
7.90 184.67
12.40 1850.00

结论: s = 7.9 是当前节点的最佳分割点,因为它使得分裂后的 RSS 最小。

可视化理解:好的分裂 vs 差的分裂

好的分裂与差的分裂对比 并排比较了最优分裂(s=7.9)和次优分裂(s=2.85)的效果,显示了它们的RSS值。 最优分裂 (s=7.9) RSS = 184.67 次优分裂 (s=2.85) RSS = 4668.75

过拟合:一个无休止的故事

如果我们不加限制,决策树会一直分裂下去,直到每个叶节点只有一个样本。

  • 此时,训练集上的 RSS = 0,模型“完美”地记住了所有答案。
  • 但是,它在面对新数据时会表现得非常糟糕。我们称之为过拟合 (Overfitting)

何时停止分裂?– 超参数的作用

为了防止过拟合,我们需要设定一些停止条件,这些在 scikit-learn 中被称为超参数 (Hyperparameters)

我们来认识几个最重要的:

  1. 最大深度 (max_depth)
  2. 叶节点最少样本数 (min_samples_leaf)
  3. 最小分裂样本数 (min_samples_split)
  4. 最小不纯度减少量 (min_impurity_decrease)

超参数 (1): max_depth

树允许生长的最大层数。 这是控制模型复杂最直接的方法。

最大深度 max_depth 一棵树在达到最大深度2后停止生长。 ... ... ... Leaf Leaf max_depth = 2, 禁止继续生长!

超参数 (2): min_samples_leaf

一个叶节点必须包含的最少训练样本数。 这可以防止模型为少数几个异常点创建单独的规则。

叶节点最少样本数 min_samples_leaf 一个节点因为分裂后子节点的样本数少于阈值而停止分裂。 Samples = 12 成为叶节点 Samples = 8 Samples = 4 如果 min_samples_leaf = 5, 则不允许这次分裂!

超参数 (3): min_samples_split

一个内部节点必须包含的最少样本数才能被分裂。

最小分裂样本数 min_samples_split 一个节点因为其样本数少于分裂阈值而成为叶节点。 Samples = 15 成为叶节点 如果 min_samples_split = 20, 这个节点就不能再分裂了!

一个完整的回归树例子:预测每股收益率

下图展示了一个训练好的、深度为2的回归树,用于预测公司的每股收益率 (EPS)

每股收益预测决策树示例 一个深度为2的回归树,用于预测EPS,布局清晰可读。 pps <= 1093.0 mse = 2.783 samples = 4224, value = 0.857 pps <= 111.665 mse = 1.168 samples = 4215 value = 0.801 roa <= 0.069 mse = 84.15 samples = 9 value = 26.811 mse = 0.99 samples = 4190 value = 0.64 mse = 2.33 samples = 25 value = 2.363 mse = 101.3 samples = 5 value = 31.08 mse = 25.4 samples = 4 value = 21.475

解读这棵树:追踪一条路径

让我们追踪一家普通公司:pps = 100, roa = 0.05

  1. 根节点: pps (100) <= 1093.0? 。往左走。
  2. 左侧节点: pps (100) <= 111.665? 。往左走。
  3. 到达叶节点: 最终预测该公司的 EPS 为 0.64

解决方案:决策树的剪枝 (Pruning)

剪枝是防止过拟合、简化决策树的主要方法。有两种策略:

  1. 预剪枝 (Pre-pruning): 在树的生长过程中,提前停止。我们之前讨论的停止条件(如max_depth, min_samples_leaf)就是一种预剪枝。
  2. 后剪枝 (Post-pruning): 先生成一棵可能过拟合的“大树”,然后自底向上地裁剪掉一些分支。

后剪枝通常效果更好,因为它看到了树的全貌。

可视化:预剪枝 vs 后剪枝

预剪枝与后剪枝的对比 两棵树,左边显示了预剪枝(提前停止),右边显示了后剪枝(生长后裁剪)。 预剪枝 (Pre-pruning) 达到停止条件, 不再生长 后剪枝 (Post-pruning) 先完整生长, 再修剪

后剪枝的核心思想:代价复杂度剪枝

这是一种最常用的后剪枝方法。它的思想是,我们不再仅仅最小化 RSS,而是最小化一个加了惩罚项的代价函数:

\[ \large{C_{\alpha}(T) = \underbrace{\sum_{m=1}^{|T|} \sum_{x_i \in R_m} (y_i - \hat{y}_{R_m})^2}_{\text{误差项 (RSS)}} + \underbrace{\alpha |T|}_{\text{复杂度惩罚项}}} \]

  • \(|T|\) 是树 \(T\)叶节点数量(衡量树的复杂度)。
  • \(\alpha \ge 0\) 是一个惩罚参数 (Tuning Parameter),需要通过交叉验证来选择。

惩罚参数 \(\alpha\) 的作用

\(\alpha\) 控制着我们对模型简单性和拟合优度之间的权衡。

惩罚参数 alpha 的作用 一个滑块代表alpha值,从0到无穷大,对应下方的树从复杂到简单。 惩罚参数 α α = 0 α → ∞ 中等 α 最大树 (过拟合) 最优子树 (泛化好) 根节点 (欠拟合)

第三部分:分类树 (Classification Tree)

分类树的目标:预测一个类别

当我们想要预测一个离散的目标变量 y 时(例如“违约”/“不违约”,股票“涨”/“跌”/“平”),我们使用分类树。

  • 预测方式: 对于一个叶节点,它的预测结果是该节点中数量最多的那个类别。
  • 核心问题: RSS 不再适用。我们需要一个新的标准来衡量一个节点的“纯度 (purity)”,并以此来指导分裂。

核心概念:节点纯度 (Purity)

一个好的分裂应该使得分裂后的子节点更“纯”

节点纯度示意图 三个容器,分别代表纯、不纯和最不纯的节点。 最纯的节点 (Gini=0) 较不纯的节点 最不纯的节点 (Gini=0.5)

衡量“不纯度”的指标

我们有两种常用的指标来量化一个节点的“不纯度” (Impurity): 1. 基尼不纯度 (Gini Impurity) 2. 信息熵 (Entropy)

指标一:基尼不纯度 (Gini Impurity)

对于一个给定的节点,基尼不纯度的计算公式为: \[ \large{G = \sum_{k=1}^{K} \hat{p}_{mk} (1 - \hat{p}_{mk})} \] 其中: * \(K\) 是类别的总数。 * \(\hat{p}_{mk}\) 是在节点 m 中,第 k 类样本所占的比例

直观理解: \(G\) 度量了从一个节点中随机抽取的两个样本类别不同的概率。 * \(G=0\): 完全纯。 * \(G\) 越大: 越不纯。

基尼不纯度计算示例

假设一个节点有10个样本,4个是“违约”(+),6个是“不违约”(-)。

  • \(p(+) = 4/10 = 0.4\)
  • \(p(-) = 6/10 = 0.6\)

该节点的基尼不纯度为: \[ \large{G = p(+)(1-p(+)) + p(-)(1-p(-))} \] \[ \large{G = 0.4(0.6) + 0.6(0.4) = 0.24 + 0.24 = 0.48} \]

指标二:信息熵 (Entropy)

信息熵源于信息论,衡量的是一个系统的不确定性混乱程度

\[ \large{D = - \sum_{k=1}^{K} \hat{p}_{mk} \log_2(\hat{p}_{mk})} \]

  • \(D=0\): 完全纯,系统没有任何不确定性。
  • \(D\) 越大: 越混乱,不确定性越大。

注意: 约定 \(0 \log 0 = 0\)

信息熵计算示例

同样是10个样本,4个“违约”(+),6个“不违约”(-)。

  • \(p(+) = 0.4\)
  • \(p(-) = 0.6\)

该节点的信息熵为: \[ \large{D = - [p(+) \log_2(p(+)) + p(-) \log_2(p(-))]} \] \[ \large{D = - [0.4 \times (-1.32) + 0.6 \times (-0.74)] \approx 0.97} \]

基尼 vs 熵:有何区别?

  • 实践中: 两者效果非常相似,最终生成的树几乎没有差别。基尼系数的计算稍微快一些,因为它不涉及对数运算。scikit-learn 的默认选项是基尼系数。
  • 理论上: 熵更倾向于产生更“平衡”的分裂。
import numpy as np
import matplotlib.pyplot as plt

p = np.linspace(0.001, 0.999, 200)
gini = 2 * p * (1-p)
entropy = - (p * np.log2(p) + (1-p) * np.log2(1-p))


fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(p, gini, label='Gini Impurity (基尼不纯度)', color='royalblue', linewidth=2.5)
ax.plot(p, entropy, label='Entropy (信息熵)', color='crimson', linewidth=2.5)
ax.set_title('基尼不纯度 vs. 信息熵 (二分类)', fontsize=16)
ax.set_xlabel('类别 1 的比例 (p)', fontsize=12)
ax.set_ylabel('不纯度', fontsize=12)
ax.axvline(0.5, color='grey', linestyle='--', lw=1)
ax.annotate('不纯度最高', xy=(0.5, 1.0), xytext=(0.55, 0.9),
            arrowprops=dict(facecolor='black', shrink=0.05, width=1, headwidth=8),
            fontsize=12)
ax.legend()
plt.show()
Figure 1: 二分类问题中,基尼不纯度与信息熵的比较

分类树的分裂标准:不纯度减少量

与回归树类似,分类树在选择最佳分裂时,寻找的是那个能够最大化不纯度减少量 (Impurity Decrease) 的分裂。

\[ \large{\Delta I = I_{parent} - (\frac{N_{left}}{N_{parent}} I_{left} + \frac{N_{right}}{N_{parent}} I_{right})} \]

这个值也被称为信息增益 (Information Gain)(如果使用熵作为不纯度度量)。

可视化:信息增益

我们选择能让分裂后的子节点(右边两个小罐子)的加权平均纯度最高(不纯度最低)的分裂。

信息增益可视化 一个不纯的父节点分裂成两个更纯的子节点,显示了信息增益。 父节点: 不纯 子节点1: 纯 子节点2: 纯 信息增益很高!

第四部分:Python实践–预测股票收益

我们的任务:用基本面数据预测收益率

我们将使用真实的金融数据来构建一个回归树。

  • 目标: 预测一家公司未来的每股收益 (EPS)
  • 特征:
    • PPS: 每股股价 (Price per Share)
    • BM: 市净率 (Book-to-Market Ratio)
    • ROA: 资产回报率 (Return on Assets)
  • 工具: pandas, scikit-learn, matplotlib, yfinance.

步骤一:导入必要的库

这是我们的标准起手式:导入所有需要的Python库。

# 数据处理与分析
import pandas as pd
import numpy as np

# 机器学习库
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error

# 可视化
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

# 数据获取
import yfinance as yf

步骤二 (1/2): 获取和准备数据

为了让我们的例子更真实、可复现,我们将使用 yfinance 实时获取一批公司的财务数据。我们将获取标普500指数成分股的数据。

# 为了演示,我们只获取前50家公司的数据,避免运行时间过长
# 注意:yfinance获取数据可能因网络问题失败
# 在真实项目中,需要更鲁棒的错误处理和数据清洗
try:
    sp500_tickers = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')['Symbol'].tolist()
    sp500_tickers = [t.replace('.', '-') for t in sp500_tickers]
    
    all_data = []
    for ticker_str in sp500_tickers[:50]:
        ticker_obj = yf.Ticker(ticker_str)
        info = ticker_obj.info
        
        # 目标变量:每股收益
        eps = info.get('trailingEps')
        # 特征变量
        price = info.get('previousClose')
        book_value = info.get('bookValue')
        shares = info.get('sharesOutstanding')
        bm = book_value / shares if book_value and shares else None
        roa = info.get('returnOnAssets')
        
        if all([eps, price, bm, roa]):
             all_data.append({'Ticker': ticker_str, 'eps_basic': eps, 'PPS': price, 'BM': bm, 'ROA': roa})
    
    data = pd.DataFrame(all_data).dropna().reset_index(drop=True)
except Exception as e:
    print("无法获取实时数据,将使用模拟数据。")
    # 如果yfinance失败,创建一个模拟DataFrame
    mock_data = {
        'Ticker': [f'C{i}' for i in range(20)],
        'eps_basic': np.random.randn(20) * 2 + 5,
        'PPS': np.random.rand(20) * 500 + 50,
        'BM': np.random.rand(20) * 2 + 0.5,
        'ROA': np.random.randn(20) * 0.05 + 0.08
    }
    data = pd.DataFrame(mock_data)

data.head()
无法获取实时数据,将使用模拟数据。
部分获取到的公司财务数据
Ticker eps_basic PPS BM ROA
0 C0 3.217087 224.596400 1.291589 0.110854
1 C1 6.459987 236.418131 0.960727 0.039050
2 C2 7.169707 84.651506 2.466021 0.089162
3 C3 6.061229 143.139664 0.679207 0.167248
4 C4 7.336585 64.722483 0.746060 0.046595

步骤二 (2/2): 定义特征和目标变量

# 定义特征矩阵 X (我们的输入)
X = data[['PPS', 'BM', 'ROA']]
# 定义目标向量 y (我们想要预测的)
y = data['eps_basic']

print("特征矩阵 X 的维度:", X.shape)
print("目标向量 y 的维度:", y.shape)
特征矩阵 X 的维度: (20, 3)
目标向量 y 的维度: (20,)

步骤三:划分训练集和测试集

这是机器学习流程中至关重要的一步。我们必须将数据分为两部分: * 训练集 (Training Set): 用于“教”模型如何做预测。 * 测试集 (Test Set): 用于评估模型在未见过的数据上的表现。

可视化:训练集 vs 测试集

训练集与测试集划分 一个数据块被分割成80%的训练集和20%的测试集。 完整数据集 训练集 (80%) 测试集 (20%)

步骤三:执行代码

# test_size=0.2 表示测试集占20%
# random_state=42 确保每次运行代码时,划分结果都是一样的,保证了结果的可复现性
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f'训练集大小: {X_train.shape}')
print(f'测试集大小: {X_test.shape}')
训练集大小: (16, 3)
测试集大小: (4, 3)

步骤四:模型初始化与训练

现在我们来构建我们的决策树模型。

  • 我们使用 DecisionTreeRegressor 因为我们的目标y是连续的。
  • 我们设置 max_depth=2 作为预剪枝策略,防止模型过于复杂,也方便我们后续的可视化和解读。
# 1. 初始化一个回归决策树模型,并设定最大深度为2
cart_regressor = DecisionTreeRegressor(max_depth=2, random_state=42)

# 2. 使用训练数据 (X_train, y_train) 来训练模型
# .fit() 方法是所有scikit-learn模型的训练入口
cart_regressor.fit(X_train, y_train)

print('模型训练完成!')
模型训练完成!

步骤五:进行预测

模型训练好之后,我们就可以用它来做预测了。我们将训练好的模型 cart_regressor 应用于我们从未用过的测试集 X_test 上。

# 使用 .predict() 方法进行预测
y_pred = cart_regressor.predict(X_test)

# 将结果整理成DataFrame,方便比较
results = pd.DataFrame({'真实值 (y_test)': y_test, '预测值 (y_pred)': y_pred})
print(results.head())
    真实值 (y_test)  预测值 (y_pred)
0       3.217087      4.611716
17      2.936132      4.611716
15      4.285092      7.640651
1       6.459987      4.611716

步骤六:评估模型性能

对于回归问题,最常用的评估指标是均方误差 (Mean Squared Error, MSE)。它计算的是预测值与真实值之差的平方的平均值。MSE越小,模型性能越好。

\[ \large{MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2} \]

# 使用 mean_squared_error 函数计算测试集的MSE
mse = mean_squared_error(y_test, y_pred)

print(f'模型在测试集上的均方误差 (MSE) 是: {mse:.4f}')
模型在测试集上的均方误差 (MSE) 是: 4.8571

步骤七:可视化与解读决策树

scikit-learn 提供了强大的 plot_tree 函数,让我们能够直观地看到模型内部的决策规则。这是决策树模型最大的魅力所在。

plt.figure(figsize=(20, 10))

plot_tree(cart_regressor, 
          feature_names=X.columns.tolist(), 
          filled=True, # 使用颜色填充节点以表示纯度
          rounded=True, # 使用圆角矩形
          fontsize=12,
          precision=3) # 显示3位小数

plt.title('决策树模型:预测每股收益 (EPS)', fontsize=20)
plt.show()
Figure 2: 训练好的每股收益预测决策树 (最大深度=2)

如何解读这张图?放大看一个节点

解读 plot_tree 的节点 一个决策树节点的放大图,并对每一行文字进行了解释。 ROA <= 0.051 mse = 14.892 samples = 32 value = 5.234 分裂规则 (问题) 该节点的均方误差 落入该节点的样本数 该节点的预测值 (样本均值)

第五部分:总结与展望

决策树的优点与缺点

优点 (Pros)

  • 可解释性强:规则直观易懂。
  • 预处理要求低:不需特征缩放。
  • 处理非线性关系:能捕捉复杂特征交互。
  • 处理混合数据:能同时用于数值和类别特征。

缺点 (Cons)

  • 不稳定:对数据微小变动敏感。
  • 容易过拟合:需要剪枝来控制。
  • 贪心算法:不保证找到全局最优树。
  • 预测性能相对较弱:单个决策树精度通常不高。

核心权衡:可解释性 vs. 预测精度

在机器学习中,这是一种永恒的权衡。

可解释性与预测精度的权衡 一个坐标轴,显示不同模型在可解释性和预测精度上的位置。 预测精度 可解释性强 可解释性弱 线性回归 决策树 随机森林 神经网络

展望:从一棵树到一片森林

决策树最大的缺点是不稳定。但这个缺点也正是它的力量来源!

通过将许多不同(因此不稳定)的决策树的结果结合起来,我们可以构建出预测能力极强、同时又非常稳健的模型。

这就是我们下一章的主题:集成学习 (Ensemble Learning),包括随机森林 (Random Forest)梯度提升树 (Gradient Boosting Trees)

练习:知识理解

我们使用决策树模型对一个数据集进行拟合。通过交叉验证,我们发现模型在训练数据中的损失远小于在验证数据集中的损失。

  1. 这个问题是过拟合还是欠拟合?
  2. 以下哪种方法可能可以对于这种问题有所帮助? 为什么?
      1. 减少决策树的深度。
      1. 对现有决策树的叶节点进行分裂。
      1. 对决策树进行剪枝。

练习答案与解析

  1. 这是典型的过拟合 (Overfitting)。模型在它“熟悉”的训练数据上表现很好,但在“陌生”的验证数据上表现很差,说明它学习了太多训练数据特有的噪声,缺乏泛化能力。

  2. 有帮助的方法是 (a) 和 (c)

    • (a) 减少决策树的深度: 这是预剪枝的一种方式。通过限制树的复杂度,强迫模型学习更具泛化性的规律。
    • (b) 对叶节点进行分裂: 这会使树更深、更复杂,从而加剧过拟合。
    • (c) 对决策树进行剪枝: 这是后剪枝。通过移除对泛化能力贡献不大的分支来简化模型,是对抗过拟合的核心技术。

最终总结

  • 决策树通过一系列分层规则来对数据进行分割,模拟人类决策过程。
  • 回归树使用 RSS 作为分裂标准,预测值为叶节点内样本的均值
  • 分类树使用基尼不纯度信息熵作为分裂标准,预测值为叶节点内样本的众数
  • 决策树非常易于解释,但单个树容易过拟合不稳定
  • 通过剪枝(预剪枝/后剪枝)和设置超参数可以有效控制过拟合。
  • 它们是构建更强大的集成模型的基础。

Q & A

有任何问题吗?