三个臭皮匠,赛过诸葛亮
集成学习概念图
三个简单的“弱学习器”(圆形、方形、三角形)通过聚合过程,组合成一个复杂的“强学习器”。
弱学习器 A
弱学习器 B
弱学习器 C
+
+
强学习器
在金融与经济中,预测无处不在
我们依赖各种模型进行关键决策:
金融与经济中的预测应用
四个图标分别代表银行、对冲基金、政府和保险公司,并配有它们各自的典型预测任务。
银行
预测客户
是否会发生
信用卡违约
对冲基金
预测股票
明天走势
上涨或下跌
政府
预测下季度
GDP 增长率
保险公司
预测客户
提出索赔的
风险
核心问题:单一模型 vs. 群体智慧
任何一个单一模型都可能存在缺陷、偏差或错误。
本章要探讨的问题是:
如果我们将许多“还不错”的模型结合起来,能否得到一个“非常强大”的超级模型?
剧透一下:答案是肯定的。这个方法就叫做集成学习 (Ensemble Learning) 。
本章学习目标
完成本讲后,你能够:
核心内容: 用训练误差与验证误差诊断单棵决策树的过拟合。
核心内容: 根据偏差、方差与学习器相关性,在 Bagging 与 Boosting 之间作有证据的选择。
拓展内容: 手算一次熵、信息增益、基尼指数与 AdaBoost 权重更新;90 分钟核心内容只要求解释这些量如何驱动分裂与重加权。
核心内容: 解释为何本讲选择随机森林,并说明单棵树与 AdaBoost 的测试结果只能作为辅助比较。
核心内容: 从实际模型输出解释特征重要性,并说明“重要性不等于因果”。
拓展内容: 解释 Stacking 的样本外元特征,并与前两类机制比较。
开始前回顾:先看单树为何会失败
若一棵树继续分裂直到叶节点只剩一个样本,训练误差与测试误差通常如何变化?
Bootstrap 抽样是“无放回”还是“有放回”?
在正类稀少时,只看准确率会遗漏什么?
请先独立写下三个判断,再揭示答案。
揭示与补救
叙事主线:先诊断单树的高方差,再引出降低方差的 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)}
\]
理论的核心结论
集成学习之所以有效,依赖于两个关键假设:
独立性 (Independence) :每个“臭皮匠”的思考方式需要有差异。如果所有人都犯同样的错误,集成是没有意义的。
优于随机 (Better than Random) :每个“臭皮匠”的能力都必须比瞎猜要好一点 (对于二分类问题,准确率 > 50%)。
只要满足这两个条件,我们几乎总能通过集成得到一个强大的学习器。
拓展内容 B:三种集成机制的完整展开
学习提示: 以下完整机制图与 Stacking 为拓展;核心摘要位于 单树与剪枝之后 。
实践中,我们如何创造出满足那两个条件的“一群臭皮匠”呢?
集成学习的三大流派
集成学习的三大流派
图示主题为“集成学习的三大流派”。主要标签包括:集成学习方法对比 (Ensemble Methods)、Bagging、关系: 独立并行、解决: 降低方差 (Variance)、好比: 专家独立分析后投票、Boosting、关系: 顺序依赖、解决: 降低偏差 (Bias);这些元素共同展示该页的关系、比较或流程。
集成学习方法对比 (Ensemble Methods)
Bagging
关系: 独立并行
解决: 降低方差
类比: 独立投票
Boosting
关系: 顺序依赖
解决: 降低偏差
类比: 专攻错题
Stacking
关系: 分层结构
解决: 提升预测精度
类比: 主席汇总
机制一:Bagging (Bootstrap Aggregating)
Bagging 的工作流程非常直观:自助采样 (Bootstrap) + 聚合 (Aggregating) 。
采样 (Bootstrap) : 从原始训练集 D 中,进行有放回地 随机抽样,生成 T 个大小相同的新训练集 D_1, D_2, ..., D_T。
训练 (Train) : 在每个新训练集 D_t 上,独立地、并行地训练一个基学习器 h_t。
聚合 (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 是一族将弱学习器“提升”为强学习器的算法,它采用的是一种串行 的、迭代 的方式。
初始化 : 赋予所有训练样本相同的权重。
迭代训练 (t=1 to T) :
在当前带权重的样本集上,训练一个弱学习器 h_t。
增大 被 h_t 预测错误 的样本的权重。
减小 被 h_t 预测正确 的样本的权重。
最终组合 : 最终的强学习器是所有弱学习器的加权组合 。
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 是一种更精巧的组合策略,它试图学习如何“聪明地”组合基学习器的预测,而不是简单地投票或平均。
第一层 (Level 0) : 训练多个不同的基学习器。将它们的预测结果作为新的特征。
第二层 (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) 的直观理解
数据集纯度示意图
三个容器代表数据集。第一个容器混合了两种颜色的球,代表低纯度。第二个大部分是同一种颜色,代表中等纯度。第三个全是同一种颜色,代表高纯度。
低纯度
中等纯度
高纯度
如何量化“纯度”?
我们用两个主要指标来衡量“不纯度”或“混乱程度”:
信息熵 (Information Entropy) -> 用于 ID3, C4.5 算法
基尼不纯度 (Gini Impurity) -> 用于 CART 算法
目标都是一样的:选择一个分裂,使得分裂后子节点的不纯度加权和最小 。
度量纯度之一:信息熵
信息熵 H(D) 是度量一个数据集 D 不确定性 或混乱程度 的指标。
熵越大,表示数据集越混乱(混合的类别越多)。
熵越小,表示数据集越纯净(大部分样本属于同一类别)。
对于一个有 K 个类别的数据集 D,其信息熵定义为:
\[
\large{H(D) = - \sum_{k=1}^{K} p_k \log_2(p_k)}
\]
其中 p_k 是第 k 类样本所占的比例。
熵的数值特性
假设一个数据集中只有两类:正例 (+) 和 反例 (-)。
完全纯净
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/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. 计算 D1 和 D2 的基尼指数 :
\[
\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 算法会遍历所有属性的所有可能二分点,计算它们的加权基尼指数,并选择最小的那个作为最优分裂点。
决策树的现实问题:过拟合
决策树的现实问题:过拟合
图示主题为“决策树的现实问题:过拟合”。主要标签包括:决策树过拟合过程示意、欠拟合、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 略升,是否足以宣布“下行预警可用”?先答,再进入同一数据切分的模型比较。
拓展内容 C:随机森林构造与 AdaBoost 完整推导
学习提示: 下面进入同一组 A 股数据的模型比较 。随机森林构造与 AdaBoost 完整推导可在课后选学。
Part 4: 强大的集成模型
现在,我们把决策树这个“积木”和 Bagging/Boosting 这两种“搭建方法”结合起来。
Bagging + 决策树 = 随机森林
随机森林 (Random Forest) 是 Bagging 的一个非常成功的扩展和改进,它在 Bagging 的基础上,额外引入了特征随机性 。
构建过程 :
进行 T 次自助采样 (Bootstrap) ,得到 T 个训练子集。
对每个子集训练一棵决策树。在训练这棵树的每个节点 进行分裂时:
不再是 从所有 d 个特征中选择最优特征。
而是随机选择 l 个特征 (l < d) ,然后从这 l 个特征中选择最优的。
最终将 T 棵树通过投票或平均进行预测。
随机森林的双重随机性
随机森林的双重随机性示意图
展示了随机森林的两个随机来源:行采样(自助采样)和列采样(特征随机选择)。
原始数据集
1. 行采样
训练子集 (用于一棵树)
2. 列采样 (特征随机)
随机森林为何更强大?“求同存异”
特征随机性的作用 : 降低了森林中树与树之间的相关性 。
为何降低相关性很重要:
通过强制每棵树在分裂时只能考虑一部分特征,随机森林让每棵树都从不同的“视角”去学习,变得“各有所长”。
当它们组合在一起时,就能形成更强大的互补效应,进一步降低整体的方差。
Boosting + 决策树 = AdaBoost
AdaBoost (Adaptive Boosting) 是 Boosting 家族最经典的算法。
核心思想的迭代循环 :
训练一个弱学习器 h_t。
评估 h_t 的表现,赋予它一个权重 α_t (表现越好,权重越高)。
根据 h_t 的预测结果,更新训练样本的权重 w (预测错误的样本权重变大)。
重复此过程。
最终模型是所有弱学习器的加权 组合。
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 :
训练弱学习器 : 使用当前带权重的训练集训练一个弱学习器 h_t(x),使其最小化加权错误。
计算加权错误率 ε_t : h_t 分类错误的样本的权重之和。
\[
\large{\epsilon_t = \sum_{n=1}^{N} w_{t,n} I(h_t(x_n) \ne y_n)}
\]
计算该学习器的权重 α_t :
\[
\large{\alpha_t = \frac{1}{2} \ln\left(\frac{1 - \epsilon_t}{\epsilon_t}\right)}
\]
错误率 ε_t 越低,α_t 越大,话语权越大。
AdaBoost 算法流程详解 (3/4): 更新权重
更新样本权重 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) # 使相邻季度筛选与标签日期清除可以检查
训练
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' ] # 只核对已拟合基准模型设置;此时仍不查看测试标签
实战第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} ' ) # 显式记录开测试前的唯一选择
观察规则: 只有当实际输出更高时才称为提升;单次时间切分不构成“显著优于”的统计证据。
实战第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' ] # 只核对辅助比较设置;此时仍不查看测试标签
观察规则
比较实际 ROC-AUC 与 AP(平均精度);若排序不同,应解释指标为何强调不同错误,而不是预设胜者。
实战第5步:阈值已确定,再打开一次测试检查
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步证据:排序不等于可用分类阈值
多数类基线
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。
决策含义:
实战第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() # 展示当前步骤的结果。
结果分析:测试比较不能决定胜者
本次比较不能证明集成模型普遍优于单树,也不能把一次差值称为“显著”。它展示的是一条可重复的比较分析步骤。
集成学习的另一大优势:可解释性
集成模型,特别是基于树的模型,还有一个巨大的优点:它们可以告诉我们,哪些输入特征对于做出最终决策最重要 。
这在经济和金融应用中至关重要。我们不仅想预测,更想理解 预测背后的驱动因素。
可视化特征重要性
让我们读取本次拟合的随机森林实际使用的字段,而不是预先写入变量名称。
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 中只可能出现 pe_ratio_ttm、pb_ratio_lf、dividend_yield_ttm、ev_to_ebitda_ttm 与 log_market_cap。先读图中实际 top-k,再结合数据字典说明含义。
MDI 偏好连续、可分裂点较多或高度相关的变量;它不提供方向,也不等于因果效应。业务解释必须同时报告样本期、代理标签与这种偏置。
形成性检查:修复高方差
题目: 单树训练 AUC 为 0.99、验证 AUC 为 0.66;100 棵树的误差高度相关。你会先采用哪项措施?
请先选择“更多树 / 限制树并增加随机性 / Boosting”,并用偏差—方差解释。
五分钟 核心内容本讲回顾
检查自己是否掌握:
独立作答: 先诊断,再读同一测试期证据。请分别回答:
若训练 AUC 为 0.99、验证 AUC 为 0.66,说明什么?应先尝试哪类修复?
测试期中森林 AP 更高(0.1051 > 0.0972)但 ROC-AUC 略低(0.7258 < 0.7284),支持什么、不能支持什么?
本例用什么验证准则选阈值?为什么 F2 最大化不等于 FP/FN 货币成本最小化,且不能在测试期重选?
参考答案与补救:
训练好、验证差提示高方差,应先限制树并用 Bagging/特征随机化降低相关误差。
两个排序指标给出不同次序,只能说森林在该测试期 AP 更高,不能证明它全面优于单树;任何排序指标都不保证 0.50 是合适分类阈值。
本例在验证期确定 F2 最大的阈值;F2 只是召回偏重的综合指标,不是未声明的 FP/FN 成本函数。
测试期只检查一次。
核心内容总结:失败—修复—证据—决策
单树“训练很好、验证变差”先诊断为高方差风险。
Bagging 通过样本/特征随机化降低方差;Boosting 通过逐步纠错降低偏差。
本讲基于高方差修复与可解释性目标事先选择随机森林;单树/AdaBoost 的同窗测试指标只用于描述机制差异。
只用验证期 F2 最大化确定随机森林分类阈值;测试期负责一次最终检查,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 )) # 展示实际执行结果
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;图表由本页代码生成。
本章总结
课后选学: 在独立且单个模型优于随机猜测等条件下,Hoeffding 不等式可为多数投票给出指数误差界。
两大路径 :
Bagging (以随机森林为代表) : 并行训练,通过投票/平均来降低方差 ,使模型更稳定。
Boosting (以AdaBoost为代表) : 串行训练,迭代地关注错误样本,通过加权组合来降低偏差 ,使模型更准确。
核心组件 : 决策树是集成学习最理想的“积木”,其自身的高方差或可控的“弱”性使其能与 Bagging 和 Boosting 完美结合。
本讲做法: