07 集成学习

三个臭皮匠,赛过诸葛亮

集成学习概念图 三个简单的“弱学习器”(圆形、方形、三角形)通过聚合过程,组合成一个复杂的“强学习器”。 弱学习器 A 弱学习器 B 弱学习器 C + + 强学习器

在金融与经济中,预测无处不在

我们依赖各种模型进行关键决策:

金融与经济中的预测应用 四个图标分别代表银行、对冲基金、政府和保险公司,并配有它们各自的典型预测任务。 银行 预测客户 是否会发生 信用卡违约 对冲基金 预测股票 明天走势 上涨或下跌 政府 预测下季度 GDP 增长率 保险公司 预测客户 提出索赔的 风险

核心问题:单一模型 vs. 群体智慧

任何一个单一模型都可能存在缺陷、偏差或错误。

本章要探讨的问题是:

如果我们将许多“还不错”的模型结合起来,能否得到一个“非常强大”的超级模型?

剧透一下:答案是肯定的。这个方法就叫做集成学习 (Ensemble Learning)

本章学习目标

完成本讲后,你能够:

  1. 核心内容:用训练误差与验证误差诊断单棵决策树的过拟合。
  2. 核心内容:根据偏差、方差与学习器相关性,在 Bagging 与 Boosting 之间作有证据的选择。
  3. 拓展内容:手算一次熵、信息增益、基尼指数与 AdaBoost 权重更新;90 分钟核心内容只要求解释这些量如何驱动分裂与重加权。
  4. 核心内容:解释为何本讲选择随机森林,并说明单棵树与 AdaBoost 的测试结果只能作为辅助比较。
  5. 核心内容:从实际模型输出解释特征重要性,并说明“重要性不等于因果”。
  6. 拓展内容:解释 Stacking 的样本外元特征,并与前两类机制比较。

指标约定:AP 不等于梯形 PR-AUC

  • 本讲所有 average_precision_score 输出均称为 AP(平均精度)。

  • AP 按召回增量加权精确率,不等于经验 PR 曲线的梯形积分面积;表格、练习与参考输出统一使用 AP。

开始前回顾:先看单树为何会失败

  1. 若一棵树继续分裂直到叶节点只剩一个样本,训练误差与测试误差通常如何变化?
  2. Bootstrap 抽样是“无放回”还是“有放回”?
  3. 在正类稀少时,只看准确率会遗漏什么?

请先独立写下三个判断,再揭示答案。

揭示与补救

  • 训练误差趋近 0,但测试误差可能上升;Bootstrap 是有放回抽样;准确率会掩盖少数类识别失败,应同时检查 AP(平均精度)、召回率与混淆矩阵。

  • 若第 1 题答错,回到“决策树的现实问题”;若第 2 或第 3 题答错,分别回到 Bagging 工作流或多数类基线。

叙事主线:先诊断单树的高方差,再引出降低方差的 Bagging;随后讨论降低偏差的 Boosting 与融合异质模型的 Stacking。

学习内容概览

本章学习内容概览 四个阶段:先观察单棵树的不足,再理解原因,随后学习集成模型,最后分析真实数据。 1 单树失效 (Failure first) 2 理论与机制 (Why and how) 3 集成模型 (Repair) 4 Python 实战 (Real-world Impact)

90 分钟学习安排:先看失败,再学修复

  • 0–12 分钟|单树失败:先看树如何分裂、为何高方差,并预测默认阈值会漏掉多少少数类。
  • 12–42 分钟|核心机制:Bagging 降方差、Boosting 重加权;Stacking 作为拓展。
  • 42–70 分钟|真实数据:同一 A 股样本、同一时间切分比较单树、随机森林和 AdaBoost。
  • 70–85 分钟|决策:多数类基线、AP(平均精度)、混淆矩阵和验证期阈值。
  • 85–90 分钟|本讲回顾:说明“排序更好”为什么不等于“默认阈值可用”。

核心内容学习顺序

先建立失败诊断:只看训练分数会误判

  • 在训练期拟合、验证期诊断,在完成模型选择前不查看测试期。

  • 若单树训练 AUC 接近 1、验证 AUC 明显下降,首先得到的是高方差风险,不是“模型已经可用”。

  • 本讲选择随机森林,是因为它能降低单棵树的高方差,同时保留特征重要性。

  • 模型先于验证与测试结果确定;验证期只用于选择分类阈值。

  • 单棵树与 AdaBoost 的测试结果只作辅助比较,不能据此更换模型。

先预测:若随机森林验证 ROC-AUC 与单树接近,它在 0.5 阈值下会自然获得正类召回吗?

检查 1 揭示

不会。AUC 衡量排序,0.5 是另一个分类阈值;答“会”时回到“排序不等于分类阈值”,先画出阈值移动如何改变 FP/FN。

单棵树表现不佳时,可以怎样改进?

失败诊断 核心修复 验证期预期
训练好、验证差,样本扰动下不稳定 Bagging:并行重采样,多树平均 排序更稳定;0.5 阈值仍不保证有召回
受限树仍系统性漏掉难样本 Boosting:串行提高错分样本权重 难样本排序可能改善;分类阈值仍须在验证期确定

核心内容继续

先用这张表完成机制预测,再向前进入决策树为何失败。以下 Hoeffding、完整 AdaBoost 推导与 Stacking 均可沿链接跳过。

拓展内容 A:理论依据

学习提示:以下 Hoeffding 理论为拓展;课堂主线请从 决策树为何失败 继续。

为什么“群体智慧”是可靠的?

霍夫丁不等式 (Hoeffding’s Inequality)

霍夫丁不等式从概率论上给出了一个保证:

大量独立随机变量的均值,会以极高的概率收敛于其真实期望值

换句话说,只要样本够多,样本均值就非常接近总体均值。

这听起来很抽象,我们用一个经典的抛硬币实验来理解。

直观理解:抛硬币实验

  • 问题:有一枚可能不均匀的硬币,正面朝上的真实概率是 p (未知)。我们如何估计 p
  • 方法:抛 n 次,计算正面朝上的频率 h (样本均值)。
  • 直觉n 越大,h 应该越接近真实的 p

霍夫丁不等式精确地量化了这种“接近”的程度。

霍夫丁不等式的数学表达

它告诉我们,样本均值 h 与真实期望 p 的差距大于任意小量 ε 的概率,是随着样本数 n 的增加而指数级下降的

\[ \large{P(|h - p| > \epsilon) \le 2e^{-2n\epsilon^2}} \]

这个 \(e\)负指数项是关键。这意味着我们每增加一次观测(抛一次硬币),犯大错误的概率就会急剧减小。

从抛硬币到集成学习的飞跃

从抛硬币到集成学习的飞跃 图示主题为“从抛硬币到集成学习的飞跃”。主要标签包括:抛硬币实验、一枚硬币、抛一次的结果 (正面/反面)、抛 T 次硬币、最终的正面频率、集成学习 (Ensemble)、一个“弱”学习器、一次预测的结果 (正确/错误);这些元素共同展示该页的关系、比较或流程。 抛硬币实验 一枚硬币 抛一次的结果 (正面/反面) 抛 T 次硬币 最终的正面频率 集成学习 (Ensemble) 一个“弱”学习器 一次预测的结果 (正确/错误) 训练 T 个独立的学习器 投票后的最终预测

集成学习的错误率呈指数级下降

基于这个类比,霍夫丁不等式的一个推论告诉我们:

  • 基学习器条件: 共有 \(T\) 个独立二元分类器,每个错误率均为 \(\epsilon<0.5\)

  • 集成规则: 使用简单多数投票形成 \(H(x)\)

  • 理论结果: 在这些强条件下,集成犯错概率的上界随 \(T\) 指数级下降:

\[ \large{P(H(x) \ne y) \le \exp(-2T(0.5 - \epsilon)^2)} \]

理论的核心结论

集成学习之所以有效,依赖于两个关键假设:

  1. 独立性 (Independence):每个“臭皮匠”的思考方式需要有差异。如果所有人都犯同样的错误,集成是没有意义的。
  2. 优于随机 (Better than Random):每个“臭皮匠”的能力都必须比瞎猜要好一点 (对于二分类问题,准确率 > 50%)。

只要满足这两个条件,我们几乎总能通过集成得到一个强大的学习器。

拓展内容 B:三种集成机制的完整展开

学习提示:以下完整机制图与 Stacking 为拓展;核心摘要位于 单树与剪枝之后

实践中,我们如何创造出满足那两个条件的“一群臭皮匠”呢?

集成学习的三大流派

集成学习的三大流派 图示主题为“集成学习的三大流派”。主要标签包括:集成学习方法对比 (Ensemble Methods)、Bagging、关系: 独立并行、解决: 降低方差 (Variance)、好比: 专家独立分析后投票、Boosting、关系: 顺序依赖、解决: 降低偏差 (Bias);这些元素共同展示该页的关系、比较或流程。 集成学习方法对比 (Ensemble Methods) Bagging 关系:独立并行 解决:降低方差 类比:独立投票 Boosting 关系:顺序依赖 解决:降低偏差 类比:专攻错题 Stacking 关系:分层结构 解决:提升预测精度 类比:主席汇总

机制一:Bagging (Bootstrap Aggregating)

Bagging 的工作流程非常直观:自助采样 (Bootstrap) + 聚合 (Aggregating)

  1. 采样 (Bootstrap): 从原始训练集 D 中,进行有放回地随机抽样,生成 T 个大小相同的新训练集 D_1, D_2, ..., D_T
  2. 训练 (Train): 在每个新训练集 D_t 上,独立地、并行地训练一个基学习器 h_t
  3. 聚合 (Aggregate):
    • 分类: 简单投票
    • 回归: 简单平均

Bagging 工作流程图

Bagging 工作流程图 图示主题为“Bagging 工作流程图”。主要标签包括:Bagging 流程 (Bootstrap Aggregating)、原始训练集 D、1. 自助采样 (Bootstrap)、训练集 D₁、训练集 D₂、训练集 Dₙ、2. 并行训练 (Parallel Training)、学习器 h₁;这些元素共同展示该页的关系、比较或流程。 Bagging 流程 (Bootstrap Aggregating) 1. 自助采样 (Bootstrap) 原始训练集 D 训练集 D₁ 训练集 D₂ 训练集 Dₙ 2. 并行训练 学习器 h₁ 学习器 h₂ 学习器 hₙ 3. 聚合 最终模型 H

Bagging 的魔力:降低方差

  • 方差 (Variance):模型在不同训练集上预测结果的波动程度。高方差意味着模型对训练数据过于敏感,容易过拟合 (Overfitting)
  • Bagging 为何有效:
    • 每个基学习器只看到了部分数据,它们各自的过拟合方向可能不同。

    • 通过对这些不同方向的错误进行平均或投票,总体的波动性被有效平滑,从而降低了方差。

  • 最成功的应用随机森林 (Random Forest)

机制二:Boosting (提升法)

Boosting 是一族将弱学习器“提升”为强学习器的算法,它采用的是一种串行的、迭代的方式。

  1. 初始化: 赋予所有训练样本相同的权重。
  2. 迭代训练 (t=1 to T):
    1. 在当前带权重的样本集上,训练一个弱学习器 h_t
    2. 增大h_t 预测错误的样本的权重。
    3. 减小h_t 预测正确的样本的权重。
  3. 最终组合: 最终的强学习器是所有弱学习器的加权组合

Boosting 工作流程图

Boosting 工作流程图 图示主题为“Boosting 工作流程图”。主要标签包括:Boosting 算法核心流程、初始数据、(等权重 w₁)、弱学习器 h₁、弱学习器 h₂、...、弱学习器 hₙ、根据 h₁ 的误差,更新数据权重为 w₂;这些元素共同展示该页的关系、比较或流程。 Boosting 算法核心流程 初始数据 等权重 w₁ 弱学习器h₁ 弱学习器h₂ 学习器hₙ h₁ 的误差决定 h₂ 的样本权重 最终强学习器 H(x) H(x) = sign(Σ αᵢhᵢ(x)) 加权组合

Boosting 的核心:降低偏差

  • 偏差 (Bias):模型的预测值与真实值之间的系统性差距。高偏差意味着模型欠拟合 (Underfitting),没有学到数据的基本规律。
  • Boosting 为何有效:
    • 每一轮新的学习器都被迫去关注上一轮学习器“搞不定”的那些困难样本。

    • 这个过程不断地修正模型的系统性错误,逐步减小偏差。

  • 著名算法AdaBoost, Gradient Boosting Machines (GBM), XGBoost

机制三:Stacking (堆叠法)

Stacking 是一种更精巧的组合策略,它试图学习如何“聪明地”组合基学习器的预测,而不是简单地投票或平均。

  1. 第一层 (Level 0): 训练多个不同的基学习器。将它们的预测结果作为新的特征。
  2. 第二层 (Level 1): 训练一个“元学习器 (Meta-Learner)”,它的输入是第一层模型的预测,输出是最终的预测。

Stacking 工作流程图

Stacking 工作流程图 图示主题为“Stacking 工作流程图”。主要标签包括:Stacking (堆叠法) 流程图、原始训练集、Level 0: 基学习器 (Base Learners)、模型 A (SVM)、模型 B (KNN)、模型 C (RF)、新特征集 (New Features from Predictions)、Level 1: 元学习器 (Meta Learner);这些元素共同展示该页的关系、比较或流程。 Stacking (堆叠法) 流程图 原始训练集 Level 0: 基学习器 模型 A (SVM) 模型 B (KNN) 模型 C (RF) 新特征集(模型预测) Level 1: 元学习器 元模型(逻辑回归)

Stacking 的优势:模型融合

  • 核心思想: Stacking 不是简单地组合预测,而是训练一个元模型来学习在什么情况下应该更相信哪个基模型
  • 例如: 元模型可能会学到:“如果模型A和模型B的预测很接近,但模型C的预测差很远,那么最终结果应该更倾向于A和B的平均值”。
  • 应用场景: 在数据科学竞赛(如 Kaggle)中非常流行,能够将多个高性能模型的优势融合在一起,榨取最后的性能提升。

Part 3: 最受欢迎的“积木”——决策树

为什么我们花这么多时间讨论决策树?因为它是迄今为止最常用、最成功的集成学习基学习器

决策树:天生的集成学习“好材料”

  • 优点:
    • 非线性,能捕捉复杂关系。
    • 模型可解释性强(单个树)。
    • 训练速度相对较快。
  • 缺点:
    • 非常容易过拟合,单个决策树的性能往往不稳定(高方差)。

绝配

决策树的高方差特性,恰好能被 Bagging (如随机森林) 通过平均来有效抑制!决策树的“弱小”(通过限制深度)又使其成为 Boosting 的完美“原料”。

决策树如何做出决策?

决策树通过一系列“是/否”问题,将复杂的数据集不断划分,直到每个子集都足够“纯净”。

  • 根节点 (Root Node): 代表整个数据集。
  • 内部节点 (Internal Node): 代表一个特征上的判断(一个问题)。
  • 分支 (Branch): 代表这个判断的输出(问题的答案)。
  • 叶节点 (Leaf Node): 代表最终的决策类别预测值

决策树剖析图

决策树剖析图 图示主题为“决策树剖析图”。主要标签包括:决策树结构 (Decision Tree Structure)、场景是教室吗?、有老师吗?、自习、上课、是 (Yes)、否 (No)、根节点 (Root Node);这些元素共同展示该页的关系、比较或流程。 决策树结构 (Decision Tree Structure) 根节点场景是教室吗? 内部节点有老师吗? 叶节点自习 上课 自习 分支承载“是 / 否”结果

核心问题:如何选择“最好的”分裂?

决策树生长的关键,在于每一步都要选择一个最优的特征来分裂数据。

“最优”的标准是:分裂之后,各个子集的“纯度”最高

“纯度”越高,意味着不确定性越小,分类越明确。

纯度 (Purity) 的直观理解

数据集纯度示意图 三个容器代表数据集。第一个容器混合了两种颜色的球,代表低纯度。第二个大部分是同一种颜色,代表中等纯度。第三个全是同一种颜色,代表高纯度。 低纯度 中等纯度 高纯度

如何量化“纯度”?

我们用两个主要指标来衡量“不纯度”或“混乱程度”:

  1. 信息熵 (Information Entropy) -> 用于 ID3, C4.5 算法
  2. 基尼不纯度 (Gini Impurity) -> 用于 CART 算法

目标都是一样的:选择一个分裂,使得分裂后子节点的不纯度加权和最小

度量纯度之一:信息熵

  • 信息熵 H(D) 是度量一个数据集 D 不确定性混乱程度的指标。
  • 熵越大,表示数据集越混乱(混合的类别越多)。
  • 熵越小,表示数据集越纯净(大部分样本属于同一类别)。

对于一个有 K 个类别的数据集 D,其信息熵定义为:

\[ \large{H(D) = - \sum_{k=1}^{K} p_k \log_2(p_k)} \]

其中 p_k 是第 k 类样本所占的比例。

熵的数值特性

假设一个数据集中只有两类:正例 (+)反例 (-)

场景 p_+ p_- H(D) 纯度
完全纯净 1.0 0.0 -1*log2(1) - 0 = 0 最高
混合 0.8 0.2 -0.8*log2(0.8) - 0.2*log2(0.2) ≈ 0.72 较低
最混乱 0.5 0.5 -0.5*log2(0.5) - 0.5*log2(0.5) = 1 最低

当正反例各占一半时,不确定性最大,熵达到最大值1。

分裂标准1:信息增益 (Information Gain)

ID3 算法使用的分裂标准是信息增益

  • 思想: 计算用某个属性 A 分裂数据集 D 后,系统不确定性(熵)下降了多少。下降得越多,说明 A 这个属性用来分类的效果越好。

  • 公式:

    \[ \large{\text{Gain}(D, A) = H(D) - H(D|A)} \]

    其中 H(D) 是分裂前的熵,H(D|A) 是分裂后各子集熵的加权平均,称为条件熵

  • 决策: 选择使 Gain(D, A) 最大的属性 A 作为分裂节点。

案例:计算“场景”属性的信息增益 (1/3)

让我们用一个例子,手动计算属性“场景”的信息增益。

数据集D: 15个样本, 9个“上课”(正例), 6个“自习”(反例)。

1. 计算分裂前的总熵 H(D):

\[ \large{H(D) = - \frac{9}{15} \log_2\left(\frac{9}{15}\right) - \frac{6}{15} \log_2\left(\frac{6}{15}\right) \approx 0.971} \]

案例:计算“场景”属性的信息增益 (2/3)

2. 计算按“场景”分裂后的条件熵 H(D|场景):

属性“场景”有3个取值:教室(7个)、宿舍(3个)、户外(5个)。

  • 教室 (D1): 5正, 2反. \(\large{H(D_1) = - \frac{5}{7}\log_2(\frac{5}{7}) - \frac{2}{7}\log_2(\frac{2}{7}) \approx 0.863}\)
  • 宿舍 (D2): 1正, 2反. \(\large{H(D_2) = - \frac{1}{3}\log_2(\frac{1}{3}) - \frac{2}{3}\log_2(\frac{2}{3}) \approx 0.918}\)
  • 户外 (D3): 3正, 2反. \(\large{H(D_3) = - \frac{3}{5}\log_2(\frac{3}{5}) - \frac{2}{5}\log_2(\frac{2}{5}) \approx 0.971}\)

条件熵是加权平均:

\[ \large{H(D|\text{场景}) = \frac{7}{15}H(D_1) + \frac{3}{15}H(D_2) + \frac{5}{15}H(D_3) \approx 0.910} \]

案例:计算“场景”属性的信息增益 (3/3)

3. 计算信息增益 Gain(D, 场景):

\[ \large{\text{Gain}(D, \text{场景}) = H(D) - H(D|\text{场景})} \]

\[ \large{\approx 0.971 - 0.910 = 0.061} \]

ID3 算法会为所有属性(场景、老师、学生等)都计算信息增益,然后选择那个增益最大的属性作为第一个分裂节点。

信息增益的缺陷与改进

  • 问题:

    • 信息增益准则对可取值数目较多的属性有所偏好。

    • 例如,如果用“学号”作为属性,每个学生一个分支,则每个叶子节点都无比纯净,信息增益会非常大,但这显然是过拟合。

  • C4.5 算法的改进: 使用信息增益率 (Gain Ratio) 来校正这种偏好。

  • 公式:

    \[ \large{\text{GainRatio}(D, A) = \frac{\text{Gain}(D, A)}{IV(A)}} \]

    其中 IV(A) 是属性A的固有值 (Intrinsic Value),属性A的取值越多,IV(A) 通常越大,起到了惩罚作用。

度量纯度之二:基尼不纯度

CART (Classification and Regression Tree) 算法使用基尼指数来选择分裂属性。

  • 基尼不纯度 Gini(D): 从数据集 D 中随机抽取两个样本,其类别标记不一致的概率。
  • 基尼指数越小,数据集的纯度越高。

\[ \large{\text{Gini}(D) = \sum_{k=1}^{K} p_k (1 - p_k) = 1 - \sum_{k=1}^{K} p_k^2} \]

分裂标准2:基尼指数增益

  • 思想: 选择一个属性 A 进行分裂,使得分裂后的基尼指数加权和最小

  • 公式: 对于属性 A 的某个划分 a,分裂后的基尼指数为:

    \[ \large{\text{GiniIndex}(D|A=a) = \sum_{v=1}^{V} \frac{|D^v|}{|D|} \text{Gini}(D^v)} \]

  • 决策: 选择使 GiniIndex(D|A=a) 最小的属性 A 和划分 a

  • 优势: 相比于熵的计算,基尼指数不涉及对数运算,计算上更高效。

案例:计算基尼指数 (1/2)

我们用同样的数据计算属性“场景=教室”这个二分划分的基尼指数。

数据集D: 15个样本。

划分:

  • D1 (“场景=教室”): 7个样本 (5正, 2反)
  • D2 (“场景!=教室”): 8个样本 (4正, 4反)

1. 计算 D1D2 的基尼指数:

\[ \large{\text{Gini}(D_1) = 1 - \left(\frac{5}{7}\right)^2 - \left(\frac{2}{7}\right)^2 \approx 0.408} \]

\[ \large{\text{Gini}(D_2) = 1 - \left(\frac{4}{8}\right)^2 - \left(\frac{4}{8}\right)^2 = 0.5} \]

案例:计算基尼指数 (2/2)

2. 计算该划分的加权基尼指数:

\[ \large{\text{GiniIndex}(D|\text{场景=教室}) = \frac{7}{15}\text{Gini}(D_1) + \frac{8}{15}\text{Gini}(D_2)} \]

\[ \large{\approx \frac{7}{15}(0.408) + \frac{8}{15}(0.5) \approx 0.190 + 0.267 = 0.457} \]

CART 算法会遍历所有属性的所有可能二分点,计算它们的加权基尼指数,并选择最小的那个作为最优分裂点。

决策树的现实问题:过拟合

  • 如果不对决策树的生长加以限制,它会一直分裂,直到每个叶子节点只包含一个样本。

  • 这时训练集上的错误率为0,但模型会变得极其复杂,对新数据的泛化能力很差。

决策树的现实问题:过拟合 图示主题为“决策树的现实问题:过拟合”。主要标签包括:决策树过拟合过程示意、欠拟合、X1、A、B、高偏差、模型过于简单、良好拟合;这些元素共同展示该页的关系、比较或流程。 决策树过拟合过程示意 欠拟合 X1 A B 高偏差 · 过简 良好拟合 X1 X2 X3 A B C D 均衡泛化 过拟合 X1 高方差 · 学到噪声 模型复杂度提升 → 过拟合:训练表现极佳,但泛化能力差 Overfitting: strong training fit, weak generalization

解决方案:剪枝 (Pruning)

为了防止过拟合,我们需要对决策树进行“剪枝”。

  • 预剪枝 (Pre-pruning): 在树的生长过程中,如果一个分裂不能带来泛化性能的提升(例如,在验证集上性能下降),就提前停止分裂
    • 优点: 速度快,生成的树更小。
    • 缺点: 可能过于“短视”,错过一些好的分裂组合。
  • 后剪枝 (Post-pruning): 先生成一棵完整的决策树,然后自底向上地考察节点。如果剪掉一个子树能提升泛化性能,就把它剪掉。
    • 优点: 通常效果更好,更不容易错过好的结构。
    • 缺点: 计算开销更大。

核心内容机制摘要:把失败诊断接回可检验证据

失败诊断 核心修复 可检验预期
单树对样本扰动敏感、方差高 Bagging:并行重采样,多树平均 排序应更稳定,但 0.5 阈值不保证有召回
单树系统性漏掉难样本、偏差高 Boosting:串行提高错分样本权重 可能改善难样本排序,也必须确定验证阈值
  • 转折预测: 若随机森林 AUC 略升,是否足以宣布“下行预警可用”?先答,再进入同一数据切分的模型比较。
  • 揭示: 不够;还要比较 AP(平均精度)、正类召回、混淆矩阵和验证期阈值。

  • 学习提示:

    • 核心内容直接进入同一 A 股案例检查

    • 需要完整随机森林构造与 AdaBoost 推导者进入下面的拓展内容,并在推导末尾向前返回案例检查。

拓展内容 C:随机森林构造与 AdaBoost 完整推导

学习提示:下面进入同一组 A 股数据的模型比较。随机森林构造与 AdaBoost 完整推导可在课后选学。

Part 4: 强大的集成模型

现在,我们把决策树这个“积木”和 Bagging/Boosting 这两种“搭建方法”结合起来。

Bagging + 决策树 = 随机森林

随机森林 (Random Forest) 是 Bagging 的一个非常成功的扩展和改进,它在 Bagging 的基础上,额外引入了特征随机性

构建过程:

  1. 进行 T自助采样 (Bootstrap),得到 T 个训练子集。
  2. 对每个子集训练一棵决策树。在训练这棵树的每个节点进行分裂时:
    • 不再是从所有 d 个特征中选择最优特征。
    • 而是随机选择 l 个特征 (l < d),然后从这 l 个特征中选择最优的。
  3. 最终将 T 棵树通过投票或平均进行预测。

随机森林的双重随机性

随机森林的双重随机性示意图 展示了随机森林的两个随机来源:行采样(自助采样)和列采样(特征随机选择)。 原始数据集 F1F2...Fk... 1. 行采样 训练子集 (用于一棵树) F1F2...Fk... 2. 列采样 (特征随机)

随机森林为何更强大?“求同存异”

  • 特征随机性的作用: 降低了森林中树与树之间的相关性

  • 为何降低相关性很重要:

    • 如果不随机选择特征,森林中的每棵树在顶层节点分裂时,都很可能会选择同一个最强的特征,导致所有树的结构都非常相似。

    • 这样的集成,效果就大打折扣。

  • 通过强制每棵树在分裂时只能考虑一部分特征,随机森林让每棵树都从不同的“视角”去学习,变得“各有所长”。

  • 当它们组合在一起时,就能形成更强大的互补效应,进一步降低整体的方差。

Boosting + 决策树 = AdaBoost

AdaBoost (Adaptive Boosting) 是 Boosting 家族最经典的算法。

核心思想的迭代循环:

  1. 训练一个弱学习器 h_t
  2. 评估 h_t 的表现,赋予它一个权重 α_t (表现越好,权重越高)。
  3. 根据 h_t 的预测结果,更新训练样本的权重 w (预测错误的样本权重变大)。
  4. 重复此过程。

最终模型是所有弱学习器的加权组合。

AdaBoost 算法流程详解 (1/4): 初始化

目标

训练一个强分类器 \(\large{H(x) = sign(\sum \alpha_t h_t(x))}\)

1. 初始化: 所有 N 个训练样本的权重被初始化为相等:

\[ \large{w_{1,n} = 1/N} \quad \text{for } n=1, \dots, N \]

AdaBoost 算法流程详解 (2/4): 训练与评估

循环 for t = 1 to T:

  1. 训练弱学习器: 使用当前带权重的训练集训练一个弱学习器 h_t(x),使其最小化加权错误。

  2. 计算加权错误率 ε_t: h_t 分类错误的样本的权重之和。

    \[ \large{\epsilon_t = \sum_{n=1}^{N} w_{t,n} I(h_t(x_n) \ne y_n)} \]

  3. 计算该学习器的权重 α_t:

    \[ \large{\alpha_t = \frac{1}{2} \ln\left(\frac{1 - \epsilon_t}{\epsilon_t}\right)} \]

    错误率 ε_t 越低,α_t 越大,话语权越大。

AdaBoost 算法流程详解 (3/4): 更新权重

  1. 更新样本权重 w: 这是算法的核心步骤。

    \[ \large{w_{t+1, n} = \frac{w_{t,n} \exp(-\alpha_t y_n h_t(x_n))}{Z_t}} \]

    (\(Z_t\) 是归一化因子,确保新权重和为1)

    直观解释:

    • 如果样本 n 被分类正确 (\(y_n h_t(x_n) = 1\)),指数项为负,w 变小。
    • 如果样本 n 被分类错误 (\(y_n h_t(x_n) = -1\)),指数项为正,w 变大。

AdaBoost 算法流程详解 (4/4): 最终组合

3. 最终输出: 将所有 T 个弱学习器按照其权重 α_t 进行加权投票,得到最终的强分类器:

\[ \large{H(x) = \text{sign}\left(\sum_{t=1}^{T} \alpha_t h_t(x)\right)} \]

Part 5:A 股下季大幅下行风险实战

选学内容结束:下面回到模型选择与测试结果的解读。

任务定义

  • 用公司在季度 (t) 已记录的估值特征,预测本地字段 market_cap 表示的总市值在 (t+1) 季度是否下降至少 20%。

  • RQData 股票财务字段文档market_cap 定义为总股本乘 A 股未复权收盘价,并把 a_share_market_val_in_circulation 单列为流通 A 股市值;

  • 本地文件保留这两个不同字段。

  • 这是教学用的市场下行代理标签,不是信用违约、财务困境或监管认定。

实战数据与评价方法

  • 本地文件:本地 data/stock/valuation_factors_quarterly_15_years.h5
  • HDF key:valuation_factors;索引为 order_book_id, date
  • 样本期:2015-01-01 至 2024-12-31;特征单位沿用本地数据字典,市值取自然对数
  • 切分:训练期至 2021 年;验证期 2022 年;测试期 2023—2024 年,禁止随机打乱
  • 模型:单树、随机森林、AdaBoost;三者共享完全相同的样本和特征

实战第1步:加载本地真实数据并构造标签

标签必须由下一季度观测构造;所有输入只来自当前季度,从定义上隔离未来信息。

Code
from pathlib import Path  # 使用跨平台路径对象定位本地数据
import numpy as np  # 提供对数变换与有限值检查
import pandas as pd  # 读取HDF5并构造公司—季度面板

# 公网下载:https://assets.qiufei.site/data/stock/valuation_factors_quarterly_15_years.h5
# 下载后把下一行改为本机文件位置;按课程结构存放时可用 Path("data/stock/valuation_factors_quarterly_15_years.h5")。
# Windows:Path(r"C:\qiufei\data\stock\valuation_factors_quarterly_15_years.h5")
# macOS:Path("/Users/你的用户名/data/stock/valuation_factors_quarterly_15_years.h5")
# Linux:Path("/home/你的用户名/data/stock/valuation_factors_quarterly_15_years.h5")
valuation_path = Path("/home/ubuntu/r2_data_mount/data/stock/valuation_factors_quarterly_15_years.h5")
valuation_raw = pd.read_hdf(valuation_path, key='valuation_factors')  # 从固定HDF key读取本地观测
valuation_panel = valuation_raw.reset_index()  # 将公司代码与日期索引还原为普通字段
valuation_panel['date'] = pd.to_datetime(valuation_panel['date'])  # 统一季度日期类型以便时间切分
valuation_panel = valuation_panel.query("'2015-01-01' <= date <= '2024-12-31'").copy()  # 固定可重复样本期
valuation_panel = valuation_panel.sort_values(['order_book_id', 'date'])  # 按公司和时间排序以构造下一季标签
valuation_panel['target_date'] = valuation_panel.groupby('order_book_id')['date'].shift(-1)  # 保存标签真正实现的日期,而非只保存下一条记录
valuation_panel['next_market_cap'] = valuation_panel.groupby('order_book_id')['market_cap'].shift(-1)  # 先取得同一公司的下一条记录
current_quarter = valuation_panel['date'].dt.to_period('Q')  # 将特征日期映射到自然季度
target_quarter = valuation_panel['target_date'].dt.to_period('Q')  # 将标签日期映射到自然季度
valuation_panel['is_exact_next_quarter'] = target_quarter == current_quarter + 1  # 只允许严格相邻季度,排除跨季缺口
valuation_panel['next_quarter_return'] = valuation_panel['next_market_cap'] / valuation_panel['market_cap'] - 1  # 计算下一季市值变动率
valuation_panel['downside_risk'] = (valuation_panel['next_quarter_return'] <= -0.20).astype(int)  # 定义至少下跌20%的代理标签
Code
feature_columns = ['pe_ratio_ttm', 'pb_ratio_lf', 'dividend_yield_ttm', 'ev_to_ebitda_ttm', 'market_cap']  # 只使用当前季度可见字段
model_panel = valuation_panel.loc[valuation_panel['is_exact_next_quarter']].dropna(subset=feature_columns + ['next_market_cap', 'target_date']).copy()  # 排除“下一条但非下一季”的记录
model_panel['log_market_cap'] = np.log(model_panel['market_cap'].clip(lower=1))  # 用对数市值降低量级偏斜
feature_columns = ['pe_ratio_ttm', 'pb_ratio_lf', 'dividend_yield_ttm', 'ev_to_ebitda_ttm', 'log_market_cap']  # 更新模型字段清单
model_panel[feature_columns] = model_panel[feature_columns].replace([np.inf, -np.inf], np.nan)  # 将无限估值倍数标记为缺失
model_panel = model_panel.dropna(subset=feature_columns)  # 保留可进入模型的完整观测
input_feature_matrix = model_panel[feature_columns]  # 构造具有真实业务语义的特征矩阵
target_values = model_panel['downside_risk']  # 提取下一季度大幅下行标签
assert (model_panel['target_date'].dt.to_period('Q') == model_panel['date'].dt.to_period('Q') + 1).all()  # 确认每个标签都来自严格下一季度

时间切分单独成块,强调它是实证设计的一部分,而非可随意替换的工具参数。

Code
train_mask = (model_panel['date'] <= '2021-12-31') & (model_panel['target_date'] < '2022-01-01')  # 训练标签也必须在验证期前实现
validation_mask = model_panel['date'].between('2022-01-01', '2022-12-31') & (model_panel['target_date'] < '2023-01-01')  # 验证标签必须在测试期前实现
test_mask = model_panel['date'].between('2023-01-01', '2024-12-31')  # 保留最终两年为伪样本外测试
training_features, y_train = input_feature_matrix.loc[train_mask], target_values.loc[train_mask]  # 提取训练样本且不打乱时间
validation_features, y_validation = input_feature_matrix.loc[validation_mask], target_values.loc[validation_mask]  # 提取验证样本
testing_features, y_test = input_feature_matrix.loc[test_mask], target_values.loc[test_mask]  # 提取最终测试样本
assert model_panel.loc[train_mask, 'target_date'].max() < model_panel.loc[validation_mask, 'date'].min()  # 禁止训练标签跨入验证期
assert model_panel.loc[validation_mask, 'target_date'].max() < model_panel.loc[test_mask, 'date'].min()  # 禁止验证标签跨入测试期
split_summary = pd.DataFrame({'n': [train_mask.sum(), validation_mask.sum(), test_mask.sum()], 'feature_end': [model_panel.loc[train_mask, 'date'].max(), model_panel.loc[validation_mask, 'date'].max(), model_panel.loc[test_mask, 'date'].max()], 'target_end': [model_panel.loc[train_mask, 'target_date'].max(), model_panel.loc[validation_mask, 'target_date'].max(), model_panel.loc[test_mask, 'target_date'].max()]}, index=['训练', '验证', '测试'])  # 同时报告特征与标签实现边界
display(split_summary)  # 使相邻季度筛选与标签日期清除可以检查
n feature_end target_end
训练 50811 2021-09-30 2021-12-31
验证 8880 2022-06-30 2022-09-30
测试 9695 2024-09-30 2024-12-31

实战第1步:评价边界

  • 在打开测试期前,预先确定随机森林为准备实际使用的模型,并只在 2022 验证期以“F2 最大”选择其阈值。

  • F2 对召回的权重高于精确率,但不等于货币化 FP/FN 成本。

  • 单树与 AdaBoost 的测试结果仅作事后描述,不能用于选择“胜者”或替换确定分析步骤。

实战第2步:训练基准模型 - 单个决策树

我们先训练一个决策树作为性能基准。为了防止过拟合,我们限制其最大深度为5。

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score  # 同时评估排序能力与少数类识别

# --- 训练模型 ---
# max_depth=5 限制树的深度,防止过拟合
dt_clf = DecisionTreeClassifier(max_depth=5, random_state=42)
# 执行 `dt_clf.fit`,生成当前步骤需要的结果或可视化。
dt_clf.fit(training_features, y_train)
dt_clf.get_params()['max_depth']  # 只核对已拟合基准模型设置;此时仍不查看测试标签
5

实战第3步:训练 Bagging 模型 - 随机森林

现在,让我们看看由100棵决策树组成的“森林”表现如何。n_estimators 就是基学习器的数量 T

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.ensemble import RandomForestClassifier
# --- 训练模型 ---
# n_estimators=100: 构建100棵树
# n_jobs=-1: 使用所有CPU核心并行计算,加快速度
rf_clf = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42, n_jobs=-1)
# 执行 `rf_clf.fit`,生成当前步骤需要的结果或可视化。
rf_clf.fit(training_features, y_train)
from sklearn.metrics import precision_recall_curve  # 只用验证期确定随机森林分类阈值
validation_probability = rf_clf.predict_proba(validation_features)[:, 1]  # 在测试访问前生成验证期概率
validation_precision, validation_recall, candidate_thresholds = precision_recall_curve(y_validation, validation_probability)  # 枚举验证期 PR 分类阈值
validation_f2 = 5 * validation_precision[:-1] * validation_recall[:-1] / (4 * validation_precision[:-1] + validation_recall[:-1] + 1e-12)  # 让召回权重高于精确率
selected_threshold = candidate_thresholds[np.argmax(validation_f2)]  # 在任何测试预测前确定 F2 最高阈值
print(f'验证期确定随机森林阈值: {selected_threshold:.4f}')  # 显式记录开测试前的唯一选择
验证期确定随机森林阈值: 0.0504

观察规则:只有当实际输出更高时才称为提升;单次时间切分不构成“显著优于”的统计证据。

实战第4步:训练 Boosting 模型 - AdaBoost

最后,我们来试试 AdaBoost。它也是基于决策树,但采用的是串行的、关注错误样本的提升策略。

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
from sklearn.ensemble import AdaBoostClassifier
# --- 训练模型 ---
# AdaBoost 通常使用比较浅的树(“树桩”),这里用max_depth=1
base_estimator = DecisionTreeClassifier(max_depth=1)
ada_clf = AdaBoostClassifier(
    estimator=base_estimator,
    n_estimators=100,
    random_state=42
)
# 执行 `ada_clf.fit`,生成当前步骤需要的结果或可视化。
ada_clf.fit(training_features, y_train)
ada_clf.get_params()['n_estimators']  # 只核对辅助比较设置;此时仍不查看测试标签
100

观察规则

比较实际 ROC-AUC 与 AP(平均精度);若排序不同,应解释指标为何强调不同错误,而不是预设胜者。

实战第5步:阈值已确定,再打开一次测试检查

  • 前一步已经只用 2022 年验证期确定随机森林阈值。

  • 现在才首次生成测试预测:随机森林接受最终评价;单棵树与 AdaBoost 只作辅助比较,不能改变已经选好的模型。

Code
from sklearn.dummy import DummyClassifier  # 建立只预测多数类的最低基线
from sklearn.metrics import confusion_matrix, recall_score  # 量化一次测试检查中的事件识别

majority_model = DummyClassifier(strategy='most_frequent')  # 固定多数类预测规则
majority_model.fit(training_features, y_train)  # 只从训练期学习多数类
majority_prediction = majority_model.predict(testing_features)  # 在同一测试期产生基线预测
majority_probability = majority_model.predict_proba(testing_features)[:, 1]  # 取得基线正类概率
y_pred_dt = dt_clf.predict(testing_features)  # 在确定后生成单树描述性测试预测
y_prob_dt = dt_clf.predict_proba(testing_features)[:, 1]  # 在确定后生成单树描述性排序分数
y_pred_rf = rf_clf.predict(testing_features)  # 在确定后生成准备实际使用的模型默认分类阈值
y_prob_rf = rf_clf.predict_proba(testing_features)[:, 1]  # 在确定后生成准备实际使用的模型排序分数
y_pred_ada = ada_clf.predict(testing_features)  # 在确定后生成 AdaBoost 描述性测试预测
y_prob_ada = ada_clf.predict_proba(testing_features)[:, 1]  # 在确定后生成 AdaBoost 描述性排序分数
threshold_prediction = (y_prob_rf >= selected_threshold).astype(int)  # 将确定阈值应用于测试期
auc_dt = roc_auc_score(y_test, y_prob_dt)  # 为后续事后图保存单树测试 AUC
auc_rf = roc_auc_score(y_test, y_prob_rf)  # 为后续事后图保存森林测试 AUC
auc_ada = roc_auc_score(y_test, y_prob_ada)  # 为后续事后图保存 AdaBoost 测试 AUC
evaluation_rows = []  # 汇总可直接比较的测试证据
for model_name, prediction, probability in [('多数类基线', majority_prediction, majority_probability), ('单树@0.5', y_pred_dt, y_prob_dt), ('随机森林@0.5', y_pred_rf, y_prob_rf), ('[email protected]', y_pred_ada, y_prob_ada), ('随机森林@验证阈值', threshold_prediction, y_prob_rf)]:  # 比较同一测试样本
    true_negative, false_positive, false_negative, true_positive = confusion_matrix(y_test, prediction).ravel()  # 展开混淆矩阵四格
    evaluation_rows.append([model_name, roc_auc_score(y_test, probability), average_precision_score(y_test, probability), recall_score(y_test, prediction, zero_division=0), true_negative, false_positive, false_negative, true_positive])  # 保存排序与分类指标
evaluation_table = pd.DataFrame(evaluation_rows, columns=['模型/阈值', 'ROC-AUC', 'AP(平均精度)', '召回率', 'TN', 'FP', 'FN', 'TP'])  # 形成可核对结果表
print(f'验证期确定阈值: {selected_threshold:.4f}')  # 报告阈值来源而非事后调测试集
display(evaluation_table.round(4))  # 展示多数类基线、混淆矩阵与少数类指标

第5步证据:排序不等于可用分类阈值

模型/阈值 ROC-AUC AP(平均精度) 正类召回
多数类基线 0.5000 0.0483 0.0000
单树 @ 0.5 0.7284 0.0972 0.0000
随机森林 @ 0.5 0.7258 0.1051 0.0000
AdaBoost @ 0.5 0.7304 0.1077 0.0000
随机森林 @ 验证阈值 0.0504 0.7258 0.1051 0.8996
  • 真实混淆矩阵: 多数类与随机森林 @ 0.5 均为 TN=9,227、FP=0、FN=468、TP=0;验证阈值下为 TN=3,005、FP=6,222、FN=47、TP=421,准确率降至 0.3534。

  • 决策含义:

    • 本例事先用验证期 F2 最大化确定阈值,因而明确偏向召回;执行结果表明高召回伴随大量误报。

    • 这不是货币成本最小化,也不保证适用于其他数据;若要用 FP/FN 成本决策,必须先明确成本比并在验证期重新确定。

    • 测试期只用于最终检查。

实战第6步:事后描述,不用于模型选择

图形只汇报本次固定测试期的结果。ROC-AUC 衡量整体排序能力;AP(平均精度) 更关注稀少的“大幅下行”样本。

Code
results = pd.DataFrame({'Model': ['单个决策树', '随机森林', 'AdaBoost'], 'AUC': [auc_dt, auc_rf, auc_ada]})  # 汇总三种模型的实际测试期AUC
results = results.sort_values('AUC', ascending=True)  # 按数值排序以便水平柱状图从低到高展示
colors = ['#86BBD8', '#F26419', '#33658A']  # 固定三种模型的高对比配色
Code
import matplotlib.pyplot as plt  # 导入绘图库以生成模型比较图
import seaborn as sns  # 导入统计绘图主题工具
sns.set_theme(style="whitegrid", context="talk")  # 设定适合投影的白底主题
plt.rcParams['font.family'] = 'Source Han Serif SC'  # 使用本机已验证可用的中文字体,避免标题和类别标签缺字
plt.rcParams['axes.unicode_minus'] = False  # 保留中文字体环境下的负号显示
fig, ax = plt.subplots(figsize=(10, 6), dpi=100)  # 创建单一水平柱状图画布

# 绘图
bars = ax.barh(results['Model'], results['AUC'], color=colors, height=0.6)
# 执行 `ax.set_xlim`,生成当前步骤需要的结果或可视化。
ax.set_xlim(0.45, 1.0)
# 执行 `ax.set_xlabel`,生成当前步骤需要的结果或可视化。
ax.set_xlabel('ROC AUC Score', fontsize=20, labelpad=10)
# 执行 `ax.set_title`,生成当前步骤需要的结果或可视化。
ax.set_title('模型性能对比:下一季度大幅下行风险', fontsize=20, pad=20, weight='bold')
# 在柱状图上显示数值
for bar in bars:  # 遍历当前教学对象以完成重复计算。
    width = bar.get_width()
    # 执行 `ax.text`,生成当前步骤需要的结果或可视化。
    ax.text(width + 0.0005, bar.get_y() + bar.get_height()/2, f'{width:.4f}',
            ha='left', va='center', fontsize=20, weight='bold')
# 美化图表
ax.spines[['top', 'right', 'bottom']].set_visible(False)
# 执行 `ax.xaxis.grid`,生成当前步骤需要的结果或可视化。
ax.xaxis.grid(True, linestyle='--', which='major', color='grey', alpha=0.5)
# 执行 `ax.yaxis.grid`,生成当前步骤需要的结果或可视化。
ax.yaxis.grid(False)
# 执行 `ax.tick_params`,生成当前步骤需要的结果或可视化。
ax.tick_params(axis='y', labelsize=14, length=0)
# 执行 `ax.tick_params`,生成当前步骤需要的结果或可视化。
ax.tick_params(axis='x', labelsize=12)
# 执行 `plt.tight_layout`,生成当前步骤需要的结果或可视化。
plt.tight_layout()  # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
横向柱状图比较单棵决策树、随机森林和AdaBoost在2023至2024测试期的ROC-AUC。
Figure 1: 三种模型在中国上市公司下一季度大幅下行预测任务上的测试期ROC-AUC

结果分析:测试比较不能决定胜者

  • Figure 1 只描述固定测试期中三个预先拟合模型设置的 ROC-AUC;它不是模型选择证据。

  • 准备实际使用的模型仍是测试前确定的随机森林及其验证期阈值。

本次比较不能证明集成模型普遍优于单树,也不能把一次差值称为“显著”。它展示的是一条可重复的比较分析步骤。

集成学习的另一大优势:可解释性

集成模型,特别是基于树的模型,还有一个巨大的优点:它们可以告诉我们,哪些输入特征对于做出最终决策最重要

这在经济和金融应用中至关重要。我们不仅想预测,更想理解预测背后的驱动因素。

可视化特征重要性

让我们读取本次拟合的随机森林实际使用的字段,而不是预先写入变量名称。

Code
# 导入依赖以支持本页的数据处理、建模或可视化。
import numpy as np
# 获取特征重要性
importances = rf_clf.feature_importances_
feature_names = input_feature_matrix.columns
df_importance = pd.DataFrame({'feature': feature_names, 'importance': importances})
df_importance = df_importance.sort_values('importance', ascending=False).head(15)
# 设置绘图风格
fig, ax = plt.subplots(figsize=(10, 7), dpi=100)
# 绘图
sns.barplot(
    x='importance',
    y='feature',
    data=df_importance,
    palette='viridis',
    ax=ax
)
# 美化图表
ax.set_title('特征重要性分析 (来自随机森林)', fontsize=20, pad=20, weight='bold')
# 执行 `ax.set_xlabel`,生成当前步骤需要的结果或可视化。
ax.set_xlabel('相对重要性 (Mean Decrease in Impurity)', fontsize=20, labelpad=10)
# 执行 `ax.set_ylabel`,生成当前步骤需要的结果或可视化。
ax.set_ylabel('')
# 执行 `ax.spines['top'].set_visible`,生成当前步骤需要的结果或可视化。
ax.spines['top'].set_visible(False)
# 执行 `ax.spines['right'].set_visible`,生成当前步骤需要的结果或可视化。
ax.spines['right'].set_visible(False)
# 执行 `ax.tick_params`,生成当前步骤需要的结果或可视化。
ax.tick_params(axis='both', which='major', labelsize=12)
# 执行 `plt.tight_layout`,生成当前步骤需要的结果或可视化。
plt.tight_layout()  # 展示当前步骤的结果。
# 执行 `plt.show`,生成当前步骤需要的结果或可视化。
plt.show()  # 展示当前步骤的结果。
水平条形图按本次随机森林输出排列五个真实估值字段的平均不纯度下降重要性。
Figure 2: 随机森林对下一季度大幅下行风险的MDI特征重要性

商业解释:从实际字段到审慎结论

Figure 2 中只可能出现 pe_ratio_ttmpb_ratio_lfdividend_yield_ttmev_to_ebitda_ttmlog_market_cap。先读图中实际 top-k,再结合数据字典说明含义。

MDI 偏好连续、可分裂点较多或高度相关的变量;它不提供方向,也不等于因果效应。业务解释必须同时报告样本期、代理标签与这种偏置。

形成性检查:修复高方差

  • 题目: 单树训练 AUC 为 0.99、验证 AUC 为 0.66;100 棵树的误差高度相关。你会先采用哪项措施?

请先选择“更多树 / 限制树并增加随机性 / Boosting”,并用偏差—方差解释。

  • 检查 2 揭示与补救:
    • 先限制单树复杂度并通过特征随机化降低树间相关性,再评估 Bagging。

    • 若只答“更多树”,回到 Bagging 的相关性条件;若答 Boosting,回到“高训练 AUC + 低验证 AUC”的方差诊断。

形成性检查:何时先试 Boosting

  • 题目:
    • 一棵受限浅树在训练与验证期都持续漏掉同一类难样本,两个分数都偏低且间距很小;Bagging 后仍是同样的系统性漏判。

    • 先试 Bagging 还是 Boosting?

  • 揭示与补救:
    • 先试 Boosting,让后续弱学习器提高持续错分样本的权重;这里是高偏差/系统性遗漏,而非训练好、验证差的高方差。

    • 若仍答 Bagging,回到“方差修复”和“偏差修复”的对照表。

分步练习:解释实际 top-k

  • 任务: 运行重要性代码,提交 top-2 字段、字段定义、一个可检验的非因果解释,以及 MDI 的一个局限。

  • 完整示例答案:

    • 执行结果的 top-2 是 pb_ratio_lf(最新口径市净率,即价格/每股账面价值,无量纲)与 log_market_cap(本地文件原始市值货币单位取自然对数)。

    • 可检验解释是“在本样本期,这两项较高或较低的区间是否伴随不同的下一季大幅下跌率”;这只是预测关联。

    • MDI 可能偏好可切分点多的连续变量,且相关变量会分摊重要性,因此不提供方向或因果效应。

五分钟 核心内容本讲回顾

  • 检查自己是否掌握:
    • 根据高方差或系统性遗漏选择 Bagging 或 Boosting。

    • 解释为什么测试期比较不能用于反向选择模型。

    • 说明 MDI 反映预测关联,不提供因果方向。

  • 独立作答: 先诊断,再读同一测试期证据。请分别回答:
  1. 若训练 AUC 为 0.99、验证 AUC 为 0.66,说明什么?应先尝试哪类修复?
  2. 测试期中森林 AP 更高(0.1051 > 0.0972)但 ROC-AUC 略低(0.7258 < 0.7284),支持什么、不能支持什么?
  3. 本例用什么验证准则选阈值?为什么 F2 最大化不等于 FP/FN 货币成本最小化,且不能在测试期重选?
  • 参考答案与补救:
    • 训练好、验证差提示高方差,应先限制树并用 Bagging/特征随机化降低相关误差。

    • 两个排序指标给出不同次序,只能说森林在该测试期 AP 更高,不能证明它全面优于单树;任何排序指标都不保证 0.50 是合适分类阈值。

    • 本例在验证期确定 F2 最大的阈值;F2 只是召回偏重的综合指标,不是未声明的 FP/FN 成本函数。

    • 测试期只检查一次。

核心内容总结:失败—修复—证据—决策

  1. 单树“训练很好、验证变差”先诊断为高方差风险。
  2. Bagging 通过样本/特征随机化降低方差;Boosting 通过逐步纠错降低偏差。
  3. 本讲基于高方差修复与可解释性目标事先选择随机森林;单树/AdaBoost 的同窗测试指标只用于描述机制差异。
  4. 只用验证期 F2 最大化确定随机森林分类阈值;测试期负责一次最终检查,MDI 只作关联性诊断。
  • 若业务需要成本准则,必须另行预先声明并在验证期重选。

拓展内容:综合练习任务——滚动检验稳定性

  • 任务: 把测试期依次改为 2021、2022、2023、2024 年,训练集只使用此前季度;提交每年 ROC-AUC、AP(平均精度)、正类比例及 top-3 字段。

  • 完整答案提醒:

    • 逐年按时间重新拟合,完整报告四年的 ROC-AUC、AP(平均精度)、正类比例和各模型的前三项重要变量;

    • 说明 MDI 只反映预测关联,并存在偏好和重要性分摊问题;

    • 最后根据实际结果比较年度稳定性。

Code
rolling_rows = []  # 保存四个年度的伪样本外证据
for test_year in [2021, 2022, 2023, 2024]:  # 逐年向前滚动测试窗口
    rolling_test_start = pd.Timestamp(f'{test_year}-01-01')  # 确定年度测试起点
    rolling_train_mask = (model_panel['date'] < rolling_test_start) & (model_panel['target_date'] < rolling_test_start)  # 清除到测试期才实现的训练标签
    rolling_test_mask = model_panel['date'].between(f'{test_year}-01-01', f'{test_year}-12-31')  # 确定当年季度
    rolling_model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42, n_jobs=-1)  # 每折重新拟合同一模型设置
    rolling_model.fit(input_feature_matrix.loc[rolling_train_mask], target_values.loc[rolling_train_mask])  # 禁止未来年份进入训练
    rolling_probability = rolling_model.predict_proba(input_feature_matrix.loc[rolling_test_mask])[:, 1]  # 生成当年伪样本外概率
    rolling_target = target_values.loc[rolling_test_mask]  # 取得同一年度真实标签
    assert model_panel.loc[rolling_train_mask, 'target_date'].max() < model_panel.loc[rolling_test_mask, 'date'].min()  # 确认每个滚动折的标签实现边界
    importance_order = np.argsort(rolling_model.feature_importances_)[::-1][:3]  # 按当年模型实际重要性取得前三名位置
    yearly_top_three = ', '.join(input_feature_matrix.columns[importance_order])  # 将当年前三字段保存为可核对文本
    rolling_rows.append([test_year, len(rolling_target), rolling_target.mean(), roc_auc_score(rolling_target, rolling_probability), average_precision_score(rolling_target, rolling_probability), yearly_top_three])  # 汇总年度指标与实际前三字段
rolling_table = pd.DataFrame(rolling_rows, columns=['测试年', 'n', '正类比例', 'ROC-AUC', 'AP(平均精度)', 'top-3字段'])  # 生成覆盖全部交付物的答案表
display(rolling_table.round(4))  # 展示实际执行结果
测试年 n 正类比例 ROC-AUC AP(平均精度) top-3字段
0 2021 16319 0.1243 0.7381 0.2456 pb_ratio_lf, log_market_cap, pe_ratio_ttm
1 2022 8880 0.1541 0.6413 0.2260 pb_ratio_lf, log_market_cap, pe_ratio_ttm
2 2023 4678 0.0782 0.7213 0.1635 pb_ratio_lf, log_market_cap, pe_ratio_ttm
3 2024 5017 0.0203 0.7148 0.0447 pb_ratio_lf, log_market_cap, pe_ratio_ttm

滚动检验:实际参考输出

  • 完整参考输出:
    • 2021—2024 年的 ROC-AUC 分别为 0.7381、0.6413、0.7213、0.7148;AP(平均精度)分别为 0.2456、0.2260、0.1635、0.0447,对应正类比例为 0.1243、0.1541、0.0782、0.0203。

    • 四个年度模型的实际 top-3 均为 pb_ratio_lf, log_market_cap, pe_ratio_ttm

    • 2024 年 AP 很低主要反映事件极少且排序收益有限;年度差异说明单次测试窗不能代表稳定业务价值。

  • 答题提示: 若答案没有逐年重新拟合或 top-3 来自全样本模型,回到时间切分与“每折重拟合”;若把重要性写成因果,回到 MDI 边界。

来源与延伸阅读

  • Breiman, L. (2001), “Random Forests”, Machine Learning, 45, 5–32;随机森林机制的邻近权威来源。
  • Freund, Y. and Schapire, R. E. (1997), “A Decision-Theoretic Generalization of On-Line Learning,” Journal of Computer and System Sciences, 55, 119–139。
  • scikit-learn User Guide:Decision Trees、Ensembles 与 permutation importance。
  • 数据来源:本地 valuation_factors_quarterly_15_years.h5 / valuation_factors;图表由本页代码生成。

本章总结

  1. 课后选学:在独立且单个模型优于随机猜测等条件下,Hoeffding 不等式可为多数投票给出指数误差界。
  2. 两大路径:
    • Bagging (以随机森林为代表): 并行训练,通过投票/平均来降低方差,使模型更稳定。
    • Boosting (以AdaBoost为代表): 串行训练,迭代地关注错误样本,通过加权组合来降低偏差,使模型更准确。
  3. 核心组件: 决策树是集成学习最理想的“积木”,其自身的高方差或可控的“弱”性使其能与 Bagging 和 Boosting 完美结合。
  4. 本讲做法:
  • 为修复高方差并保留重要性诊断,本讲事先选择随机森林,只在验证期选择并确定其阈值;

  • 测试期只做一次最终检查,单树/AdaBoost 指标仅作事后描述。

  • 一般项目若要选模型,也必须在训练/验证阶段完成。

  • MDI 特征重要性是关联性诊断,不是因果结论。