19 大数据机器学习方法在金融学中的应用

本章会用到的数据

  • 公开下载上市公司财务报表。本章使用八家公司的年度记录,以 6 月 30 日为预测日期,以当年最终 EPS 为目标。

  • 这些数据能做什么:完成一个从提出问题、建立基准到比较模型和解释结果的综合项目。

  • 分析时注意:6 月 30 日只能使用此前已经披露的财务信息;2024 年数据只用于最终测试。

  • 结果说明:报告模型是否胜过简单基准,并说明误差、限制和不能得出的结论。

【课堂核心】6 学时学习安排

学习内容 分钟
目标与先修检查 20
FM 与形成性检查 55
Lasso 计算与验证 35
IPCA 适用范围 35
SDF 与检查 20
双重选择与 ADML 比较 40
点时预测说明 25
四个综合练习 110
本章小结 20

课程简介:我们将要学习什么?

这堂课要求学生比较经典方法与机器学习候选,计算核心对象,并用点时样本、基线和最终测试为结论划定边界。

我们将开启一场从经典理论到现代方法的探索之旅。

学习目标 1:比较研究范式与验证过程

大数据革新金融研究范式 从传统模型到机器学习的范式转变,后者通过大数据分析揭示隐藏模式。 传统模型 Linear Models 大数据 & ML 比较研究与验证流程

可自查表现 O1

比较经典因子检验与机器学习预测检查的问题、信息集和验证证据;画出“决策时点 → 点时数据 → 开发窗 → 最终测试 → 条件性结论”流程,并指出两种前瞻偏差。

学习目标 2:计算并解释核心对象

非线性Lasso 一条曲线拟合数据点,旁边是一个Lasso筛选器图标,象征捕捉非线性关系和变量选择。 Lasso 解释非线性Lasso

可自查表现 O2

计算并区分 FM 的时间序列截距、横截面截距与残差,解释 Lasso 惩罚和 SDF Euler 条件;对非线性基函数与自适应 Lasso,只陈述成立条件,不声称未经实际运行的经验性能。

学习目标 3:界定前沿方法的证据边界

IPCA 的条件载荷与潜在因子 可观测公司特征经参数矩阵映射为时变条件载荷;潜在因子是另行联合估计的对象,载荷与因子共同进入收益方程。 特征参数化载荷;因子另行联合估计 公司特征c_it ∈ R^L 映射Γβ′ 条件载荷β_i,t+1 ∈ R^Kβ_i,t+1 = Γβ′ c_it 潜在因子f_t+1 ∈ R^K(与载荷联合估计) 收益方程x_i,t+1 = β′ f_t+1+ ε_i,t+1

可自查表现 O3

画出 IPCA 从可观测特征到条件载荷的映射,列出点时面板、因子规格与最终测试定价误差三项检查要求;未完成这些证据时,明确拒绝“更优、更稳健”的结论 (Kelly 等 2019年)

学习目标 4:诊断选择与纠偏边界

机器学习纠偏 假设数据生成过程下的概念示意:正交得分相对直接 plug-in 对 nuisance 小扰动的一阶敏感度更低,不是实证结果。 0 偏差 直接 plug-in ADML正交得分 0 一阶敏感度降低(条件性)

可自查表现 O4

比较双重选择的两次控制选择与 ADML 的正交得分,诊断稀疏性、识别、交叉拟合或标准误条件缺失时哪些结论必须撤回;把边界写入项目失败案例卡。

目标与先修检查:先画学习任务图

  1. 用一句话分别区分描述、预测、统计推断与因果识别。
  2. 画出点时数据、扩展窗口、最终测试的顺序,并标出缩放、选模和标签开放位置。
  3. 为 O1—O4 各写一项可观察的学习成果,并把它连接到后文的练习或项目阶段。

完成要求:写清四类问题的区别、完整时间顺序,以及四个目标各自对应的练习;缺一项就返回第 6—7 章复习。不要只写“理解”或“了解”,要写出可以检查的结果。

目标检查反馈:活动—反馈—评分标准映射

目标 练习 反馈 / 可自查评分标准
O1 比较验证过程 点时预测练习;综合练习 1 检查样本数量变化;问题边界 15% + 无泄漏验证 25%
O2 计算核心对象 Lasso 练习;综合练习 2 检查目标函数、验证结果与模型选择;模型与基线 20%
O3 说明方法适用范围 IPCA 练习;综合练习 3 检查信息集、验证方法与不采用条件;风险检查 20%
O4 诊断选择偏差 双重选择与 ADML 练习;综合练习 3—4 检查控制变量选择及其适用条件;缺失条件时必须撤回结论

通过标准是每个目标都有“活动、输出、反馈/评分标准”三项内容;IPCA/ADML 未实际运行时只能提交适用范围,不得冒充性能验证。

本章导览:一场寻找Alpha的探索之旅

本章将像一场侦探小说,我们将使用机器学习这个“放大镜”,在海量数据中寻找真正能预测股票收益的“线索” (Alpha)。

  • 第一部分:横截面预测
    • 任务: 如何更准确地预测股票的未来收益?
  • 第二部分:方法详解
    • 工具: Fama-MacBeth, Lasso, 交叉验证等。
  • 第三部分:因子模型新视角
    • 创新: 工具变量主成分分析 (IPCA)。
  • 第四部分:识别有效因子
    • 利器: 双重选择Lasso与自纠偏机器学习。

核心挑战:信号 vs. 噪音

金融市场的核心挑战是:在充满噪音的汪洋大海中,识别并验证哪些变量是真正的“信号”,具有解释力和定价能力。

信号与噪音 一条平滑的信号曲线被大量随机的噪音点所覆盖,机器学习的任务就是找出这条信号曲线。 信号 (α) 噪音 机器学习:在高维数据和复杂关系中披沙拣金

第一部分:股票收益率的横截面预测

经典方法的困境:高维数据下的失灵

传统的线性模型(如Fama-MacBeth回归)是实证金融的基石,但在面对成百上千个潜在预测变量时,会遇到两大障碍:

1. 多重共线性

许多预测变量高度相关(如不同类型的动量或价值指标),导致模型估计不稳定,系数的经济意义难以解释。

2. 过拟合 (Overfitting)

模型在样本内表现完美,因为它学习到了数据中的“噪音”而非真实的“信号”,导致样本外预测能力极差。

过拟合的直观理解

“过拟合”就像一个学生,把练习册上的所有题目和答案都背了下来,但在真正的考试中遇到新题目时却一筹莫展。

过拟合 vs. 合理拟合 左侧图显示一条复杂曲线完美穿过所有数据点(过拟合),右侧图显示一条平滑曲线捕捉了数据的主要趋势(合理拟合)。 过拟合 (样本内 R² ≈ 1) 合理拟合 (泛化能力强)

机器学习的对策(1):正则化

核心直觉:在拟合误差之外加入系数惩罚,改变偏差—方差权衡;是否改善样本外表现,取决于特征缩放、惩罚强度、样本与设计,并须在开发窗口验证。

正则化 一个天平,一端是“拟合优度”,另一端是“模型复杂度”,正则化是在两者之间取得平衡。 拟合优度 min RSS 模型复杂度 min Σ|β| 目标:在两者间取得平衡 min (RSS + λΣ|β|)

L1、L2 与弹性网的行为不同

  • Lasso(L1):可能产生精确零系数;零与非零由惩罚、缩放、样本和相关设计共同决定。
  • 岭回归(L2):通常连续收缩系数而不产生精确稀疏解。
  • 弹性网:组合 L1 与 L2,在相关特征下兼顾稀疏倾向与稳定收缩。

非零系数是当前惩罚规格下的选择,不自动等于“真实重要变量”;正则化也不保证消除过拟合。

机器学习的对策(2):降维

核心直觉:将大量相关的原始变量映射为少数构造变量。样本 PCA 主成分在所用样本内按构造正交,但截断会以丢失部分信息换取降维;这不等于消除了未来样本或其他模型中的全部共线性问题。

降维 许多原始变量(细线)汇入一个处理单元(PCA),然后输出少数几个主成分(粗线)。 原始高维特征 (L个) P/E, P/B, MOM, ... 降维 (e.g., PCA) 低维主成分 (K个) PC1, PC2, ...
  • 代表方法:主成分分析 (PCA)。
  • 效果:用少数几个综合因子捕捉原始数据的大部分信息,简化模型。

机器学习的对策(3):处理非线性关系

核心直觉:金融市场的关系往往不是简单的线性关系。机器学习模型,特别是神经网络,具有强大的非线性拟合能力。

神经网络结构 一个简单的神经网络,包含输入层、一个隐藏层和输出层,层与层之间的节点全连接。 输入层 (特征) 隐藏层 (非线性变换) 输出层 (预测)
  • 代表方法:神经网络、回归树、梯度提升树。

项目阶段式研究:Gu, Kelly, and Xiu (2020)

Gu, Kelly, and Xiu (2020) 在 Review of Financial Studies 系统比较了多类机器学习方法 (Gu 等 2020年)。后续数字必须对应论文的具体表号、样本与度量,不能脱离原表解释。

他们系统性地比较了多种机器学习方法在美国股票收益率预测中的表现,堪称一场机器学习方法在金融预测领域的“奥林匹克竞赛”。

参赛选手包括

  • 线性模型:弹性网 (Elastic Net)
  • 降维模型:主成分回归 (PCR), 偏最小二乘 (PLS)
  • 非线性模型:回归树, 神经网络 (Neural Networks)

GKX (2020) 的论文内结果:模型表现

以下概括限于该论文的美国股票样本、预测设计和指标,不能直接外推到 A 股 (Gu 等 2020年)

竞赛结果

  1. 神经网络表现最佳 🏆
    • 在论文报告的候选模型与样本中,神经网络的样本外指标较高。
  2. 回归树优于线性模型
    • 论文内比较与非线性预测关系一致,但不构成因果识别。
  3. 浅层学习优于深层学习
    • 论文内浅层结构的相对表现较好;是否适用于其他市场仍须重新验证。

GKX (2020) 的核心发现(2):重要特征

机器学习模型不仅能预测,还能告诉我们哪些特征最重要。最重要的预测特征大都与我们熟知的“异象”有关:

  • 动量 (Momentum)
  • 流动性 (Liquidity)
  • 风险测度 (Risk Measures)
  • 估值比率 (Valuation Ratios)
  • 基本面信号 (Fundamental Signals)
  • 短期反转 (Short-term Reversal)

这些是论文样本内的变量重要性结果。变量重要性解释模型预测函数,不等于理论验证或因果效应 (Gu 等 2020年)

中国市场迁移前的待检验假设

下列制度与投资者结构表述在本课件中尚无可定位来源和可复算结果,只能作为研究问题,不能作为事实前提。

  • 散户主导
    • 导致更高的波动性和更频繁的短线交易行为。
  • 政府影响
    • 中央控制的银行主导金融体系,市场自动修正机制可能受限。
  • 卖空限制
    • 导致负面信息难以及时反映在股价中,市场定价效率可能较低。
  • 信息不对称
    • 相较于机构,散户在信息获取上处于劣势。

文献定位任务:未进入依据的历史作者名

  • 当前状态:旧课件中的部分作者名无法唯一定位版本、题名、样本与表号。
  • 处理:删除相应精确结论,不把作者名本身当作证据。
  • 重新纳入条件:补齐 DOI/URL、版本、原表号与复算口径。

第二部分:机器学习预测方法详解

温故知新:Fama-MacBeth回归

在进入机器学习世界之前,我们必须先理解它的参照物——Fama-MacBeth (FM) 回归。这是一个巧妙的两步法,用于处理面板数据,检验因子模型的有效性。

核心优势:巧妙地处理了股票收益在截面上残差相关性高,但在时间序列上相关性不高的数据特征。

Fama-MacBeth回归流程 一个三步流程图:第一步是时间序列回归得到Beta,第二步是横截面回归得到风险溢价,第三步是汇总检验。 第一步: 时间序列回归 (对每个股票 i) 产出: β_i 第二步: 横截面回归 (对每个时间 t) 产出: λ_t 第三步: 汇总 (求时间均值) 产出: λ̄, t-stat

FM回归第一步:时间序列回归

目标:为每个资产 \(i\) 估计其在因子 \(f_t\) 上的暴露 (exposure),即 \(\beta_i\)

方法:对每一个资产 i,用其整个时间序列的收益率 \(r_{i,t}\) 对同时期的因子收益率 \(f_t\) 进行回归。

\[ \large{R^e_{i,t} = \alpha_i + \beta_i' f_t + \epsilon_{i,t} \quad \text{for } t=1, ..., T} \]

  • 这个回归对每个股票(或投资组合)单独进行。
  • 产出:每个股票的一个(或一组)固定的因子载荷 \(\hat{\beta_i}\)

FM回归第二步:横截面回归

目标:估计因子 \(f_t\) 在每个时期 \(t\) 的风险溢价 (risk premium),即 \(\lambda_t\)

方法:在每一个时间点 t,用该时刻所有资产的收益率 \(r_{i,t}\)第一步中得到的因子暴露 \(\hat{\beta_i}\) 进行回归。

\[ \large{R^e_{i,t} = \lambda_{0t} + \hat\beta_i'\lambda_t + u_{i,t} \quad \text{for } i=1, ..., N} \]

  • 这个回归是在每个时间点进行的。
  • 口径\(\lambda_{0t}\) 是 zero-beta/intercept,\(u_{i,t}\) 是横截面残差;二者都不是第一步资产截距 \(\alpha_i\)
  • 产出:一系列随时间变化的风险溢价估计值 \(\hat{\lambda}_t\)

FM回归第三步:汇总与检验

目标:得到因子风险溢价的最终估计值,并进行统计检验。

方法:将第二步中得到的每个时期的风险溢价 \(\hat{\lambda_t}\) 取时间序列上的平均值。

\[ \large{\bar\lambda = \frac{1}{T} \sum_{t=1}^{T} \hat\lambda_t} \]

统计推断:标准误基于 \(\hat\lambda_t\) 时间序列,并按序列相关使用预先说明带宽与小样本处理的 HAC。第一步生成的 \(\hat\beta_i\) 还可能需要 Shanken、相应 GMM 或 bootstrap 修正,取决于因子可交易性与模型设定。

检查:三个截距/残差不能互换

请分别标记第一步 \(\alpha_i\)、第二步 \(\lambda_{0t}\)\(u_{it}\) 的含义;再说明 \(\lambda_t\) 的标准误为何不能只按横截面残差计算。

反馈:先分清时间序列与横截面对象

  • \(\alpha_i\):资产 \(i\) 的时间序列截距。
  • \(\lambda_{0t}\):时期 \(t\) 的横截面 zero-beta/intercept。
  • \(u_{it}\):时期 \(t\) 的横截面残差。
  • 推断来源:风险价格标准误来自 \(\lambda_t\) 时间序列。
  • 订正路径:依次回看时间序列回归横截面回归汇总和标准误

FM回归的挑战:EIV问题

FM回归的一个核心计量问题是 变量误差 (Errors-in-Variables, EIV)

  • 问题来源:第二步的横截面回归中,使用的自变量 \(\hat{\beta_i}\) 本身是第一步回归的估计值,而不是真实的 \(\beta_i\)
  • 后果边界:生成 beta 会影响第二步点估计与推断;方向与一致性取决于 \(N,T\) 渐近序列、因子可交易性、第一步误差和两步规格。
  • 推断修正:Shanken (1992) 讨论估计 beta 引起的修正;具体标准误仍取决于模型、样本与假设 (Shanken 1992年)
  • 禁止简化:不能断言风险价格必然偏差且不一致,也不能把文献名当作自动校正按钮。

机器学习的切入点:Lasso回归

  • 问题区别:FM 处理资产 beta 的横截面定价;高维股票特征预测是另一估计问题。
  • Lasso 工具:在最小二乘目标中加入系数绝对值之和,即 L1 惩罚。
  • 稀疏效果:在控制拟合误差的同时,允许部分系数精确等于零。
  • 边界:Lasso 不自动修正 FM 的生成 beta EIV;目标也不是脱离预测风险追求“零越多越好”。

Lasso回归的目标函数

Lasso通过最小化以下目标函数来求解系数向量 \(g_t\)

\[ \large{\hat{g}_t^{Lasso} = \underset{g}{\arg\min} \underbrace{\frac{1}{2N}\sum_{i=1}^{N}(\tilde{r}_{i,t} - z_{i,t-1}'g_t)^2}_{\text{拟合项:半均方残差}} + \underbrace{\gamma \sum_{j=1}^{K} |g_{j,t}|}_{\text{惩罚项:系数绝对值之和}}} \]

  • \(\gamma\) 是控制惩罚强度的超参数。更强惩罚通常带来更强的整体收缩并可能更稀疏,但在相关设计下单个系数可进入、退出或变号,非零数量不保证沿路径单调。
  • 必须在确定的 \(\gamma\) 网格上同时报告实际非零数与时间外误差,再按事先确定规则选择;不能把稀疏度单调性当作定理。
  • 本章记号:标准 Lasso 的惩罚强度统一写作 \(\gamma\)
  • 软件映射:当前归一化下,sklearn.linear_model.Lassoalpha \(=\gamma\)
  • 避免混淆:文献中的同目标 \(\lambda\) 可对应 \(\gamma\),但它不同于 FM 风险价格 \(\lambda_t\) 与 SDF 载荷 \(\lambda_g\);RSS 归一化改变时需重新缩放。

O2 核心任务:Lasso 规格与验证

  • 公式说明:拟合项与 L1 惩罚各自的角色。
  • 开发期证据:三个预设惩罚强度的扩展窗结果。
  • 比较对象:训练均值基线、非零系数数目与确定规格。
  • 禁止:只交系数图,或用最终测试期改选候选。

O2 练习:先完成再查看答案

完成内容公式标注、逐窗表与一句不采用条件后再看反馈;任一预处理若在全样本拟合,请重做重做。

O2 反馈:计算对象与采用标准

  • 稀疏机制:L1 允许系数精确为零。
  • 选择规则:惩罚强度只用开发窗选择;同窗训练均值是最低基线。
  • 采用条件:确定候选在最终测试期保持优势,且系数与误差稳定。
  • 否则结论:当前设计不采用该候选。
  • 订正路径:拟合项、L1 项或 \(\gamma=\texttt{alpha}\) 映射有误,回看目标与参数映射

Lasso为何可能产生稀疏估计

L1惩罚项的几何形状(菱形)是Lasso实现变量筛选的关键。

Lasso vs. Ridge 左图Lasso的菱形约束区域更容易在角点与等高线相切,导致系数为零。右图Ridge的圆形约束区域通常在非轴位置相切。 β₁β₂ Lasso (L1) 解: β₁=0 (稀疏解) β₁β₂ Ridge (L2) 解: β₁≠0, β₂≠0
  • 条件结论:L1 约束的角点使精确零更可能出现;实际非零数仍由惩罚强度、特征缩放、样本与设计决定,弱惩罚下可以并不稀疏。

Lasso代码示例

下面是一个用Python scikit-learn 实现Lasso回归的简单例子。

代码
# 为“Lasso代码示例”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“Lasso代码示例”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 为“Lasso代码示例”,从 `sklearn.linear_model` 导入`Lasso` 用于拟合带 L1 惩罚的线性回归。
from sklearn.linear_model import Lasso
# 为“Lasso代码示例”,导入 `StandardScaler`,由全部机制拟合样本逐特征估计均值与标准差;本页不作时间外检验。
from sklearn.preprocessing import StandardScaler
# 为“Lasso代码示例”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt

# 固定随机种子,使稀疏系数与 Lasso 收缩机制示例可重复。
np.random.seed(42)
# 固定样本数与特征数,界定“Lasso代码示例”的机制演示规模。
n_samples, n_features = 100, 20
# 生成固定种子的 100×20 高斯特征矩阵,演示 Lasso 的稀疏恢复机制。
X = np.random.randn(n_samples, n_features)
# 假设只有前5个特征是真正重要的
true_coef = np.zeros(n_features)
# 只给前五项赋非零真系数,明确其余十五项都是噪声特征。
true_coef[:5] = [5, -4, 3, -2, 1]
# 用线性信号 `X @ true_coef` 加标准差为 2 的高斯噪声生成响应。
y = X @ true_coef + np.random.randn(n_samples) * 2

# 创建由全部机制拟合样本逐特征估计统计量的缩放器。
scaler = StandardScaler()
# 逐特征估计这组机制拟合样本的均值与标准差,使 20 个特征处在可比尺度。
X_scaled = scaler.fit_transform(X)

# 建立并拟合 Lasso,使 L1 惩罚在标准化机制样本上产生稀疏系数。
# 按本章半均方残差目标,估计器 `alpha` 与理论惩罚强度 `γ` 数值相同;更强惩罚通常加强收缩但不保证支持集单调。
lasso = Lasso(alpha=1.0)
# 在标准化教学样本上估计当前 alpha 的 Lasso 系数,用于展示 L1 变量选择而非样本外结论。
_ = lasso.fit(X_scaled, y)  # 抑制无表头 estimator repr,后页以具名系数图报告证据

Lasso 系数恢复图

代码
# 准备在单一坐标轴上比较真实系数与 Lasso 估计系数,不绘制预测值或观测响应。

# 创建 `fig, ax` 画布,承载“Lasso在稀疏生成机制中的系数收缩示意”的并排视觉比较。
fig, ax = plt.subplots(figsize=(9, 4))
# 用 `ax.stem` 绘制离散系数的棒状位置与高度。
ax.stem(np.arange(n_features), lasso.coef_, markerfmt='o', basefmt=" ")
# 以 `np.arange(n_features)` 为横轴、`true_coef` 为纵轴绘制曲线,展示“Lasso在稀疏生成机制中的系数收缩示意”。
ax.plot(np.arange(n_features), true_coef, 'r--', label='True Coefficients')
# 将横轴标为“特征索引”,明确横向编码的变量。
ax.set_xlabel('特征索引', fontsize=16)
ax.set_ylabel('Lasso 系数', fontsize=16)  # 标明纵轴为正则化后的系数尺度
ax.set_title('标准化教学样本的 Lasso 系数', fontsize=18)  # 说明图中系数来自标准化教学样本
ax.tick_params(labelsize=16)  # 放大刻度以保证课堂投影可读
# 将纵轴标为“系数大小”,明确纵向编码的变量。
ax.set_ylabel('系数大小', fontsize=16)
# 将图题设为“Lasso回归的系数收缩效果”,直接说明当前图形的比较目的。
ax.set_title('Lasso回归的系数收缩效果', fontsize=18)
# 显示“Lasso代码示例”图例,使颜色或线型与比较对象一一对应。
ax.legend(fontsize=16)
# 显示 Lasso 估计系数的茎叶标记与真实系数虚线,检查 L1 惩罚下的变量筛选和收缩幅度。
plt.show()
茎叶图以特征索引为横轴、估计系数为纵轴;蓝色Lasso系数与红色真实系数对比,展示强信号被保留而多数噪声系数收缩到零。
图 1: Lasso在稀疏生成机制中的系数收缩示意

现实的复杂性:非线性关系

如果特征与预期收益之间的关系不是线性的怎么办?例如,规模因子的影响可能在小盘股区间很大,但在大盘股区间逐渐减弱。

我们可以使用 非线性Lasso (或称非参数组Lasso) 来捕捉这种关系。

非线性Lasso的实现方式:基函数展开

核心思想:用一组基函数 (basis functions) \(p_k(z)\) 来近似复杂的非线性函数 \(m_{t,j}(z)\)

\[ \large{m_{t,j}(z) \approx \sum_{k=1}^{L+2} g_{t,j,k} p_k(z)} \]

  • \(p_k(z)\) 可以是多项式项 (\(1, z, z^2\)),也可以是分段线性的“样条”函数 (splines)。
  • 样条函数可在节点 (knots) 处分段拼接;给定有限基、节点与平滑/正则化约束时,其可表达复杂度有明确上限,增加灵活性也会提高估计方差。

非线性Lasso的目标函数

我们将每个非线性函数 \(m_{t,j}(z)\) 对应的所有基函数系数 \(g_{t,j,k}\) 看作一个“组”。

组Lasso (Group Lasso) 的思想是:要么把整个组的系数都保留,要么把它们全部置为零。

令拟合误差为:

\[ \large{\mathcal{L}_t(g)=\sum_{i=1}^{N_t}\left(\tilde r_{i,t+1}-\sum_{j=1}^{J}m_{t,j}(z_{i,t,j})\right)^2.} \]

组 Lasso 的目标函数写为:

\[ \large{\hat g_t^{NPLasso}=\underset g{\arg\min}\left\{\mathcal L_t(g)+\gamma_{\mathrm{group}}\sum_{j=1}^{J}\sqrt{\sum_{k=1}^{L+2}g_{t,j,k}^{2}}\right\}.} \]

  • 惩罚项变成组内系数平方和的平方根之和。某组在给定样本、缩放与惩罚强度下可能整体归零;这表示该拟合规格未保留该组,不是特征真实无关的证明。

Lasso的一致性问题与自适应Lasso

  • 标准 Lasso:选择一致性需要不可表示条件等设计约束、足够的最小信号、合适惩罚率与噪声条件。
  • 条件失败:即使样本增大,也可能无法稳定恢复真实支持集。
  • 自适应 Lasso:oracle/选择一致性还依赖初始估计、权重、设计、信号、噪声与惩罚率。
  • 有限样本边界:上述结论不能无条件推广。

来源:Zhao & Yu (2006), JMLRZou (2006), JASA

核心思想:按初始估计分配不同惩罚

  • 初始估计绝对值较大:惩罚较小。
  • 初始估计绝对值较小:惩罚较大。
  • 是否改善选择稳定性,仍由条件与样本外证据决定。

自适应Lasso的实现:两步法

  1. 第一轮训练:运行一个标准的Lasso(或岭回归)得到初始的系数估计 \(\hat{g}^{(1)}\)
  2. 构造权重:根据第一轮的系数估计,为每个变量 \(j\) 构造一个权重 \(w_j = 1 / |\hat{g}_j^{(1)}|^\delta\) (通常 \(\delta=1\) )。
  3. 第二轮训练:在 Lasso 的惩罚项中加入这个权重,\(\gamma_{\mathrm{adapt}} \sum_{j=1}^{K} w_j |g_{j,t}|\)

只有当初始估计、权重指数与惩罚率合适,并满足稀疏性、设计矩阵与信号强度等条件时,两步法才可能获得选择一致性;有限样本仍须报告选择稳定性与样本外误差。

关键步骤:如何选择惩罚参数 \(\gamma\)

Lasso 模型中的惩罚参数 \(\gamma\) 是一个超参数 (hyperparameter),必须只在开发窗按事先确定候选选择:

  • \(\gamma>0\) 很小时:解通常趋近未惩罚最小二乘解,但不与 OLS 目标完全相同;设计矩阵可能使解不唯一,样本外泛化仍须验证。
  • \(\gamma\) 足够大时:受惩罚的斜率可以全部归零。
    • 默认 fit_intercept=True 且截距不受罚时,剩余模型给出训练响应均值。
    • 这是本章的最低基线,不能预先判定“毫无用处”。

交叉验证 (Cross-Validation) 是开发期比较超参数候选的常用工具;时间、公司依赖与调参层级决定具体切分方式。

K折交叉验证 (K-Fold Cross-Validation)

最常用的方法是 K折交叉验证,例如 5折交叉验证

5折交叉验证 将数据分为5折,每次用4折训练,1折验证,重复5次。 第 1 轮 第 2 轮 第 3 轮 第 4 轮 第 5 轮 选择使平均验证误差最小的 λ

交叉验证的陷阱:前瞻性偏差

在时间序列数据(如股票收益)中,随机K折交叉验证有一个致命缺陷:前瞻性偏差 (Look-ahead Bias)

  • 问题:随机划分会使得模型在训练时用到“未来”的信息,而去预测“过去”的验证集。
  • 后果:这在现实中是不可能的,会导致对模型表现的严重高估。

解决方案:时间序列交叉验证

对于时间序列数据,必须采用一种能保留时间顺序的交叉验证方法,如 扩展窗口法 (Expanding Window)

时间序列交叉验证 使用扩展窗口法进行时间序列交叉验证,确保训练集总是在验证集之前。 第 1 轮训练验证 第 2 轮训练验证 第 3 轮训练验证 第 4 轮训练验证 确保训练集永远在验证集之前

这个过程确保了模型始终使用过去的数据来预测未来,符合实际情况。

第三部分:预测因子的实证表现

因子动物园:哪些因子真正有效?

学术界发现了成百上千个能够预测股票横截面收益的所谓“因子”或“异象”。

  • 动量 (Momentum)
  • 价值 (Value)
  • 规模 (Size)
  • 盈利能力 (Profitability)
  • 投资 (Investment)
  • 风险 (Risk)
  • 流动性 (Liquidity)
  • 质量 (Quality)
  • 应计负债 (Accruals)
  • 季节性 (Seasonality)
  • 短期反转 (Reversal)
  • 等等…

稀疏性是可检验的建模假设

少数因子模型的存在不证明大部分异象由少数风险维度生成。稀疏性必须在给定资产集合、候选因子、样本与损失下,以最终测试定价误差、选择稳定性和经济限制检验。

主流模型

  • Fama-French五因子模型 (FF5)
  • Hou-Xue-Zhang四因子模型 (q4)
  • Barillas and Shanken (2018) 的模型比较框架;论文讨论的是模型比较证据,不等于预先指定一套对所有市场普适的“六因子模型” (Barillas 和 Shanken 2018年)

来源检查:无唯一来源的历史数表已移除

  • 移除内容:无法唯一定位论文版本、原表号、样本、单位、权重与标准误定义的历史数值。
  • 不再绘图:不以这些数字比较中美因子均值或方法排序。
  • 重新使用条件:收益、\(t\) 统计量或样本外 \(R^2\) 必须连同原始表/图、样本期、单位、组合权重、基准预测与标准误口径进入依据。
  • 结论边界:任何一项缺失,都不能作市场比较。

O3 核心边界:因子模型的新视角 IPCA

IPCA 将公司特征映射为随特征变化的因子暴露。下述理论设定用于理解模型结构;本章不再把无法核对的历史表当作中国市场验证。

IPCA的理论设定

IPCA 假设股票 \(i\)\(t+1\) 期的收益率 \(x_{i,t+1}\) 由一个线性因子模型驱动。采用列向量约定:\(\mathbf c_{it}\in\mathbb R^L\)\(\boldsymbol\beta_{i,t+1}\in\mathbb R^K\)\(\mathbf f_{t+1}\in\mathbb R^K\)

\[ \large{x_{i,t+1} = \boldsymbol\beta_{i,t+1}' \mathbf f_{t+1} + \epsilon_{i,t+1}} \]

  • \(\mathbf f_{t+1}\)\(K\) 维未知的潜在因子,与载荷映射参数共同估计;它不是由公司特征直接变换得到的对象。

核心创新:IPCA 假设时变因子载荷 \(\boldsymbol\beta_{i,t+1}\) 由上一期 \(t\)可观测公司特征 \(\mathbf c_{it}\) 线性参数化:

\[ \large{\boldsymbol\beta_{i,t+1} = \Gamma_\beta' \mathbf c_{it}\in\mathbb R^K,\qquad \Gamma_\beta\in\mathbb R^{L\times K}} \]

  • \(\Gamma_\beta'\) 的维度是 \(K\times L\),所以它把 \(L\) 维特征列向量映射成 \(K\) 维条件载荷列向量。\(\Gamma_\beta\) 与潜在因子序列都是 IPCA 需要联合估计的对象。

IPCA 与 PCA:不同信息集与可检验命题

IPCA vs PCA PCA只使用收益矩阵提取因子;IPCA联合使用收益与公司特征,特征通过参数矩阵形成条件载荷,潜在因子另行联合估计,二者共同重构收益。 收益矩阵 X_t 经典 PCA 潜在因子 f_t 收益 X_t + 特征 C_tC_t 每行是 c_it′ IPCA联合估计 条件载荷 B_t=C_tΓβ 潜在因子 f_t(另行估计) 共同进入收益方程:x_t+1=C_tΓβf_t+1+ε_t+1
  • 可检验命题:IPCA利用公司特征描述时变载荷,但是否降低最终测试定价误差、是否稳定以及经济含义是否成立,都取决于点时数据、规格与评价指标 (Kelly 等 2019年)

IPCA的参数估计

\(C_t\in\mathbb R^{N_t\times L}\) 的第 \(i\) 行定义为 \(\mathbf c_{it}'\),则 \(C_t\Gamma_\beta\in\mathbb R^{N_t\times K}\) 的第 \(i\) 行就是 \(\boldsymbol\beta_{i,t+1}'\)。模型的向量形式为:

\[ \large{\mathbf{x}_{t+1} = C_t \Gamma_\beta \mathbf{f}_{t+1} + \mathbf{\epsilon}_{t+1}^*} \]

目标:选择 \(\Gamma_\beta\) 和因子序列 \(F = \{\mathbf{f}_1, ..., \mathbf{f}_T\}\),来最小化误差平方和:

\[ \large{\min_{\Gamma_\beta, F} \sum_{t=1}^{T-1} ||\mathbf{x}_{t+1} - C_t \Gamma_\beta \mathbf{f}_{t+1}||^2} \]

求解方法:交替最小二乘法 (ALS)

由于目标函数中需要同时估计 \(\Gamma_\beta\)\(\mathbf{f}_{t+1}\),这是一个双线性问题,可以通过交替最小二乘法 (Alternating Least Squares, ALS) 求解。

  1. 初始化:对 \(\Gamma_\beta\) 进行一个初始猜测。

  2. 交替迭代

    1. 给定 \(\Gamma_\beta\),求解 \(\mathbf{f}_{t+1}\): 对每个时刻 \(t\) 进行OLS回归。
    2. 给定 \(\mathbf{f}_{t+1}\),求解 \(\Gamma_\beta\): 对所有数据进行OLS回归。
  3. 终止条件:重复步骤2,直到估计值收敛。

高级方法运行结果

方法 本章状态 缺少的验证证据
IPCA 仅理论 固定 \(K\)、点时收益—特征面板、扩展窗口、最终测试定价误差与不确定性
非线性组 Lasso 仅理论 折内基函数与分组、惩罚选择、点时公开面板及最终测试比较
自适应 Lasso 仅理论 初始估计、权重与惩罚率条件、选择稳定性及最终测试比较
双重选择 算法检查 两次选择集、并集与系数稳定性;未提供有效标准误,不作边际贡献推断
ADML 仅理论 明确定义的估计对象、折外 nuisance、正交得分、置信区间与失败诊断

说明:以上方法均不构成本章“已经运行验证”的性能结论;IPCA 与 ADML 仅用于辨析理论边界,本章没有实际估计它们。理论来源:IPCA (Kelly 等 2019年);ADML (Chernozhukov 等 2022年)

O3 IPCA 适用范围任务

独立填写五格:

  1. 收益矩阵与特征矩阵的信息时点。
  2. \(C_t\Gamma_\beta f_{t+1}\) 三个对象及维度。
  3. 预先固定的 \(K\)
  4. 扩展窗与最终测试定价误差。
  5. 数据、识别或稳定性失败时的撤回结论。

这里只评边界,不要求伪造 IPCA 运行结果。

O3 练习:先完成再查看答案

提交一张五格卡;缺信息时点、最终测试指标或结果不理想时如何解释不得查看反馈,也不得进入 O3 达成记录。

O3 反馈:可自查边界

  • 信息时点\(C_t\) 只含当时可得特征。
  • 对象映射\(\Gamma_\beta\) 把特征映射到载荷,\(f_{t+1}\) 是潜在因子。
  • 验证设计\(K\) 预先确定;与 PCA 在同一扩展窗和最终测试定价误差下比较。
  • 证据不足:缺点时面板、稳定性或不确定性时,只能写“尚无公开性能证据”。

【可选拓展】检查:IPCA 的高 p 值

独立作答:若事先确定 IPCA 规格的 alpha 联合检验得到较高 p 值,哪项结论成立?A“该模型为真”;B“本检验未拒绝原假设”;C“该模型解释全部收益”。

反馈:只有 B。再写出“功效不足与多 K 选择可能掩盖偏离”才算完整;选择 A/C 表明把未拒绝误当作证明。

第五部分:理论基础——随机贴现因子 (SDF)

资产定价的核心:随机贴现因子(SDF)

在无套利且存在严格正 SDF 等条件下,被定价资产可用 Euler 条件表述:

\[ \large{P_t = E_t[M_{t+1} X_{t+1}]} \]

  • \(P_t\): 资产在 \(t\) 期的价格。
  • \(X_{t+1}\): 资产在 \(t+1\) 期的 payoff(现金流或价格,不是 return)。
  • \(M_{t+1}\): 随机贴现因子。

对 gross return \(R_{t+1}=X_{t+1}/P_t\),Euler 条件是 \(1=E_t[M_{t+1}R_{t+1}]\);对 excess return,可写 \(E_t[M_{t+1}R^e_{t+1}]=0\)

两状态检查:先归一化 SDF,再核对 Euler 条件

两状态等概率,\(R_f=1.02\),原始权重 \(m=(1.2,0.8)\)。先令 \(M=m/(E[m]R_f)\),再给资产 gross returns \(R=(0.8,1.35)\)

独立计算:\(E[M]\)\(E[MR]\)\(Cov(M,R)\) 的符号,以及 \(E[R]-R_f\)。最后说明这里的 \(R\) 为什么不能称为 payoff 或 excess return。

反馈:Euler 成立时,负协方差对应正风险溢价

  • 归一化结果\(M=(1.17647,0.78431)\)\(E[M]=0.980392=1/1.02\)
  • Euler 检查\(E[MR]=1\)
  • 风险溢价\(E[R]=1.075\)\(Cov(M,R)\approx-0.05392\),故 \(-R_fCov(M,R)=0.055=E[R]-R_f\)
  • 对象边界\(R\) 是 gross return;payoff 是 \(X=PR\);excess return 是 \(R-R_f\)
  • 订正路径:混淆三种对象,回看 SDF 定义

SDF的经济学直觉:跨期边际替代率

在标准的消费资本资产定价模型 (CCAPM) 中,SDF等于投资者的跨期边际替代率

\[ \large{m_{t+1} = \beta \frac{u'(c_{t+1})}{u'(c_t)}} \]

直觉解读:SDF衡量了投资者在不同经济状态下,对未来一单位财富的主观估值。

SDF的直觉 经济不好时,钱更值钱,SDF高。经济好时,钱不那么值钱,SDF低。 经济不好 (衰退) c↓, 边际效用 u'↑ 钱“更值钱” => SDF (m) 高 经济繁荣 (增长) c↑, 边际效用 u'↓ 钱“不值钱” => SDF (m) 低

SDF与风险溢价的关系

若无风险 gross return \(R_{f,t}\)\(t\) 已知,则 \(E_t[M]=1/R_{f,t}\),并由 \(1=E_t[M]E_t[R]+Cov_t(M,R)\) 得:

\[ \large{E_t[R] - R_{f,t} = -R_{f,t}\,Cov_t(M,R)} \]

这就是风险溢价的来源!

  • 一项资产之所以能获得正的风险溢价 (\(E(R) > R_f\)),是因为它的收益率 \(R\) 与SDF \(m\) 负相关

理解风险溢价的来源

  • SDF (m) 在经济不好时值高。
  • 风险资产 (R_stock) 在经济不好时,收益率也表现
  • 因此 \(Cov(m, R_{stock}) < 0\)

这样的资产无法为投资者提供“雪中送炭”的保障,反而会“雪上加霜”。因此,投资者为了愿意持有这种“顺周期”的风险资产,必然会要求一个正的风险溢价作为补偿。

从SDF到多因子模型

在实证中,我们通常假设SDF可以被一组可观测的定价因子 (pricing factors) \(f\) 线性近似:

\[ \large{m_{t+1} \approx a + b'f_{t+1}} \]

线性 SDF 与 beta 表示之间需要明确资产集合、矩存在与非奇异协方差、截距/无风险口径。只有因子可交易并张成相关 payoff 时,风险价格才可由因子均值与协方差映射;不可交易代理或未张成时只是矩条件表示,不自动等价于收益生成模型。

\[ \large{E(R^i) = \gamma + \lambda'\beta_i} \]

核心结论:先检查 payoff/return、gross/excess、\(E_t[M]\)、因子可交易性与张成,再讨论具体 beta 表示。

检查:SDF 与双重选择

独立作答:若近似稀疏失败,或两次 Lasso 都漏掉弱但联合重要的控制,双重选择是否仍保证有效推断?为什么?

反馈:两次选择覆盖两条遗漏路径

  • 结论:不能保证。
  • 成立条件:目标参数、近似稀疏、信号与正则性、误差结构和有效方差估计均满足要求。
  • 失败机制:弱但联合重要的控制若被两次选择同时漏掉,正交余项条件可能失败。
  • 学习路径:答错回看两条选择路径;答对进入算法页。

第六部分:识别有效因子——双重选择Lasso

新的挑战:高维环境下的因子筛选

我们想知道,在一个包含了所有已知因子 \(h_t\) 的“基准模型”之上,一个新的因子 \(g_t\) 是否具有额外的解释能力?

这等价于检验这个新因子在SDF中的载荷 \(\lambda_g\) 是否显著不为零。

问题:当基准因子 \(h_t\) 很多时,未正则 OLS 可能不可识别或估计不稳定,并面临过拟合风险。

解决方案:双重选择Lasso

Feng, Giglio, and Xiu (2020) 提出了一种基于 双重选择Lasso (Double-Selection Lasso) 的方法来检验新因子 (Feng 等 2020年)

核心思想:在论文的目标参数、近似稀疏与正则性条件下,分别选择结果方程和处理方程的控制变量并取并集,以降低遗漏重要控制的一阶风险;不保证选择全部混杂、无偏或因果识别。

双重选择Lasso的步骤

双重选择Lasso流程 一个三步流程图:第一步Lasso选出与收益相关的因子,第二步Lasso选出与新因子相关的因子,第三步用合并后的因子集进行OLS回归。 第1步: LassoE(r) on h选出 I₁ 第2步: Lassog on h选出 I₂ 合并控制变量I = I₁ ∪ I₂ 第3步: OLSE(r) on (g, h_I)

在论文给定的稀疏性、正则性与样本条件下,这个三步法支持对新因子载荷 \(\hat{\lambda}_g\) 的有效推断;课堂流程图不能替代对这些假设的检查 (Feng 等 2020年)

双重选择 Lasso:从理论到公开任务

双重选择分别在结果方程和处理变量方程中筛选控制变量,再取并集进入最终低维模型。它降低“只按结果相关性筛选”漏掉混杂控制的风险,但不自动保证因果识别;后文综合项目把它作为预测与系数稳定性候选,与 Lasso、Ridge 和浅层森林在同一扩展窗口比较。

线性SDF假设的局限性

到目前为止,我们都假设SDF可以被因子线性近似。但这个假设不一定成立,SDF可能是因子的一个复杂的非线性函数。

我们需要一种方法,在SDF具有非线性结构的假设下,来识别和估计因子的定价能力。

O4 核心比较:自纠偏机器学习法 (ADML)

自动去偏机器学习 (Automatic Debiased Machine Learning, ADML) 的一般框架见 Chernozhukov, Newey, and Singh (2022) (Chernozhukov 等 2022年)。把它用于特定 SDF/因子检验还需要额外的识别、交叉拟合与正则性条件。

核心思想

  1. 允许SDF是因子的一个未知的、一般的非线性函数 \(m_t = H(f_t)\)
  2. 使用通用的机器学习方法(如Lasso, 神经网络)来估计模型中的中间量。
  3. 通过引入一个巧妙构造的影响函数,来纠正由机器学习估计带来的偏差,从而可以对最终的参数进行有效的统计推断。

正交化与乘积余项的条件

这是概念导读,不是具体得分推导。有效推断至少需要:

  • 识别与 Neyman orthogonality。
  • cross-fitting 与适合依赖结构的切分。
  • nuisance 的 \(L_2\) 误差乘积率 \(o_p(n^{-1/2})\)
  • 有限矩、overlap/权重控制与一致方差估计。

O4 有界比较任务

在一张两列表中比较:双重选择用两次 Lasso 选择集并集后做低维估计;ADML 需要明确估计对象、正交得分、折外交叉拟合、nuisance 乘积率与一致方差。各写一个结果不理想时如何解释,禁止声称二者自动建立因果识别。

O4 练习:先完成再查看答案

先交比较表与失败案例卡,再进入反馈;只写“ADML 更先进”或省略识别条件均不通过。

O4 反馈:正交化不等于免检

比较维度 双重选择 ADML
流程 \(S_Y\cup S_D\) 后低维估计 训练折拟合 nuisance;折外聚合正交得分
正交对象 并集覆盖两条遗漏路径;非一般 Neyman 正交 目标参数绑定 Neyman-orthogonal score
交叉拟合 不自动获得;终式仍须匹配依赖结构 必须按公司/时间依赖做折外交叉拟合
速率与方差 近似稀疏/选择正则性;面板一致方差 \(L_2\) 误差乘积 \(o_p(n^{-1/2})\);一致方差
方法失败 并集漏关键混杂,或终式共线/样本过小 overlap差、权重爆炸、双 nuisance 错设或切分错误
影响函数检查 不适用;并集不替代识别诊断 不能省略 cross-fitting、重叠/矩条件和有限样本诊断

作答要求:流程、正交、cross-fitting、乘积率、方差、两种失败与影响函数共 8 分;至少 7 分且不得声称自动因果识别。未达标者返回 相关内容 与上一页补齐后重交 (Chernozhukov 等 2022年)

数值机制示意(未运行 DGP)

下图是机制示意,不是论文模拟结果的复刻。实际比较必须报告数据生成过程、样本量、重复次数、偏差、覆盖率与标准误。

ADML vs. Plug-in 假设数据生成过程下的概念示意:正交得分相对直接 plug-in 对 nuisance 小扰动的一阶敏感度更低,不是实证结果。 真值 (0) 正交估计量示意 真值 (0) 偏差 直接 plug-in 示意

边界:在论文假设与实现正确时,正交化/去偏可支持渐近推断;本示意图不能证明有限样本“消除偏差” (Chernozhukov 等 2022年)

ADML 的证据边界

  • 删除证据:ADML3 的精确收益、异常定价组合数和中美比较缺少唯一可追溯来源。
  • 保留目标:解释正交化、交叉拟合和双重稳健各自解决的问题。
  • 禁止结论:缺点时样本、识别假设、标准误与最终测试验证时,不得写成中国市场经验结果。

机器学习在金融应用中的核心挑战

机器学习方法虽强大,但不能简单套用,因为金融数据有其独特性。

信号极弱,噪音极大

资产定价的信号在总收益方差中占比极小。大部分波动都是不可预测的“噪音”。

数据稀缺,目标不稳

股票交易历史短,时间序列样本少。金融市场的结构会随时间演变,预测目标本身就不稳定。

另一个挑战:对协方差矩阵的忽视

  • 金融应用的目标不同:在投资组合管理中,我们更关心整个投资组合的收益和风险,而不仅仅是单个资产的预测。
  • 协方差矩阵是关键:预测误差的协方差矩阵决定了最终投资组合的风险收益特征。
  • 普通ML方法的忽视:大多数机器学习方法很少关注对预测误差协方差矩阵的建模,这在金融应用中是一个巨大的缺陷。

阶段小结:我们学到了什么?

  1. 高维挑战:传统计量方法在处理海量金融数据时面临过拟合等问题。
  2. ML方法状态
    • Lasso通过正则化进行变量选择。
    • IPCA把公司特征映射为条件载荷;本章只完成理论研读,未验证其样本外优势。
    • 双重选择Lasso在相应稀疏性、识别和标准误条件下支持新因子边际贡献检验。
    • ADML可允许非线性辅助模型,并用正交矩条件降低一阶估计误差的影响;本章只学习原理,不报告实证结果。
  3. 如何下结论:是否优于传统模型必须由同一数据、同一基线和最终测试决定;不能用来源不清的旧数表证明市场差异或方法普遍优越。

未来展望:金融与机器学习的融合之路

机器学习正在深刻地重塑实证资产定价的研究范式。

  • 从“检验”少数理论驱动的因子,到从海量数据中“发现”新的定价模式。
  • 从线性假设到拥抱复杂的非线性关系。
  • 从关注个别系数的显著性,到更看重模型的样本外预测能力和经济价值。

作为未来的经济学家和金融从业者,掌握这些前沿工具,将是你们的核心竞争力。

综合项目:A股公司年度预测与模型检查

项目任务与允许数据

  • 公司池:固定的 8 家 A 股公司。
  • 核心数据:公开 stock/financial_statement.h5financial_data 键。
  • 预测目标:决策日所在财年的全年基本每股收益(EPS)。
  • 独立拓展:非标准审计意见不替代核心答案。
  • 点时约束:只用决策日前已披露信息;info_date 晚于决策日的记录不得进入特征。

项目验证合同

  • 决策时点:每年 6 月 30 日;目标为同一财年的全年基本 EPS,标签采用当前固定数据版本中该公司—财年最后披露版本,并且只允许在决策日后打开。
  • 最低基线:训练期均值/多数类,以及一个线性或浅树模型。
  • 验证:扩展窗口在预测日拟合,待对应标签披露后评分;2024 年最终测试为一次性测试期。
  • 边界:这是小样本课程检查,不代表全 A 股,不支持交易收益、因果关系或监管合规结论。

四个项目阶段与需要完成的内容

  1. 预测说明(O1):数据字典、公司清单、目标、决策时点、披露滞后图;完成内容 1 页设计说明和可复算样本数量变化表。
  2. Lasso 计算(O2):完成内容 L1 目标标注、完整惩罚比较结果、训练均值基线、非零系数数目和确定规格。
  3. 前沿边界(O3、O4):提交 IPCA 五格适用范围、双重选择—ADML 比较表、公司×年度误差和披露滞后敏感性;完成内容失败案例卡。
  4. 最终检查(O1—O4):只打开一次最终测试期;完成内容可运行代码、6 页以内报告、3 分钟口头说明和限制清单。

评分标准:与课程目标对齐

维度 权重 可自查证据
问题与数据边界 15% 决策时点、目标、样本数量变化、字段来源清楚
无泄漏验证 25% 扩展窗口、折内预处理、最终测试记录
模型与基线 20% 至少两个基线、调参比较结果、可复算指标
解释与风险检查 20% 误差切片、稳定性、结果不理想时如何解释、禁止性结论
复现与沟通 20% 环境、随机种子、代码、图表 alt、简洁报告

O1 通过综合练习 1 自查;O2 通过 Lasso 练习反馈 和综合练习 2 自查;O3 通过 IPCA 练习反馈 和综合练习 3 自查;O4 通过 ADML 练习反馈 和综合练习 3—4 自查。完成四个综合练习,即完成本章学习任务。

综合项目练习:先完成再查看答案

  • O1:点时样本数量变化。
  • O2:Lasso 候选与基线比较结果。
  • O3/O4:IPCA/ADML 适用范围与失败案例。
  • 最终证据:一次性最终测试、误差切片与敏感性。
  • 重做条件:缺任何 O1—O4 证据,或依据测试期改选模型。

代表性答案:数据与开发窗口

  1. 样本数量变化:逐年输出原始行数、年度 Q4 行数、决策日前可得行数、公司—财年最后版本行数与完整特征行数;任何一步为零都停止。
  2. 点时特征:规模、负债率与 ROA 均由决策日前最后可得财报构造;核心答案不使用 B/M。
  3. B/M 拓展边界:用决策日前最后可得账面权益除以同日总市值;不得把每股 bookValue 再除以股份数,非正权益单列。
  4. 扩展窗口:在 2019/2020 决策日拟合,分别到 2022/2023 选择截止日评分;每折重拟合预处理与模型,并保留全部候选。

代表性答案:最终测试与结论

  1. 最终测试:流程、阈值与超参数在 2023-06-30 前确定;2024 年只打开一次。
  2. 固定输出modelfold/test_period、样本数、基线指标、模型指标与相对基线差。
  3. 误差检查:公司×年度误差表;决策日提前到 4 月 30 日的敏感性;至少一个因披露滞后被排除的公司—年度。
  4. 结论模板:复杂模型未胜基线时,只能写“当前固定样本与设计没有证明增量预测价值”;SHAP 不作因果外推。

重做条件:随机拆分面板、用测试期选超参数、以财年替代实际披露日、只报最优模型,或把 8 家公司外推到全市场。

完整流程答案(1/6):读取固定公司年度财报

代码
from pathlib import Path  # 管理综合项目公开数据路径
from urllib.request import urlretrieve  # 复用本章隐藏设置单元安装的浏览器标识下载器
import numpy as np, pandas as pd  # 计算财务比率并构造点时公司年度面板
from IPython.display import HTML  # 把宽表拆成投影友好的并排HTML表
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择综合项目财务报表。
project_financial_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5'), Path('C:/qiufei/data/stock/financial_statement.h5'), Path('data/course/financial_statement.h5')] if candidate_path.exists()), Path('data/course/financial_statement.h5'))
if not project_financial_path.exists():
    project_financial_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/stock/financial_statement.h5', project_financial_path)
project_financial_key = 'financial_data'  # 固定 HDF5 数据键
project_company_pool = ['600276.XSHG', '600009.XSHG', '600585.XSHG', '603259.XSHG', '002415.XSHE', '600104.XSHG', '600019.XSHG', '600406.XSHG']  # 确定八家公司
project_columns = ['order_book_id', 'quarter', 'info_date', 'total_assets', 'total_liabilities', 'net_profit_parent_company', 'basic_earnings_per_share']  # 确定输入与目标字段
project_company_frames = []  # 收集选择性读取的公司记录
for project_company_id in project_company_pool:  # 避免全量加载 340MB 财务表
    project_company_frame = pd.read_hdf(project_financial_path, key=project_financial_key, where=f'order_book_id == "{project_company_id}"', columns=project_columns)  # 只读取当前公司和必要列
    project_company_frames.append(project_company_frame)  # 保存当前公司数据版本
project_raw_reports = pd.concat(project_company_frames, ignore_index=True)  # 合并固定公司池
project_raw_reports['info_date'] = pd.to_datetime(project_raw_reports['info_date'])  # 统一披露日类型
project_annual_reports = project_raw_reports.query("quarter.str.endswith('q4')", engine='python').copy()  # 只保留年度报告记录
project_annual_reports['fiscal_year'] = project_annual_reports['quarter'].str[:4].astype(int)  # 提取报告财年
project_target_versions = project_annual_reports.rename(columns={'fiscal_year': 'target_year', 'info_date': 'target_info_date', 'basic_earnings_per_share': 'target_eps'}).copy()  # 保留每个公司财年的全部标签披露版本
print({'文件': project_financial_path.name, '键': project_financial_key, '原始行': len(project_raw_reports), '年度Q4行': len(project_annual_reports), '公司数': project_annual_reports['order_book_id'].nunique()})  # 输出固定输入说明
{'文件': 'financial_statement.h5', '键': 'financial_data', '原始行': 622, '年度Q4行': 158, '公司数': 8}

完整流程答案(2/6):点时面板与披露滞后

代码
def build_eps_panel(decision_month_day):  # 按给定决策日构造无前瞻偏差的 EPS 面板
    target_reports = project_target_versions.sort_values(['order_book_id', 'target_year', 'target_info_date']).drop_duplicates(['order_book_id', 'target_year'], keep='last')  # 最终版本只用于验证窗与最终测试窗事后评分
    prediction_grid = target_reports[['order_book_id', 'target_year']].drop_duplicates().copy()  # 以已观测目标的公司年构造预测网格
    prediction_grid['decision_date'] = pd.to_datetime(prediction_grid['target_year'].astype(str) + decision_month_day)  # 确定每个目标年的决策日
    feature_candidates = prediction_grid.merge(project_annual_reports, on='order_book_id', how='left')  # 为每个预测年列举本公司历史年报
    feature_candidates = feature_candidates.query('fiscal_year < target_year and info_date <= decision_date').copy()  # 只允许目标年之前且决策日已披露的年报
    eligible_features = feature_candidates.sort_values(['order_book_id', 'target_year', 'info_date', 'fiscal_year']).drop_duplicates(['order_book_id', 'target_year'], keep='last')  # 选择决策日前最新可得版本,不假定年报恰好滞后一年
    panel = eligible_features.merge(target_reports[['order_book_id', 'target_year', 'target_info_date', 'target_eps']], on=['order_book_id', 'target_year'], how='inner')  # 合并点时特征与同一目标年 EPS
    panel = panel.query('target_info_date > decision_date').copy()  # 只在决策日后打开目标,排除标签泄漏
    panel['log_assets'] = np.log(panel['total_assets'])  # 构造对数规模特征
    panel['debt_ratio'] = panel['total_liabilities'] / panel['total_assets']  # 构造负债率特征
    panel['roa'] = panel['net_profit_parent_company'] / panel['total_assets']  # 构造上一财年 ROA 特征
    panel = panel.replace([np.inf, -np.inf], np.nan).dropna(subset=['target_eps']).sort_values(['target_year', 'order_book_id'])  # 清除无效目标并按时间排序
    assert not panel.duplicated(['order_book_id', 'target_year']).any(), '公司—目标年键不唯一'  # 强制面板唯一性
    annual_counts = target_reports.groupby('target_year').size().rename('年度Q4目标行')  # 统计当前数据版本中的年度目标行
    disclosed_counts = feature_candidates.groupby('target_year').size().rename('决策日前候选行')  # 统计决策日前已披露的历史年报
    unique_counts = eligible_features.groupby('target_year').size().rename('公司年唯一特征行')  # 统计去重后的点时特征行
    complete_counts = panel.dropna(subset=['log_assets', 'debt_ratio', 'roa']).groupby('target_year').size().rename('完整特征行')  # 统计三个特征均可用的行
    panel.attrs['sample_flow'] = pd.concat([annual_counts, disclosed_counts, unique_counts, complete_counts], axis=1).fillna(0).astype(int)  # 保存可复算的逐年样本数量变化
    return panel  # 返回给定决策日的完整点时样本
project_panel = build_eps_panel('-06-30')  # 构造六月末主分析面板
代码
late_feature_rows = project_annual_reports.assign(target_year=project_annual_reports['fiscal_year'] + 1, decision_date=lambda frame: pd.to_datetime(frame['target_year'].astype(str) + '-06-30')).query('info_date > decision_date')  # 定位披露滞后排除记录
sample_flow_recent = project_panel.attrs['sample_flow'].tail(12)  # 保留十二个最近目标年的完整样本数量变化
sample_flow_recent.index.name = '目标年份'
sample_flow_halves = [sample_flow_recent.iloc[:6], sample_flow_recent.iloc[6:]]  # 将十二行拆成两张并排短表,避免默认输出滚动
display(HTML('<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">' + ''.join(flow.to_html() for flow in sample_flow_halves) + '</div>'))  # 同屏展示逐年全量证据
display(late_feature_rows[['order_book_id', 'quarter', 'info_date', 'decision_date']].rename(columns={
    'order_book_id': '证券代码', 'quarter': '报告期', 'info_date': '披露日', 'decision_date': '预测日'
}).head(1))
年度Q4目标行 决策日前候选行 公司年唯一特征行 完整特征行
目标年份
2014 8 55 7 7
2015 8 62 7 7
2016 8 69 7 7
2017 8 76 7 7
2018 8 78 8 8
2019 8 86 8 8
年度Q4目标行 决策日前候选行 公司年唯一特征行 完整特征行
目标年份
2020 8 94 8 8
2021 8 102 8 8
2022 8 110 8 8
2023 8 118 8 8
2024 8 126 8 8
2025 8 134 8 8
证券代码 报告期 披露日 预测日
3 600276.XSHG 2005q4 2007-02-08 2006-06-30

完整流程答案(2b/6):逐拟合时点确定标签数据版本

每个验证折或最终重训都先定义 fit_cutoff_date,然后在该截止日内选择公司—财年的最后可得标签版本。禁止先取 2026 固定数据版本的最终修订值再回测。

代码
def build_label_snapshot(feature_panel, fit_cutoff_date):  # 为单个拟合时点构造可实现的训练标签数据版本
    fit_cutoff = pd.Timestamp(fit_cutoff_date)  # 将检查截止日固定为唯一时间类型
    label_candidates = project_target_versions.query('target_info_date <= @fit_cutoff').copy()  # 排除拟合日之后才披露或修订的标签
    available_labels = label_candidates.sort_values(['order_book_id', 'target_year', 'target_info_date']).drop_duplicates(['order_book_id', 'target_year'], keep='last')  # 在截止日前选择最后可得版本
    feature_columns = [column for column in feature_panel.columns if column not in ['target_info_date', 'target_eps']]  # 防止事后目标列混入训练数据版本
    snapshot = feature_panel[feature_columns].merge(available_labels[['order_book_id', 'target_year', 'target_info_date', 'target_eps']], on=['order_book_id', 'target_year'], how='inner')  # 合并时点一致的特征与标签
    snapshot = snapshot.dropna(subset=['target_eps']).sort_values(['target_year', 'order_book_id'])  # 只保留已披露且数值完整的标签
    assert snapshot['target_info_date'].le(fit_cutoff).all(), '训练标签晚于拟合截止日'  # 阻止未来修订标签进入模型
    assert not snapshot.duplicated(['order_book_id', 'target_year']).any(), '标签数据版本公司年不唯一'  # 强制每个拟合样本唯一
    snapshot.attrs['label_flow'] = {'拟合截止日': fit_cutoff.date(), '候选标签': len(label_candidates), '截止日前唯一公司年': len(available_labels), '完整训练行': len(snapshot)}  # 保存逐折样本数量变化
    return snapshot  # 返回可实现的点时训练样本

完整流程答案(3/6):扩展窗口候选检查

这一段先确定每个拟合日与选择截止日,再依次展示双重选择估计器、候选集合、逐折比较结果与折间选择检查;不得只保留最终胜者。

完整流程答案(3/6a):双重选择估计器

处理变量固定为负债率,控制变量为规模与 ROA。两个 Lasso 只在每个训练折拟合;最终低维线性模型使用两次选择的并集。该实现用于预测候选与系数稳定性检查,不宣称负债率对 EPS 的因果效应。

代码
from sklearn.base import BaseEstimator, RegressorMixin  # 提供可被clone的估计器接口
import numpy as np  # 为双重选择类的拟合与预测显式提供数组运算
from sklearn.impute import SimpleImputer  # 在每个训练折内估计缺失值中位数
from sklearn.linear_model import Lasso, LinearRegression  # 执行两次选择与最终低维拟合
from sklearn.preprocessing import StandardScaler  # 在每个训练折内逐特征确定缩放统计量
代码
class DoubleSelectionRegressor(BaseEstimator, RegressorMixin):  # 封装折内双重选择流程
    def __init__(self, treatment_index=1, alpha=0.01):  # 固定处理变量位置和惩罚强度
        self.treatment_index = treatment_index  # 保存负债率在特征矩阵中的位置
        self.alpha = alpha  # 保存事先确定L1惩罚
    def fit(self, feature_matrix, outcome_values):  # 只在当前训练折估计选择与系数
        feature_array = np.asarray(feature_matrix)  # 统一输入为数值矩阵
        self.imputer_ = SimpleImputer(strategy='median').fit(feature_array)  # 只从训练折估计缺失中位数
        imputed_features = self.imputer_.transform(feature_array)  # 转换当前训练特征
        self.scaler_ = StandardScaler().fit(imputed_features)  # 只从当前训练折逐特征估计均值与标准差
        scaled_features = self.scaler_.transform(imputed_features)  # 用当前训练折逐特征统计量转换训练特征
        control_positions = [position for position in range(scaled_features.shape[1]) if position != self.treatment_index]  # 排除处理变量得到控制集
        controls = scaled_features[:, control_positions]  # 构造规模与ROA控制矩阵
        outcome_selector = Lasso(alpha=self.alpha, max_iter=5000).fit(controls, outcome_values)  # 结果方程筛选控制变量
        treatment_selector = Lasso(alpha=self.alpha, max_iter=5000).fit(controls, scaled_features[:, self.treatment_index])  # 处理方程筛选控制变量
        self.outcome_selected_controls_ = np.flatnonzero(np.abs(outcome_selector.coef_) > 1e-10).tolist()  # 保存结果方程选择集供算法检查
        self.treatment_selected_controls_ = np.flatnonzero(np.abs(treatment_selector.coef_) > 1e-10).tolist()  # 保存处理方程选择集供算法检查
        selected_controls = np.union1d(self.outcome_selected_controls_, self.treatment_selected_controls_).astype(int)  # 取两次选择并集
        self.selected_positions_ = [self.treatment_index] + [control_positions[position] for position in selected_controls]  # 固定最终低维特征位置
        self.final_model_ = LinearRegression().fit(scaled_features[:, self.selected_positions_], outcome_values)  # 拟合最终预测模型
        self.treatment_coefficient_ = float(self.final_model_.coef_[0])  # 保存标准化负债率系数但不赋予因果含义
        return self  # 返回已拟合估计器供Pipeline调用
    def predict(self, feature_matrix):  # 对时间外窗口生成预测
        imputed_features = self.imputer_.transform(np.asarray(feature_matrix))  # 使用训练折中位数转换新样本
        scaled_features = self.scaler_.transform(imputed_features)  # 使用确定的训练折逐特征统计量转换新样本
        return self.final_model_.predict(scaled_features[:, self.selected_positions_])  # 用确定并集与系数预测

完整流程答案(3/6a):候选与窗口设置

代码
from sklearn.base import clone  # 为每个时间窗复制独立流程
from sklearn.ensemble import RandomForestRegressor  # 提供事先列出的非线性候选
from sklearn.impute import SimpleImputer  # 仅在训练折估计中位数插补
from sklearn.linear_model import Lasso, Ridge  # 提供L1稀疏与L2线性候选
from sklearn.metrics import mean_absolute_error  # 统一验证与测试指标
from sklearn.pipeline import make_pipeline  # 将插补、缩放和模型绑定为折内流程
from sklearn.preprocessing import StandardScaler  # 仅在各训练折逐特征估计线性特征的均值与标准差
project_features = ['log_assets', 'debt_ratio', 'roa']  # 确定三个点时财务特征
candidate_models = {  # 事先列出候选及固定超参数网格
    # 登记低惩罚 Ridge 候选,供点时 EPS 预测的同窗比较。
    'Ridge(0.1)': make_pipeline(SimpleImputer(strategy='median'), StandardScaler(), Ridge(alpha=0.1)),  # 缩放统计量只由当前训练折逐特征估计
    # 登记中等惩罚 Ridge 候选,检验线性收缩强度的预测取舍。
    'Ridge(1.0)': make_pipeline(SimpleImputer(strategy='median'), StandardScaler(), Ridge(alpha=1.0)),  # 缩放统计量只由当前训练折逐特征估计
    'Lasso(0.01)': make_pipeline(SimpleImputer(strategy='median'), StandardScaler(), Lasso(alpha=0.01, max_iter=5000)),  # 逐特征折内缩放后检验章内核心 Lasso
    '双重选择Lasso': DoubleSelectionRegressor(treatment_index=1, alpha=0.01),  # 将高级方法放入相同验证与最终测试流程
    # 登记浅层随机森林候选,比较受限非线性与线性基准的最终测试表现。
    '浅层森林': make_pipeline(SimpleImputer(strategy='median'), RandomForestRegressor(n_estimators=100, max_depth=2, min_samples_leaf=3, random_state=42))
}
validation_years = [2019, 2020]  # 只使用能在最终模型选择日前取得标签的开发期验证年
validation_records = []  # 保存全部候选与训练均值基线
double_selection_audits = []  # 保存两次选择集合、并集与目标系数
validation_fold_inputs = []  # 缓存各折按时点确定的训练与验证样本

完整流程答案(3/6a):逐折拟合

代码
for validation_year in validation_years:  # 逐年执行扩展窗口验证
    fit_cutoff_date = pd.Timestamp(f'{validation_year}-06-30')  # 确定当前验证折的实际拟合截止日
    selection_cutoff_date = pd.Timestamp(f'{validation_year + 3}-06-30')  # 按公开数据版本约三年披露滞后定义选择截止日
    training_snapshot = build_label_snapshot(project_panel, fit_cutoff_date)  # 只打开截止日前已披露的训练标签版本
    selection_snapshot = build_label_snapshot(project_panel, selection_cutoff_date)  # 只用选择截止日前可得的验证标签版本
    training_rows = training_snapshot.query('target_year < @validation_year').copy()  # 只使用验证年之前且当时可得的标签
    validation_rows = selection_snapshot.query('target_year == @validation_year').copy()  # 评分时不读取截止日后的最终修订标签
    assert len(training_rows) > 0 and len(validation_rows) > 0, '综合项目训练折或验证折为空'  # 先阻止空表让时点断言真空通过
    assert training_rows['target_info_date'].le(fit_cutoff_date).all(), '验证折含晚披露训练标签'  # 对每折执行标签时点断言
    assert validation_rows['target_info_date'].le(selection_cutoff_date).all(), '验证折含选择截止日后标签'  # 阻止验证指标穿越披露时点
    training_flow = training_snapshot.attrs['label_flow']  # 读取拟合截止日的标签样本数量变化
    selection_flow = selection_snapshot.attrs['label_flow']  # 读取选择截止日的标签样本数量变化
    validation_total = project_panel['target_year'].eq(validation_year).sum()  # 统计该验证年最终评分池规模
    print({'验证年': validation_year, '拟合截止日': fit_cutoff_date.date(), '拟合候选标签': training_flow['候选标签'], '拟合唯一公司年': training_flow['截止日前唯一公司年'], '训练行': len(training_rows), '选择截止日': selection_cutoff_date.date(), '选择候选标签': selection_flow['候选标签'], '验证行': len(validation_rows), '验证排除行': validation_total - len(validation_rows), '验证标签最晚披露': validation_rows['target_info_date'].max().date()})  # 报告版本、两阶段时点与排除流
    baseline_predictions = np.repeat(training_rows['target_eps'].mean(), len(validation_rows))  # 构造训练期均值基线
    validation_records.append({'model': '训练均值', 'fold': validation_year, 'n': len(validation_rows), 'mae': mean_absolute_error(validation_rows['target_eps'], baseline_predictions)})  # 保存基线结果
    validation_fold_inputs.append({'fold': validation_year, 'selection_cutoff': selection_cutoff_date, 'training_rows': training_rows, 'validation_rows': validation_rows})  # 交给下一代码块复用同一确定样本
{'验证年': 2019, '拟合截止日': datetime.date(2019, 6, 30), '拟合候选标签': 86, '拟合唯一公司年': 86, '训练行': 67, '选择截止日': datetime.date(2022, 6, 30), '选择候选标签': 110, '验证行': 8, '验证排除行': 0, '验证标签最晚披露': datetime.date(2022, 4, 30)}
{'验证年': 2020, '拟合截止日': datetime.date(2020, 6, 30), '拟合候选标签': 94, '拟合唯一公司年': 94, '训练行': 74, '选择截止日': datetime.date(2023, 6, 30), '选择候选标签': 118, '验证行': 8, '验证排除行': 0, '验证标签最晚披露': datetime.date(2023, 4, 29)}
代码
for fold_input in validation_fold_inputs:  # 在已确定的扩展窗口上拟合所有候选
    validation_year = fold_input['fold']  # 读取当前验证年
    selection_cutoff_date = fold_input['selection_cutoff']  # 读取该折标签选择截止日
    training_rows = fold_input['training_rows']  # 读取拟合日确定的训练样本
    validation_rows = fold_input['validation_rows']  # 读取选择日确定的验证样本
    for model_name, model_pipeline in candidate_models.items():  # 不遗漏任何事先列出候选
        fitted_pipeline = clone(model_pipeline).fit(training_rows[project_features], training_rows['target_eps'])  # 折内拟合完整流程
        validation_predictions = fitted_pipeline.predict(validation_rows[project_features])  # 生成当前验证年预测
        validation_records.append({'model': model_name, 'fold': validation_year, 'n': len(validation_rows), 'selection_cutoff': selection_cutoff_date.date(), 'mae': mean_absolute_error(validation_rows['target_eps'], validation_predictions)})  # 保存候选指标与可得日
        if model_name == '双重选择Lasso':  # 单独记录算法选择过程而不宣称有效推断
            control_names = [name for position, name in enumerate(project_features) if position != fitted_pipeline.treatment_index]  # 建立控制位置到具名变量的映射
            outcome_names = [control_names[position] for position in fitted_pipeline.outcome_selected_controls_]  # 翻译结果方程选择集
            treatment_names = [control_names[position] for position in fitted_pipeline.treatment_selected_controls_]  # 翻译处理方程选择集
            union_names = sorted(set(outcome_names) | set(treatment_names))  # 构造不含处理变量的具名控制并集
            double_selection_audits.append({'fold': validation_year, '结果方程控制集': outcome_names, '处理方程控制集': treatment_names, '控制并集': union_names, '并集控制数': len(union_names), '最终回归变量数': 1 + len(union_names), '负债率系数': fitted_pipeline.treatment_coefficient_})  # 保存可逐项核对的算法证据

完整流程答案(3/6a):候选比较

代码
validation_table = pd.DataFrame(validation_records)  # 形成完整验证比较结果
validation_projection = validation_table.pivot(index='model', columns='fold', values='mae').reset_index()  # 将逐窗长表压成投影可读的六行宽表
display(validation_projection.round(4))  # 同屏展示每个候选在两个验证年的MAE
double_selection_projection = pd.DataFrame(double_selection_audits)  # 保留两次具名选择、控制并集与最终回归规模
表 1: 所有事先列出候选的逐年扩展窗口 MAE
fold model 2019 2020
0 Lasso(0.01) 1.1368 1.0948
1 Ridge(0.1) 1.1315 1.0849
2 Ridge(1.0) 1.1339 1.0863
3 双重选择Lasso 1.1313 1.0847
4 浅层森林 1.1667 1.3148
5 训练均值 1.2042 1.1571

完整流程答案(3/6b):双重选择折间检查

代码
selection_audit_display = double_selection_projection.copy()  # 为投影输出建立紧凑副本
feature_labels = {'log_assets': '总资产对数', 'roa': '资产回报率'}
selection_audit_display['两个方程选择'] = selection_audit_display.apply(
    lambda row: f"结果方程:{'、'.join(feature_labels.get(name, name) for name in row['结果方程控制集']) or '无'};处理方程:{'、'.join(feature_labels.get(name, name) for name in row['处理方程控制集']) or '无'}", axis=1
)
selection_audit_display['控制变量并集'] = selection_audit_display['控制并集'].apply(
    lambda names: '、'.join(feature_labels.get(name, name) for name in names) or '无'
)
display(selection_audit_display[['fold', '两个方程选择', '控制变量并集', '最终回归变量数', '负债率系数']].rename(columns={'fold': '验证年份'}).round(4))
表 2: 双重选择的具名控制集、并集规模与负债率系数
验证年份 两个方程选择 控制变量并集 最终回归变量数 负债率系数
0 2019 结果方程:总资产对数、资产回报率;处理方程:总资产对数、资产回报率 总资产对数、资产回报率 3 0.0864
1 2020 结果方程:总资产对数、资产回报率;处理方程:总资产对数、资产回报率 总资产对数、资产回报率 3 0.0464

完整流程答案(4/6):确定模型后一次打开测试期

代码
candidate_validation_mean = validation_table.query("model != '训练均值'").groupby('model')['mae'].mean()  # 只用开发期验证指标汇总候选
frozen_model_name = candidate_validation_mean.idxmin()  # 在打开测试期前确定候选名称
final_fit_cutoff = pd.Timestamp('2023-06-30')  # 在首个最终测试预测年决策日确定最终拟合信息集
assert max(pd.Timestamp(f'{year + 3}-06-30') for year in validation_years) <= final_fit_cutoff, '验证标签尚未到选择截止日'  # 保证延迟披露的选模证据在模型选择日前可得
development_snapshot = build_label_snapshot(project_panel, final_fit_cutoff)  # 按最终拟合时点重建可得标签数据版本
development_rows = development_snapshot.query('target_year < 2024').copy()  # 只使用模型选择日已披露且早于最终测试年的开发样本
testing_rows = project_panel.query('target_year == 2024').copy()  # 只在模型确定后的下一年打开一次最终测试
assert len(development_rows) > 0 and len(testing_rows) > 0, '开发期或最终测试期为空'  # 样本异常时明确停止
assert development_rows['target_info_date'].le(final_fit_cutoff).all(), '最终重训含晚披露标签'  # 阻止当前最终修订值进入历史拟合
frozen_pipeline = clone(candidate_models[frozen_model_name]).fit(development_rows[project_features], development_rows['target_eps'])  # 仅用开发期拟合确定模型
testing_predictions = frozen_pipeline.predict(testing_rows[project_features])  # 生成一次性最终测试预测
testing_baseline = np.repeat(development_rows['target_eps'].mean(), len(testing_rows))  # 构造开发期均值基线
test_table = pd.DataFrame({'model': [frozen_model_name, '训练均值'], 'fold/test_period': ['2024', '2024'], 'n': [len(testing_rows), len(testing_rows)], 'mae': [mean_absolute_error(testing_rows['target_eps'], testing_predictions), mean_absolute_error(testing_rows['target_eps'], testing_baseline)]})  # 汇总同窗指标
test_table['relative_to_baseline'] = 1 - test_table['mae'] / test_table.loc[test_table['model'].eq('训练均值'), 'mae'].iloc[0]  # 计算相对基线差
print({'确定模型': frozen_model_name, '模型选择日期': final_fit_cutoff.date(), '确定依据': '仅使用模型选择日前已经披露的验证标签', **development_snapshot.attrs['label_flow'], '2024目标用途': '确定后仅一次性测试评分'})  # 区分选模标签与确定后测试标签
display(test_table.rename(columns={
    'model': '模型', 'fold/test_period': '测试年份', 'n': '样本数',
    'mae': '平均绝对误差', 'relative_to_baseline': '相对基线改善'
}).round(4))
{'确定模型': '双重选择Lasso', '模型选择日期': datetime.date(2023, 6, 30), '确定依据': '仅使用模型选择日前已经披露的验证标签', '拟合截止日': datetime.date(2023, 6, 30), '候选标签': 118, '截止日前唯一公司年': 118, '完整训练行': 98, '2024目标用途': '确定后仅一次性测试评分'}
表 3: 2023-06-30 确定选择后 2024 年 EPS 最终测试结果
模型 测试年份 样本数 平均绝对误差 相对基线改善
0 双重选择Lasso 2024 8 0.9785 -0.4802
1 训练均值 2024 8 0.6611 0.0000

完整流程答案(5/6):公司×年度误差切片

代码
error_table = testing_rows[['order_book_id', 'target_year', 'target_eps']].copy()  # 保留最终测试期对象与真实目标
error_table['prediction'] = testing_predictions  # 写入确定模型预测
error_table['absolute_error'] = np.abs(error_table['target_eps'] - error_table['prediction'])  # 计算逐公司年度绝对误差
error_matrix = error_table.pivot(index='order_book_id', columns='target_year', values='absolute_error')
error_matrix.index.name = '证券代码'
error_matrix.columns.name = '目标年份'
display(error_matrix.round(4))
表 4: 确定 EPS 模型在最终测试期的公司年度绝对误差
目标年份 2024
证券代码
002415.XSHE 0.4529
600009.XSHG 0.3985
600019.XSHG 1.5876
600104.XSHG 1.8344
600276.XSHG 0.5829
600406.XSHG 0.4525
600585.XSHG 0.7086
603259.XSHG 1.8104

完整流程答案(6/6):4 月 30 日敏感性

代码
april_panel = build_eps_panel('-04-30')  # 用更早决策日重建完整点时面板
april_fit_cutoff = final_fit_cutoff  # 沿用 2024 年两种决策日前均已确定的共同信息集
april_snapshot = build_label_snapshot(april_panel, april_fit_cutoff)  # 用同一模型选择日隔离决策日变化
april_development = april_snapshot.query('target_year < 2024').copy()  # 保持相同模型选择日且只用当时可得标签
april_testing = april_panel.query('target_year == 2024').copy()  # 与六月口径比较同一最终测试年
assert len(april_development) > 0 and len(april_testing) > 0, '四月敏感性窗口为空'  # 无可用样本时明确停止
assert april_development['target_info_date'].le(april_fit_cutoff).all(), '四月重训含晚披露标签'  # 对敏感性拟合执行时点断言
april_pipeline = clone(candidate_models[frozen_model_name]).fit(april_development[project_features], april_development['target_eps'])  # 保持已确定模型规格重新拟合
april_predictions = april_pipeline.predict(april_testing[project_features])  # 生成四月口径最终测试预测
sensitivity_table = pd.DataFrame({'decision_date': ['06-30', '04-30'], 'development_n': [len(development_rows), len(april_development)], 'test_n': [len(testing_rows), len(april_testing)], 'test_mae': [mean_absolute_error(testing_rows['target_eps'], testing_predictions), mean_absolute_error(april_testing['target_eps'], april_predictions)]})  # 汇总样本数量变化和指标变化
display(sensitivity_table.rename(columns={
    'decision_date': '预测日', 'development_n': '开发期样本数',
    'test_n': '测试样本数', 'test_mae': '测试期平均绝对误差'
}).round(4))
表 5: 决策日提前至 4 月 30 日后的样本数量变化与最终测试敏感性
预测日 开发期样本数 测试样本数 测试期平均绝对误差
0 06-30 98 8 0.9785
1 04-30 98 8 0.9735

实际结果判读模板:若确定复杂模型未胜训练均值,只能写“当前固定八家公司、披露口径与样本期没有证明增量预测价值”。即使胜出,也不代表交易收益、因果关系或全 A 股外推成立;测试表不得反馈到下一轮模型选择。

本章小结

  • 学完后应能:能区分 FM 三类截距/残差、payoff/gross/excess return,并复算点时 EPS 项目的逐窗与最终测试表。
  • 选择条件:模型只按开发期指标与预设 tie-break 确定;SDF/beta 表示还需资产集合、矩条件、可交易性与张成假设。
  • 未建立:IPCA/ADML 只完成核心适用范围与条件比较,未执行公开估计;当前项目不证明 alpha、因果、全 A 股外推或交易收益。
  • 后续理由:下一轮研究应先扩大合法点时样本并事先确定外部验证,而不是继续从最终测试结果调参。

参考文献

Barillas, Francisco, 和 Jay Shanken. 2018年. 《Comparing Asset Pricing Models》. The Journal of Finance 73 (2): 715~54. https://doi.org/10.1111/jofi.12607.
Chernozhukov, Victor, Whitney K. Newey, 和 Rahul Singh. 2022年. 《Automatic Debiased Machine Learning of Causal and Structural Effects》. Econometrica 90 (3): 967~1027. https://doi.org/10.3982/ECTA18515.
Feng, Guanhao, Stefano Giglio, 和 Dacheng Xiu. 2020年. 《Taming the Factor Zoo: A Test of New Factors》. The Journal of Finance 75 (3): 1327~70. https://doi.org/10.1111/jofi.12883.
Gu, Shihao, Bryan Kelly, 和 Dacheng Xiu. 2020年. 《Empirical Asset Pricing via Machine Learning》. The Review of Financial Studies 33 (5): 2223~73. https://doi.org/10.1093/rfs/hhaa009.
Kelly, Bryan, Seth Pruitt, 和 Yinan Su. 2019年. 《Characteristics Are Covariances: A Unified Model of Risk and Return》. Journal of Financial Economics 134 (3): 501~24. https://doi.org/10.1016/j.jfineco.2019.05.001.
Shanken, Jay. 1992年. 《On the Estimation of Beta-Pricing Models》. The Review of Financial Studies 5 (1): 1~33. https://doi.org/10.1093/rfs/5.1.1.