6  线性模型选择与正则化 (Linear Model Selection and Regularization)

6.1 导读

线性模型在变量多、信号弱或预测变量高度相关时容易产生不稳定估计。本章介绍子集选择、Ridge、Lasso、PCR 与 PLS,并把它们放入第 5 章建立的同一前向验证框架。经验部分分为两条互不混淆的路线:连续前向最大回撤用 RMSE 比较回归器,二元回撤事件用 Brier 分数比较概率分类器。

6.2 学习目标

完成本章后,读者应能够:

  1. 解释 Ridge 与 Lasso 的惩罚几何、收缩方式和变量选择差异。
  2. 计算并辨析 Cp、AIC、BIC 与调整 R²,说明它们为何不能替代真正的时间外评价。
  3. 在训练折内完成填补、标准化、降维和超参数选择,避免预处理泄漏。
  4. 在同一组前向折上比较 OLS、Ridge、Lasso、PCR 与 PLS 的连续响应预测误差。
  5. 比较 L2/L1 逻辑回归、PCA-score 逻辑分类器与 PLS-score 逻辑分类器的样本外概率质量。
  6. 区分内层选模与外层误差估计,并用选择频率评价 Lasso 入选集合的稳定性。

6.3 引言 (Introduction)

在回归设置中,标准线性模型

\[ Y = \beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p + \epsilon \tag{6.1}\]

通常用于描述响应\(Y\)与一组变量\(X_1, X_2, \ldots, X_p\)之间的关系。我们在第3章中看到,通常使用最小二乘法拟合这个模型。

然而,为什么我们可能想要使用另一种拟合程序来代替最小二乘?正如我们将看到的,替代拟合程序可以产生更好的预测准确性模型可解释性:

  1. 预测准确性:只要响应与预测变量之间的真实关系近似线性,最小二乘估计将具有低偏差。但如果\(n \approx p\)\(p > n\),最小二乘估计可能具有很高的方差,导致过拟合。通过约束或收缩估计的系数,我们通常可以以可忽略的偏差增加为代价显著减少方差。

  2. 模型可解释性:多元回归模型中的许多或所有变量实际上可能与响应无关。通过删除这些变量(即将相应的系数估计设置为零),我们可以获得更容易解释的模型。最小二乘法不太可能产生恰好为零的系数估计。

在本章中,我们讨论三类重要的方法来改进简单线性模型:

  • 子集选择(Subset Selection):识别与响应相关的预测变量子集
  • 收缩方法(Shrinkage):拟合包含所有\(p\)个预测变量的模型,但将系数估计收缩向零
  • 降维方法(Dimension Reduction):将预测变量投影到\(M\)维子空间

案例背景:从同期恒等式到可评价预测

杜邦分解说明净利率、资产周转率与权益乘数在口径对齐时能够代数重构同期 ROE。把这些同期构成项放进 ROE 回归,得到的高拟合度首先是目标派生泄漏警报,不能解释为未来盈利能力的证据。本章不再用这类同期草图报告预测结果。

后文的可执行案例改用第 2 章共享行情样本:以预测日已经观测到的收益、动量、波动率和成交量特征,预测随后 20 个交易日的连续最大回撤;另把是否跌破 -10% 作为二元事件。连续响应与二元事件分别用 RMSE 和 Brier 分数评价,测试期保持封存。

6.4 子集选择 (Subset Selection)

子集选择涉及识别与响应相关的预测变量子集,然后在减少后的变量集上拟合最小二乘模型。这里的“相关”是给定候选集合与样本设计下的预测关联,不是资源配置效果或因果作用。

6.4.1 最优子集选择 (Best Subset Selection)

最优子集选择为每个可能的预测变量组合拟合一个单独的最小二乘回归。

算法 6.1:最优子集选择

  1. \(M_0\) 表示不含预测变量的零模型。
  2. 对每个 \(k=1,\ldots,p\),拟合所有含 \(k\) 个预测变量的模型,并以训练 RSS 最小者记为 \(M_k\)
  3. 在预先声明的验证折上比较 \(M_0,\ldots,M_p\);Cp、AIC、BIC 与调整 R²只作为训练期复杂度诊断。

对含截距且参数个数为 \(d_m\) 的候选模型,常用量可写为

\[ C_p=\frac{\operatorname{RSS}_m}{\widehat{\sigma}^2}-n+2d_m, \tag{6.2}\]

\[ \operatorname{AIC}_m=n\log(\operatorname{RSS}_m/n)+2d_m, \qquad \operatorname{BIC}_m=n\log(\operatorname{RSS}_m/n)+d_m\log n, \tag{6.3}\]

\[ \bar R_m^2=1-\frac{\operatorname{RSS}_m/(n-d_m)}{\operatorname{TSS}/(n-1)}. \tag{6.4}\]

其中 \(\widehat{\sigma}^2\) 需要来自合适的方差估计,AIC/BIC 的写法依赖高斯同方差似然近似。四个量都在拟合与复杂度之间作权衡,但不检验时间漂移,也不允许反复查看封存测试集。训练 \(R^2\) 随变量增加而不减,因此不能单独承担模型选择。

数值例题:四项准则如何使用共同输入。 设同一响应、同一批 \(n=100\) 个观测的总平方和为 TSS = 1000,独立误差方差估计为 \(\widehat\sigma^2=4\)。三个候选模型的“参数数(含截距),RSS”依次为 \((2,800)\)\((4,720)\)\((8,700)\)。下面逐行代入,而不是只报告排序。

表 6.1: 共同样本上 Cp、AIC、BIC 与调整 R²的完整数值比较
import math  # 计算信息准则中的自然对数
import pandas as pd  # 整理可复算的候选模型比较表
criteria_inputs = [('M1', 2, 800), ('M2', 4, 720), ('M3', 8, 700)]  # 固定模型名、参数数和RSS
criteria_rows = []  # 保存逐模型代入结果
for model_name, parameter_count, residual_sum_squares in criteria_inputs:  # 对共同样本逐项计算
    cp_value = residual_sum_squares / 4 - 100 + 2 * parameter_count  # 代入Cp公式
    aic_value = 100 * math.log(residual_sum_squares / 100) + 2 * parameter_count  # 代入AIC
    bic_value = 100 * math.log(residual_sum_squares / 100) + parameter_count * math.log(100)  # 代入BIC
    adjusted_r2 = 1 - (residual_sum_squares / (100 - parameter_count)) / (1000 / 99)  # 代入调整R²
    criteria_rows.append({'model': model_name, 'd': parameter_count, 'RSS': residual_sum_squares, 'Cp': cp_value, 'AIC': aic_value, 'BIC': bic_value, 'adjusted_R2': adjusted_r2})  # 保存完整证据
criteria_example_table = pd.DataFrame(criteria_rows)  # 建立规范结果表
print(criteria_example_table.round({'Cp': 2, 'AIC': 2, 'BIC': 2, 'adjusted_R2': 3}).to_string(index=False))  # 输出可核对数值
model  d  RSS    Cp    AIC    BIC  adjusted_R2
   M1  2  800 104.0 211.94 217.15        0.192
   M2  4  720  88.0 205.41 215.83        0.257
   M3  8  700  91.0 210.59 231.43        0.247

本例中,\(M_2\)\(C_p=88\)、AIC = 205.41、BIC = 215.83,调整 \(R^2=0.258\);四项准则都选择 \(M_2\)。一致排序不是定理:BIC 的复杂度惩罚通常比 AIC 更强,\(C_p\) 又依赖 \(\widehat\sigma^2\)。这些量只有在响应、样本、候选似然和参数计数口径一致时才可横向比较;即使它们一致,也仍须由后续前向评分块检验时间外表现。

同期恒等式核对。 若使用当期杜邦构成项解释当期 ROE,应把任务明确标为代数诊断。它可以检查口径和数据质量,却不能据此声称变量预测未来 ROE、具有经济机制或代表随机抽样总体。真正的预测比较必须使用已公布特征、未来标签和前向切分。

6.4.2 逐步选择 (Stepwise Selection)

对于计算原因,当\(p\)非常大时,最优子集选择无法应用。逐步选择(stepwise selection)方法提供了计算效率高的替代方案。

6.4.2.1 前向逐步选择 (Forward Stepwise Selection)

前向逐步选择从一个不包含预测变量的模型开始,然后将预测变量一个接一个地添加到模型中,直到所有预测变量都在模型中。在每一步中,添加对拟合提供最大额外改进的变量。

算法 6.2: 前向逐步选择

  1. \(M_0\)表示零模型,不包含任何预测变量。
  2. 对于\(k = 0, \ldots, p-1\):
    1. 考虑所有通过向\(M_k\)添加一个额外预测变量来扩展\(M_k\)中预测变量的\(p-k\)个模型。
    2. 从这些\(p-k\)个模型中选择最好的,称为\(M_{k+1}\)
  3. 使用验证集误差、\(C_p\)、BIC或调整\(R^2\)\(M_0, \ldots, M_p\)中选择单个最佳模型。

Clarification on 逐步选择 vs. 最优子集选择

前向逐步选择的一个关键缺点是:它不能保证找到包含\(p\)个预测变量的子集的所有\(2^p\)个可能模型中的最佳模型。

例如,假设在一个有\(p=3\)个预测变量的数据集中,最佳的单变量模型包含\(X_1\),而最佳的两变量模型包含\(X_2\)\(X_3\)。那么前向逐步选择将无法选择最佳的两变量模型,因为\(M_1\)将包含\(X_1\),所以\(M_2\)必须也包含\(X_1\)加上一个额外变量。

尽管如此,前向逐步选择在实践中通常表现良好,并且计算效率远高于最优子集选择(当\(p=20\)时,最优子集需要拟合约100万个模型,而前向选择只需要拟合211个模型)。

6.5 收缩方法 (Shrinkage Methods)

子集选择方法涉及使用最小二乘法拟合包含预测变量子集的线性模型。作为替代,我们可以使用一种约束或正则化(regularizes)系数估计的技术来拟合包含所有\(p\)个预测变量的模型,或者等价地,将系数估计收缩向零(shrink towards zero)。

收缩回归系数的两个最著名的技术是岭回归(ridge regression)和套索回归(the lasso)。

6.5.1 岭回归 (Ridge Regression)

回忆第3章,最小二乘拟合程序通过最小化以下式子来估计\(\beta_0, \beta_1, \ldots, \beta_p\):

\[ \text{RSS} = \sum_{i=1}^{n}\left(y_i - \beta_0 - \sum_{j=1}^{p}\beta_j x_{ij}\right)^2 \]

岭回归与最小二乘法非常相似,除了系数估计通过最小化一个略有不同的量来估计。具体来说,岭回归系数估计\(\hat{\beta}^R_\lambda\)是最小化以下式子的值:

\[ \sum_{i=1}^{n}\left(y_i - \beta_0 - \sum_{j=1}^{p}\beta_j x_{ij}\right)^2 + \lambda \sum_{j=1}^{p}\beta_j^2 = \text{RSS} + \lambda \sum_{j=1}^{p}\beta_j^2 \tag{6.5}\]

其中\(\lambda \geq 0\)是一个调优参数(tuning parameter),需要单独确定。

式 6.5 在两个不同的标准之间进行权衡:

  1. 拟合优度(通过最小化RSS实现)
  2. 收缩惩罚(shrinkage penalty):\(\lambda \sum_{j=1}^{p}\beta_j^2\)

\(\lambda = 0\)时,惩罚项没有影响,岭回归产生最小二乘估计。然而,当\(\lambda \to \infty\)时,收缩惩罚的影响增长,岭回归系数估计将接近零。

Tip: 为什么岭回归能改善预测?

岭回归的优势植根于偏差-方差权衡:

  1. 方差减小:当\(\lambda\)增加时,岭回归拟合的灵活度降低,导致方差减少但偏差增加。

  2. 偏差略有增加:对于适度的\(\lambda\)值,方差的急剧减少可能远大于偏差的小幅增加,从而导致整体MSE降低。

  3. 适用于高维数据:当\(p \approx n\)\(p > n\)时,最小二乘估计的方差极高,而岭回归可以通过以小的偏差增加为代价换取大的方差减小来获得更好的预测性能。

  4. 唯一解:当\(p > n\)时,最小二乘解不唯一,而岭回归仍然可以给出唯一解。

应用边界:高维财务回归。 岭回归的价值来自条件于候选特征和惩罚网格的偏差—方差权衡,而不是“变量越多越好”。所有尺度变换必须在当前训练折内估计;若先用全样本均值和方差标准化,验证期信息就会进入模型。系数路径可用于理解收缩,却不能替代同一外层前向折上的误差比较。

岭回归通常保留全部变量,因此适合许多弱信号共同出现或预测变量高度相关的场景;它不自动给出稀疏解释。所谓改善必须是相对训练均值基线和 OLS 的外层 RMSE 改善,并同时检查不同年份的稳定性。

6.5.2 套索回归 (The Lasso)

岭回归的一项解释性局限是:在通常的有限 \(\lambda\) 与非退化设计下,它倾向于保留全部 \(p\) 个预测变量,只把系数连续收缩向零。退化设计、特殊对称或数值阈值可能产生零或近似零,因此不应把“从不等于零”当作无条件定理。在 \(p\) 较大时,这种通常的非稀疏性会增加模型摘要和解释的难度。

套索回归(Least Absolute Shrinkage and Selection Operator, LASSO)是岭回归的一个相对较新的替代方案,克服了这个缺点。套索系数\(\hat{\beta}^L_\lambda\)最小化以下量:

\[ \sum_{i=1}^{n}\left(y_i - \beta_0 - \sum_{j=1}^{p}\beta_j x_{ij}\right)^2 + \lambda \sum_{j=1}^{p}|\beta_j| = \text{RSS} + \lambda \sum_{j=1}^{p}|\beta_j| \tag{6.6}\]

比较 式 6.6式 6.5,我们看到套索和岭回归具有相似的公式。唯一的区别是岭回归惩罚 式 6.5 中的 \(\beta_j^2\) 项被套索惩罚 式 6.6 中的 \(|\beta_j|\) 替换。

在统计学术语中,套索使用\(\ell_1\)惩罚(pronounced “ell 1”)而不是\(\ell_2\)惩罚。与岭回归一样,套索将系数估计收缩向零。然而,在套索的情况下,\(\ell_1\)惩罚的影响是当调优参数\(\lambda\)足够大时,强制一些系数估计精确等于零。因此,就像最优子集选择一样,套索执行变量选择(variable selection)。结果,套索生成的模型通常比岭回归生成的模型更容易解释。我们说套索产生稀疏模型(sparse models)——即仅涉及变量子集的模型。

岭回归与套索的可检验比较。 在同一标准化设计和同一内外层切分下,岭回归允许相关变量共同保留,套索则可能从相关变量中选择少数代表。套索产生零系数不等于被排除变量“没有经济作用”,岭系数较小也不等于变量不重要。应同时报告外层误差、非零系数数目和跨折选择频率;若不同年份的入选集合变化大,应把这种不稳定性作为结果,而不是挑选一次漂亮路径。

Tip: \(\ell_1\)\(\ell_2\)惩罚的几何解释

为了理解为什么套索产生稀疏解而岭回归不产生,考虑约束形式:

  • 岭回归:在约束\(\sum \beta_j^2 \leq s\)下最小化RSS。这个约束定义了一个球体(sphere),没有尖锐的角。

  • 套索:在约束\(\sum |\beta_j| \leq s\)下最小化RSS。这个约束定义了一个菱形(diamond)或更一般的多面体,具有尖锐的角。

当最小二乘解位于约束区域之外时:

  • 对于岭回归,椭圆(等 RSS 线)通常首先在球体的光滑边界上接触,因而在非退化设计下通常得到非零但被缩小的系数;这不排除特殊设计下的零系数。
  • 对于套索,椭圆通常首先在多面体的角上接触,导致一些系数精确地设置为零。

这就是为什么套索具有变量选择属性而岭回归没有的根本原因。

6.5.3 贝叶斯视角的正则化 (A Bayesian Perspective)

我们现在从概率的贝叶斯视角(Bayesian perspective)来看岭回归和套索。在这个部分,我们假设经典的线性模型带有多元正态的误差:

\[ Y = \beta_0 + X_1\beta_1 + \cdots + X_p\beta_p + \epsilon \]

其中 \(\epsilon\) 是服从均值为 0,方差为 \(\sigma^2\) 的独立同分布正态误差。 在贝叶斯统计中,我们不仅要建立似然函数 \(f(Y|X, \beta)\),还要对未知系数 \(\beta = (\beta_1, \ldots, \beta_p)^T\) 定一个先验分布 (Prior Distribution) \(p(\beta)\),来反映我们在看到数据前对 \(\beta\) 取值的信念。截距 \(\beta_0\) 通常假设为服从均匀分布(即无信息的平坦先验)。

根据贝叶斯定理,在观测到数据后,我们对系数 \(\beta\) 的信念由后验分布 (Posterior Distribution) 给出,它正比于先验与似然的乘积:

\[ p(\beta | X, Y) \propto f(Y | X, \beta) p(\beta|X) = f(Y | X, \beta) p(\beta) \]

我们要寻找最可能的 \(\beta\) 值(即最大化后验分布),这被称为极大后验估计 (MAP, Maximum A Posteriori)。

6.5.3.1 岭回归的贝叶斯等价:

假设 \(\beta_1, \ldots, \beta_p\) 相互独立,且都服从均值为 0、方差为 \(\tau^2\)正态分布 (Normal distribution)。 写出 MAP 优化的目标函数(取负对数后):

\[ \text{MAP}(\beta) = \arg\min_{\beta} \left\{ \frac{1}{2\sigma^2} \sum_{i=1}^n (y_i - \beta_0 - \sum_{j=1}^p \beta_j x_{ij})^2 + \frac{1}{2\tau^2} \sum_{j=1}^p \beta_j^2 \right\} \]

这在上式提取相应的常数影响下,精确等价于惩罚系数 \(\lambda = \sigma^2/\tau^2\) 的岭回归方程。因此,岭回归等价于给回归系数施加独立同分布的高斯正态先验分布的最大后验估计

6.5.3.2 套索(Lasso)的贝叶斯等价:

如果我们改变先验分布,假设 \(\beta_1, \ldots, \beta_p\) 相互独立,均值服从 0、尺度参数为 \(b\)拉普拉斯分布 (Laplace distribution),其密度函数为 \(p(\beta_j) = \frac{1}{2b} \exp(-|\beta_j|/b)\)。 套索的 MAP 估计则转变为:

\[ \text{MAP}(\beta) = \arg\min_{\beta} \left\{ \frac{1}{2\sigma^2} \sum_{i=1}^n (y_i - \beta_0 - \sum_{j=1}^p \beta_j x_{ij})^2 + \frac{1}{b} \sum_{j=1}^p |\beta_j| \right\} \]

这正好对应了惩罚系数 \(\lambda = 2\sigma^2/b\) 的套索方程。这里必须区分“先验概率”和“MAP 优化”:连续拉普拉斯先验没有位于零点的原子质量,因此 \(P(\beta_j=0)=0\),不能说先验以很大概率直接抽到零。精确零来自负对数先验 \(|\beta_j|/b\) 在零点的折角。

6.5.4 [拓展] 次梯度与 KKT 的局部桥梁

一元绝对值函数在零点不可导,但它在零点的次梯度是区间 \([-1,1]\)。凸优化的一阶条件因此从“梯度等于零”推广为“零属于次梯度集合”。不要求优化背景的读者可以先记住:当损失梯度落入惩罚能够抵消的区间时,零系数就是最优解;下面的公式给出严格表达。

令不含第 \(j\) 个惩罚项的平方损失梯度为 \(g_j\)。Lasso 的 KKT/次梯度条件是

\[ 0\in g_j+\lambda\,\partial|\beta_j|, \qquad \partial|\beta_j|= \begin{cases} \{1\},&\beta_j>0,\\ [-1,1],&\beta_j=0,\\ \{-1\},&\beta_j<0. \end{cases} \tag{6.7}\]

因此只要 \(|g_j|\leq\lambda\)\(\beta_j=0\) 就满足最优性条件;这段“可容纳梯度的区间”才是精确稀疏 MAP 的直接机制。若需要在先验层面赋予零点正概率,应使用 spike-and-slab 等含点质量的先验,而不是连续拉普拉斯先验。

6.6 选择调优参数 (Selecting the Tuning Parameter)

对于岭回归和套索,我们需要选择调优参数\(\lambda\)。实现这一点的常用方法是使用交叉验证

使用交叉验证选择\(\lambda\)的步骤:

  1. 选择一组\(\lambda\)值(通常在对数尺度上)。
  2. 对于每个\(\lambda\)值:
    1. 进行\(k\)折交叉验证。
    2. 计算交叉验证MSE。
  3. 选择使交叉验证MSE最小的\(\lambda\)值。
  4. 使用选定的\(\lambda\)在整个数据集上拟合最终模型。

选择惩罚强度时,候选网格、误差指标和时间切分都应在分析前固定。每个外层评分块内部重新建立前向内层折,并把缺失值填补、标准化、降维和模型拟合封装为一个流水线。内层误差最低只表示在当前网格与历史窗口中更合适;外层误差才用于比较候选方法,封存测试集不参与任何一次搜索。

对于时间序列或面板数据,随机 \(k\) 折会把未来观测混入训练集。本章的可执行案例以评分日期为单位形成扩张窗口,并要求训练标签窗在评分起点前结束。这样既避免同日公司的拆分,也避免未来响应穿越边界。

6.7 收缩方法阶段小结

本章介绍了三种改进线性模型的重要方法:

6.7.1 1. 子集选择

  • 最优子集选择:考虑所有可能的模型子集,计算成本高(需要拟合\(2^p\)个模型)。
  • 逐步选择:前向、后向和混合方法,计算效率高,但可能找不到全局最优解。
  • 模型选择标准:\(C_p\)、AIC、BIC、调整\(R^2\)和前向验证。

6.7.2 2. 收缩方法

  • 岭回归:
    • 使用\(\ell_2\)惩罚\(\lambda \sum \beta_j^2\)
    • 一般连续收缩系数而不产生精确稀疏解;结论受设计秩、惩罚极限和数值阈值约束
    • 适用于\(p \geq n\)的情况
    • 计算效率高
  • 套索回归:
    • 使用\(\ell_1\)惩罚\(\lambda \sum |\beta_j|\)
    • 同时进行收缩和变量选择,产生稀疏解
    • 在近似稀疏且可识别的信号结构下可能优于岭回归,最终排序须由同一外层验证决定
    • 计算效率也高

6.7.3 3. 方法选择指南

  • 预测准确性:使用交叉验证比较不同方法。
  • 模型可解释性:若任务需要稀疏预测摘要,可把套索或子集选择作为候选;相关特征下的入选集合仍须做稳定性核对。
  • 高维数据(\(p \approx n\)\(p > n\)):优先使用岭回归或套索而不是最小二乘法。
  • 稀疏性假设:若有可辩护的近似稀疏假设,纳入套索候选;若相关弱信号密集,也应纳入 Ridge/弹性网并以外层误差比较。

6.7.4 实际应用建议

  1. 标准化预测变量:在应用岭回归或套索之前,总是将预测变量标准化到相同尺度。
  2. 使用交叉验证:使用交叉验证选择调优参数\(\lambda\)和评估模型性能。
  3. 解释结果:注意套索选择的变量集可能对数据的随机波动敏感,考虑稳定性选择方法。
  4. 结合领域知识:统计方法应该与领域知识结合,不要完全依赖自动变量选择。

这些方法是现代统计学习的核心工具,在高维数据分析中有着广泛的应用,从基因组学到金融预测,从文本挖掘到图像识别。

核心学习证据来自练习 1—6:学生应能计算并辨析四项复杂度准则,在同一前向折上分别输出连续 RMSE 与有限概率的 Brier/AUC,并解释成分、稀疏性和结论边界。练习 7—11 属于拓展内容。

6.8 理论来源与前沿

线性模型选择与正则化把‘统计推断’与‘预测性能’统一在一个可计算框架下:AIC/BIC 源于信息论与极大似然的近似比较;\(C_p\) 与调整 \(R^2\) 等指标尝试在拟合与复杂度之间做惩罚;岭回归 (Hoerl 和 Kennard 1970年) 与套索回归 (Tibshirani 1996年) 则把复杂度惩罚显式写入优化目标,使得模型选择成为一类凸优化与路径追踪问题。

当前研究与实践的前沿重点包括:

  1. 选择之后的推断:变量被数据驱动地选择后,传统区间与显著性结论会偏乐观,需要 post-selection inference 思路。
  2. 稳定性与可重复性:用 stability selection 等方法评估变量选择对样本扰动的敏感性,减少结论波动。
  3. 结构化正则化:针对组结构、网络结构或时间结构引入 group lasso、fused lasso 等,以嵌入先验结构。

6.9 PCR、PLS 与前向评价

设当前训练折经中心化、标准化后的矩阵为 \(X_s\in\mathbb R^{n\times p}\),中心化响应为 \(y_c\)。所有中心和尺度都来自当前训练折;可选成分数满足

\[ 1\le M\le \min\{n-1,p,\operatorname{rank}(X_s)\}. \tag{6.8}\]

6.9.1 PCR:先解释 \(X\),再回归 \(y\)

\(X_s=UDV^\top\),取前 \(M\) 个右奇异向量 \(V_M\),得分矩阵和得分回归系数为

\[ Z_M=X_sV_M,\qquad \widehat\alpha_M=(Z_M^\top Z_M)^{-1}Z_M^\top y_c. \tag{6.9}\]

标准化坐标中的系数是 \(\widehat\beta_s=V_M\widehat\alpha_M\)。对新观测 \(x_*\),先用训练折的均值 \(\bar x\) 与尺度 \(s_x\) 得到 \(x_{*,s}\),再计算

\[ \widehat y_*=\bar y+x_{*,s}^\top V_M\widehat\alpha_M. \tag{6.10}\]

PCR 完全不使用 \(y\) 选择方向,因此高方差方向未必最能预测响应;低方差但强预测的方向可能被过早丢弃。主成分方向的符号可以整体翻转,载荷解释应以相对大小和成组结构为主。

6.9.2 PLS1:用响应监督成分,再逐步去膨胀

本章采用单响应 PLS1 的 NIPALS 约定。令 \(X^{(0)}=X_s\)\(y^{(0)}=y_c\)。第 \(m\) 步先计算 \(g_m={X^{(m-1)}}^\top y^{(m-1)}\)。只有当 \(\lVert g_m\rVert_2>\varepsilon\) 时才求单位长度权重;本章把 \(\varepsilon=10^{-12}\lVert X_s\rVert_F\lVert y_c\rVert_2\) 作为事前数值容差。若协方差范数不超过该值,或随后 \(t_m^\top t_m\le\varepsilon^2\),算法停止,有效成分数就是此前成功提取的成分数。于是非退化的第 \(m\) 步为

\[ w_m=\frac{g_m}{\|g_m\|_2}, \qquad t_m=X^{(m-1)}w_m, \tag{6.11}\]

再计算 \(p_m={X^{(m-1)}}^\top t_m/(t_m^\top t_m)\)\(c_m={y^{(m-1)}}^\top t_m/(t_m^\top t_m)\),并去膨胀

\[ X^{(m)}=X^{(m-1)}-t_mp_m^\top, \qquad y^{(m)}=y^{(m-1)}-c_mt_m. \tag{6.12}\]

\(W_M=(w_1,\ldots,w_M)\)\(P_M=(p_1,\ldots,p_M)\)\(c=(c_1,\ldots,c_M)^\top\)。当实际提取了 \(M\) 个成分且 \(P_M^\top W_M\) 满秩时,一种等价的标准化坐标预测写法为

\[ \widehat y_*=\bar y+x_{*,s}^\top W_M(P_M^\top W_M)^{-1}c. \tag{6.13}\]

不同软件可能采用不同归一化、符号和去膨胀约定,因此应声明实现;这些变体的成分符号同样不可识别。为比较不同折,本章保留原始载荷,并把每个成分中绝对值最大的载荷固定为正作为确定性符号锚点;跨折只比较对齐后的载荷或绝对大小,绝不把未对齐的原始正负号解释为方向变化。PLS 使用训练响应,可能保留 PCR 忽略的低方差预测方向,也更容易把训练噪声带入成分,故 \(M\) 必须在训练折内部选择。

流程 构造成分时使用响应 最终输出 评价损失 正确解释
PCR 回归 连续值 RMSE/MSE PCA 得分上的线性回归
PLS 回归 是,连续 \(y\) 连续值 RMSE/MSE 监督式成分回归
PCA-score logistic 二元概率 Brier/对数损失 PCA 得分上的逻辑回归
PLS-score logistic 是,二元训练标签 二元概率 Brier/对数损失 PLS 得分上的逻辑回归

连续响应路线直接比较 OLS、Ridge、Lasso、PCR 与 PLS 回归;二元事件路线比较 L1/L2 逻辑回归及两种得分逻辑分类器。PCR/PLS 的连续预测绝不裁剪后冒充分类概率。

本节在干净内核中调用与第 2 章等价的样本构造函数,得到 m02_shared_samplem02_features,而不是依赖前章运行留下的对象。共享键为 order_book_idprediction_datedrawdown_event_20d 表示 \(t\)\(t+20\) 路径中任一运行峰值到其后谷值的最大跌幅是否达到 10%,标签窗终点为 label_end_date

import os  # 读取跨平台数据根环境变量
from pathlib import Path  # 用路径对象安全拼接本地文件
import numpy as np  # 构造前向分折索引与有限概率数组
import pandas as pd  # 处理公司—交易日样本
from sklearn.base import BaseEstimator, TransformerMixin  # 定义折内PLS得分变换器
from sklearn.cross_decomposition import PLSRegression  # 提取监督式PLS得分
from sklearn.decomposition import PCA  # 提取无监督PCA得分
from sklearn.impute import SimpleImputer  # 在训练折内填补缺失值
from sklearn.linear_model import Lasso, LinearRegression, LogisticRegression, Ridge  # 拟合连续与二元候选模型
from sklearn.metrics import brier_score_loss, mean_squared_error, roc_auc_score  # 评价连续误差、概率质量与排序
from sklearn.model_selection import GridSearchCV  # 完成训练内前向选参
from sklearn.pipeline import Pipeline  # 串联折内预处理和分类器
from sklearn.preprocessing import StandardScaler  # 在训练折内标准化
展开共享回撤标签函数
def calculate_forward_max_drawdown(price_values, horizon_days=20):  # 计算含预测日在内的前向峰谷最大回撤
    price_array = np.asarray(price_values, dtype=float)  # 转为连续数值数组以构造滑动路径
    window_size = horizon_days + 1  # 路径包含t日与其后horizon_days个交易日
    drawdowns = np.full(price_array.size, np.nan, dtype=float)  # 末端窗口不完整时保持未知
    if price_array.size < window_size:  # 保护短序列而不虚构标签
        return drawdowns  # 返回全缺失结果表示没有完整路径
    price_windows = np.lib.stride_tricks.sliding_window_view(price_array, window_size)  # 构造每个预测日的完整价格路径
    running_peaks = np.maximum.accumulate(price_windows, axis=1)  # 逐路径记录每一时点之前的最高价
    drawdowns[:price_windows.shape[0]] = np.min(price_windows / running_peaks - 1, axis=1)  # 取峰值后最深谷值跌幅
    return drawdowns  # 返回与原价格序列等长的前向最大回撤
展开共享行情与标签准备函数
def prepare_m02_price_panel(book_data_dir):  # 从统一数据根构造未切分的公司—预测日样本
    data_root = Path(book_data_dir).expanduser().resolve()  # 同时接受环境变量字符串或Path对象
    assert data_root.is_dir(), f'BOOK_DATA_DIR 不存在: {data_root}'  # 在读取前验证数据挂载
    price_path = data_root / 'stock/stock_price_post_adjusted.h5'  # 指向后复权日行情
    assert price_path.is_file(), f'缺少后复权行情文件: {price_path}'  # 在read_hdf前给出明确缺失文件
    company_ids = ('600104.XSHG', '002415.XSHE', '600276.XSHG', '002230.XSHE')  # 固定四家长三角教学公司
    price_frames = [pd.read_hdf(price_path, where=f'order_book_id={company_id!r}', columns=['close', 'volume']).reset_index() for company_id in company_ids]  # 在存储层选择所需公司和字段
    price_panel = pd.concat(price_frames, ignore_index=True)  # 合并为公司—交易日面板
    price_panel['prediction_date'] = pd.to_datetime(price_panel['date'])  # 把交易日定义为收盘后预测原点
    price_panel = price_panel.loc[price_panel['prediction_date'].between('2012-01-01', '2024-12-31')].copy()  # 固定研究观察期
    price_panel = price_panel.sort_values(['order_book_id', 'prediction_date']).reset_index(drop=True)  # 保证窗口只在公司内按时间推进
    grouped_prices = price_panel.groupby('order_book_id', sort=False)  # 建立公司内窗口边界
    price_panel['return_1d'] = grouped_prices['close'].transform(lambda prices: prices.pct_change(fill_method=None))  # 计算截至预测日的一日收益
    price_panel['momentum_5d'] = grouped_prices['close'].transform(lambda prices: prices.pct_change(5, fill_method=None))  # 计算截至预测日的五日动量
    price_panel['volatility_20d'] = grouped_prices['return_1d'].transform(lambda returns: returns.rolling(20, min_periods=20).std())  # 计算过去二十日波动率
    volume_mean_20d = grouped_prices['volume'].transform(lambda volume: volume.rolling(20, min_periods=20).mean())  # 估计预测日前成交量常态
    price_panel['volume_ratio_20d'] = price_panel['volume'] / volume_mean_20d  # 表示当日成交量相对过去均值的活跃度
    feature_names = ['return_1d', 'momentum_5d', 'volatility_20d', 'volume_ratio_20d']  # 固定跨章特征名称与顺序
    price_panel['max_drawdown_20d'] = grouped_prices['close'].transform(lambda prices: calculate_forward_max_drawdown(prices, horizon_days=20))  # 计算t至t+20真实峰谷最大回撤
    price_panel['label_end_date'] = grouped_prices['prediction_date'].shift(-20)  # 记录未来第二十个公司交易日
    price_panel['drawdown_event_20d'] = price_panel['max_drawdown_20d'].le(-0.10).where(price_panel['max_drawdown_20d'].notna()).astype('Int64')  # 完整路径才生成二元事件
    price_panel = price_panel.replace([np.inf, -np.inf], np.nan).dropna(subset=feature_names + ['drawdown_event_20d', 'label_end_date']).copy()  # 排除无效特征和未完成路径
    return price_panel, feature_names  # 把未切分样本交给公共构造函数
展开共享样本切分函数
def build_m02_shared_sample(book_data_dir):  # 在干净内核返回第2至第9章共享对象
    price_panel, feature_names = prepare_m02_price_panel(book_data_dir)  # 从统一数据根重建真实峰谷标签
    validation_start = pd.Timestamp('2020-01-01')  # 固定验证段起点
    test_start = pd.Timestamp('2022-01-01')  # 固定封存测试段起点
    study_end = pd.Timestamp('2024-12-31')  # 固定研究终点
    train_mask = (price_panel['prediction_date'] < validation_start) & (price_panel['label_end_date'] < validation_start)  # 清除伸入验证期的训练标签
    validation_mask = price_panel['prediction_date'].between(validation_start, test_start, inclusive='left') & (price_panel['label_end_date'] < test_start)  # 清除伸入测试期的验证标签
    test_mask = price_panel['prediction_date'].between(test_start, study_end, inclusive='both')  # 标记只供第九章最终评价的日期
    price_panel['split'] = np.select([train_mask, validation_mask, test_mask], ['train', 'validation', 'test'], default='unused')  # 赋予互斥时间段
    shared_columns = ['order_book_id', 'prediction_date', 'label_end_date', *feature_names, 'max_drawdown_20d', 'drawdown_event_20d', 'split']  # 固定跨章字段顺序
    shared_sample = price_panel.loc[price_panel['split'] != 'unused', shared_columns].copy()  # 排除边界隔离行并形成共享对象
    assert not shared_sample.duplicated(['order_book_id', 'prediction_date']).any()  # 验证公司—预测日键唯一
    assert set(shared_sample['drawdown_event_20d'].unique()) == {0, 1}  # 验证二元标签包含两类
    assert shared_sample.loc[shared_sample['split'] == 'train', 'label_end_date'].max() < validation_start  # 验证训练标签不触及验证期
    assert shared_sample.loc[shared_sample['split'] == 'validation', 'label_end_date'].max() < test_start  # 验证验证标签不触及测试期
    training_event_rate = float(shared_sample.loc[shared_sample['split'] == 'train', 'drawdown_event_20d'].mean())  # 只用训练标签估计概率基线
    test_keys = shared_sample.loc[shared_sample['split'] == 'test', ['order_book_id', 'prediction_date']].copy()  # 固定第九章最终比较键
    return shared_sample, feature_names, training_event_rate, test_keys  # 返回约定的四项跨章交付
BOOK_DATA_DIR = Path('/home/ubuntu/r2_data_mount/data').resolve()  # 明文构造在线教材的绝对数据根
os.environ['BOOK_DATA_DIR'] = str(BOOK_DATA_DIR)  # 为本章模型选择流程登记统一数据根
m02_shared_sample, m02_features, m02_training_event_rate, m02_test_keys = build_m02_shared_sample(BOOK_DATA_DIR)  # 在本章干净内核重建共享对象
required_m02_columns = {'order_book_id', 'prediction_date', 'label_end_date', 'max_drawdown_20d', 'drawdown_event_20d', 'split', *m02_features}  # 声明共享接口
assert required_m02_columns.issubset(m02_shared_sample.columns)  # 核对本章重建的第2章交付字段
m02_shared_sample = m02_shared_sample.copy()  # 避免修改第2章交付对象
m02_shared_sample['prediction_date'] = pd.to_datetime(m02_shared_sample['prediction_date'])  # 统一预测日期类型
m02_shared_sample['label_end_date'] = pd.to_datetime(m02_shared_sample['label_end_date'])  # 统一标签终点类型
m02_shared_sample = m02_shared_sample.sort_values(['prediction_date', 'order_book_id']).reset_index(drop=True)  # 固定时间与公司顺序
assert not m02_shared_sample.duplicated(['order_book_id', 'prediction_date']).any()  # 核对公司—预测日唯一键
assert m02_shared_sample['drawdown_event_20d'].isin([0, 1]).all()  # 核对二元目标编码
m06_development_sample = m02_shared_sample.loc[m02_shared_sample['split'].isin(['train', 'validation'])].copy()  # 只保留开发期供本章评价
m06_development_keys = m06_development_sample[['order_book_id', 'prediction_date']].copy()  # 提取本章实际可用键
m06_test_overlap = m06_development_keys.merge(m02_test_keys, on=['order_book_id', 'prediction_date'], how='inner')  # 与封存测试键求交集
assert m06_test_overlap.empty  # 强制本章任何折叠与搜索都不接触测试键
assert not m06_development_sample['split'].eq('test').any()  # 再核对开发样本不含测试行

m06_development_sample 是本章唯一的折叠输入;封存测试段只以键表参与空交集断言,不读取其标签或用于搜索。

连续路线用同一开发样本预测未来 20 日最大回撤。训练均值是必须击败的基线;每个模型的填补、标准化、成分数和惩罚参数都在内层前向折中确定。

def make_regression_forward_splits(frame, split_count=3):  # 为连续响应构造训练内前向折
    unique_dates = np.array(sorted(frame['prediction_date'].unique()))  # 提取不重复评分日期
    date_blocks = np.array_split(unique_dates, split_count + 1)  # 首块只用于初始训练
    forward_splits = []  # 保存拟合与评分行索引
    for score_dates in date_blocks[1:]:  # 依次将后续日期块作为评分期
        score_start = pd.Timestamp(score_dates.min())  # 获取当前评分块起点
        fit_index = np.flatnonzero(frame['label_end_date'].lt(score_start))  # 只纳入标签已可得的历史行
        score_index = np.flatnonzero(frame['prediction_date'].isin(score_dates))  # 保持同日公司在同一块
        if len(fit_index) and len(score_index):  # 只登记非空前向折
            forward_splits.append((fit_index, score_index))  # 保存当前切分
    assert len(forward_splits) >= 2  # 要求至少两个内层评分块
    return forward_splits  # 返回可供网格搜索使用的索引
regression_steps = [('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler())]  # 固定折内预处理
regression_candidates = {  # 登记连续响应候选及其内层网格
    'OLS': (Pipeline(regression_steps + [('model', LinearRegression())]), {}),  # 建立未惩罚基准
    'Ridge': (Pipeline(regression_steps + [('model', Ridge())]), {'model__alpha': [0.01, 0.1, 1, 10]}),  # 搜索L2强度
    'Lasso': (Pipeline(regression_steps + [('model', Lasso(max_iter=10000))]), {'model__alpha': [0.0001, 0.001, 0.01]}),  # 搜索L1强度
    'PCR': (Pipeline(regression_steps + [('score', PCA()), ('model', LinearRegression())]), {'score__n_components': [1, 2, 3]}),  # 搜索无监督成分数
    'PLS': (Pipeline(regression_steps + [('model', PLSRegression(scale=False))]), {'model__n_components': [1, 2, 3]}),  # 搜索监督成分数
}  # 完成候选登记
def make_component_direction_rows(direction_matrix, model_name):  # 把全部成分转成长表并统一符号
    direction_rows = []  # 保存每个成分相对于全部特征的载荷
    for component_index, raw_direction in enumerate(np.asarray(direction_matrix), start=1):  # 逐成分完成确定性符号对齐
        anchor_index = int(np.argmax(np.abs(raw_direction)))  # 选择绝对值最大载荷作锚点
        assert abs(raw_direction[anchor_index]) > 1e-12  # 拒绝无法归一化或解释的零方向
        sign_multiplier = 1.0 if raw_direction[anchor_index] > 0 else -1.0  # 把锚点方向固定为正
        for feature_name, raw_loading in zip(m02_features, raw_direction):  # 保存相对于全部特征的载荷
            direction_rows.append({'model': model_name, 'component': component_index, 'feature': feature_name, 'raw_loading': float(raw_loading), 'anchor_feature': m02_features[anchor_index], 'sign_multiplier': sign_multiplier, 'aligned_loading': float(raw_loading * sign_multiplier)})  # 登记原始与对齐方向
    return direction_rows  # 返回可恢复全部成分方向的长表记录
def summarize_search_complexity(search, model_name):  # 从已拟合搜索中提取规范复杂度证据
    best_parameters = search.best_params_  # 保存当前外层折所选参数
    component_count = best_parameters.get('score__n_components', best_parameters.get('model__n_components', np.nan))  # 提取成分数
    fitted_model = search.best_estimator_.named_steps['model']  # 取得流水线末端估计器
    sparse_names = {'Lasso', 'L1 logistic'}  # 限定需要报告稀疏度的候选
    nonzero_count = int(np.count_nonzero(np.asarray(fitted_model.coef_))) if model_name in sparse_names else np.nan  # 统计非零系数
    direction_rows = []  # 默认没有成分方向长表
    if model_name in {'PCR', 'PCA-score logistic'}:  # 读取无监督第一方向
        direction_matrix = search.best_estimator_.named_steps['score'].components_  # 提取全部已选PCA载荷
    elif model_name == 'PLS':  # 读取连续PLS第一方向
        direction_matrix = fitted_model.x_weights_.T  # 把全部已选PLS权重转为成分乘特征
    elif model_name == 'PLS-score logistic':  # 读取分类得分变换器内部第一方向
        direction_matrix = search.best_estimator_.named_steps['score'].pls_.x_weights_.T  # 提取分类得分变换器的全部PLS权重
    else:  # 其余模型没有成分方向
        direction_matrix = None  # 保持无方向状态
    if direction_matrix is not None:  # 为每个已选成分保留完整特征方向
        direction_rows = make_component_direction_rows(direction_matrix, model_name)  # 使用统一锚点规则构造长表
    direction_summary = f'complete_rows={len(direction_rows)}; sign=anchor-positive' if direction_rows else ''  # 主表只指向完整证据
    return str(best_parameters), component_count, nonzero_count, direction_summary, direction_rows  # 返回参数、复杂度与完整方向

continuous_records = []  # 保存逐年连续预测评价
continuous_selection_records = []  # 保存Lasso逐折特征选择证据
continuous_direction_records = []  # 保存PCR/PLS逐折逐成分完整载荷
continuous_years = sorted(m06_development_sample.loc[m06_development_sample['split'] == 'validation', 'prediction_date'].dt.year.unique())  # 固定外层年份
for scoring_year in continuous_years:  # 逐年形成扩张窗口评分
    score_rows = m06_development_sample['prediction_date'].dt.year.eq(scoring_year)  # 锁定当前评分块
    score_start = m06_development_sample.loc[score_rows, 'prediction_date'].min()  # 获取评分起点
    fit_rows = m06_development_sample['label_end_date'].lt(score_start)  # 排除标签窗尚未结束的行
    fit_sample = m06_development_sample.loc[fit_rows].reset_index(drop=True)  # 建立历史拟合样本
    score_sample = m06_development_sample.loc[score_rows].reset_index(drop=True)  # 建立当年评分样本
    inner_cv = make_regression_forward_splits(fit_sample, split_count=3)  # 仅在历史样本内选参
    response_mean = float(fit_sample['max_drawdown_20d'].mean())  # 用历史响应均值建立基线
    baseline_prediction = np.repeat(response_mean, len(score_sample))  # 形成当年基线预测
    baseline_rmse = mean_squared_error(score_sample['max_drawdown_20d'], baseline_prediction) ** 0.5  # 计算基线RMSE
    label_boundary = fit_sample['label_end_date'].max().date().isoformat()  # 保存拟合标签最晚可得日
    continuous_records.append({'fold': f'year-{scoring_year}', 'year': scoring_year, 'model': 'Training-mean baseline', 'fit_n': len(fit_sample), 'score_n': len(score_sample), 'label_boundary': label_boundary, 'rmse': baseline_rmse, 'best_params': '{}', 'components': np.nan, 'nonzero': np.nan, 'complexity': 'mean baseline'})  # 保存基线结果
    for model_name, (pipeline, parameter_grid) in regression_candidates.items():  # 逐一评价连续模型
        search = GridSearchCV(pipeline, parameter_grid, cv=inner_cv, scoring='neg_mean_squared_error')  # 用内层MSE选择参数
        search.fit(fit_sample[m02_features], fit_sample['max_drawdown_20d'])  # 只在历史样本拟合全部步骤
        prediction = np.asarray(search.predict(score_sample[m02_features])).reshape(-1)  # 生成连续回撤预测
        outer_rmse = mean_squared_error(score_sample['max_drawdown_20d'], prediction) ** 0.5  # 计算外层RMSE
        best_params, components, nonzero, direction, direction_rows = summarize_search_complexity(search, model_name)  # 提取参数与完整方向
        complexity = f'components={components}; nonzero={nonzero}'  # 形成统一复杂度摘要
        continuous_records.append({'fold': f'year-{scoring_year}', 'year': scoring_year, 'model': model_name, 'fit_n': len(fit_sample), 'score_n': len(score_sample), 'label_boundary': label_boundary, 'rmse': outer_rmse, 'best_params': best_params, 'components': components, 'nonzero': nonzero, 'direction': direction, 'complexity': complexity})  # 保存当年结果
        continuous_direction_records.extend([{'route': 'continuous', 'fold': f'year-{scoring_year}', **row} for row in direction_rows])  # 为每条载荷补入路线与外层折
        if model_name == 'Lasso':  # 只为稀疏连续模型登记逐特征入选状态
            fitted_coefficients = np.asarray(search.best_estimator_.named_steps['model'].coef_).reshape(-1)  # 读取Lasso系数
            continuous_selection_records.extend([{'year': scoring_year, 'feature': feature_name, 'selected': abs(coefficient) > 1e-12} for feature_name, coefficient in zip(m02_features, fitted_coefficients)])  # 保存逐折选择
表 6.2: 连续前向最大回撤的逐年外层 RMSE:基线、OLS、收缩与降维回归
continuous_result_table = pd.DataFrame(continuous_records)  # 整理逐年连续评价记录
print(continuous_result_table.to_string(index=False))  # 展示逐年误差、样本边界、参数与复杂度
print(continuous_result_table.groupby('model')['rmse'].agg(['mean', 'std']).to_string())  # 汇总跨年稳定性
continuous_selection_frequency = pd.DataFrame(continuous_selection_records).groupby('feature')['selected'].mean()  # 计算Lasso跨折选择频率
print(continuous_selection_frequency.to_string())  # 输出逐特征选择稳定性
     fold  year                  model  fit_n  score_n label_boundary     rmse                best_params  components  nonzero                  complexity                              direction
year-2020  2020 Training-mean baseline   7572      972     2019-12-31 0.048898                         {}         NaN      NaN               mean baseline                                    NaN
year-2020  2020                    OLS   7572      972     2019-12-31 0.046541                         {}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020                  Ridge   7572      972     2019-12-31 0.046541     {'model__alpha': 0.01}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020                  Lasso   7572      972     2019-12-31 0.046528   {'model__alpha': 0.0001}         NaN      4.0   components=nan; nonzero=4                                       
year-2020  2020                    PCR   7572      972     2019-12-31 0.046522 {'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2020  2020                    PLS   7572      972     2019-12-31 0.046565 {'model__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
year-2021  2021 Training-mean baseline   8464      892     2020-12-31 0.046066                         {}         NaN      NaN               mean baseline                                    NaN
year-2021  2021                    OLS   8464      892     2020-12-31 0.041455                         {}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021                  Ridge   8464      892     2020-12-31 0.041455     {'model__alpha': 0.01}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021                  Lasso   8464      892     2020-12-31 0.041451   {'model__alpha': 0.0001}         NaN      4.0   components=nan; nonzero=4                                       
year-2021  2021                    PCR   8464      892     2020-12-31 0.041458 {'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2021  2021                    PLS   8464      892     2020-12-31 0.041464 {'model__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
                            mean       std
model                                     
Lasso                   0.043989  0.003590
OLS                     0.043998  0.003597
PCR                     0.043990  0.003581
PLS                     0.044014  0.003607
Ridge                   0.043998  0.003597
Training-mean baseline  0.047482  0.002003
feature
momentum_5d         1.0
return_1d           1.0
volatility_20d      1.0
volume_ratio_20d    1.0

表中较小的 RMSE 只描述 2020—2021 年外层评分块。若某方法没有稳定优于训练均值或 OLS,应报告“未观察到增量预测证据”;不得读取 2022—2024 年标签后再调整候选网格。

下面的轻量包装器只返回 PLS 的 X 得分,避免把 PLSRegression 的连续预测误当成概率。

class PLSScoreTransformer(BaseEstimator, TransformerMixin):  # 定义监督式降维变换器
    def __init__(self, n_components=2):  # 暴露可由训练折选择的成分数
        self.n_components = n_components  # 保存成分数参数
    def fit(self, features, target):  # 只在当前训练折估计PLS方向
        self.pls_ = PLSRegression(n_components=self.n_components, scale=False)  # 建立PLS模型
        self.pls_.fit(features, target)  # 用训练折特征和二元标签拟合
        return self  # 返回已拟合变换器以兼容Pipeline
    def transform(self, features):  # 把新样本投影到已拟合PLS方向
        return self.pls_.transform(features)  # 仅返回X得分供逻辑回归使用

common_steps = [('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler())]  # 共用折内预处理
logistic_model = LogisticRegression(solver='liblinear', max_iter=5000)  # 定义稳定的小样本分类器
classification_candidates = {  # 登记四个真正输出概率的候选流程
    'L2 logistic': (Pipeline(common_steps + [('model', logistic_model)]), {'model__penalty': ['l2'], 'model__C': [0.1, 1, 10]}),  # L2收缩
    'L1 logistic': (Pipeline(common_steps + [('model', logistic_model)]), {'model__penalty': ['l1'], 'model__C': [0.1, 1, 10]}),  # L1收缩
    'PCA-score logistic': (Pipeline(common_steps + [('score', PCA()), ('model', logistic_model)]), {'score__n_components': [1, 2, 3], 'model__penalty': ['l2'], 'model__C': [0.1, 1, 10]}),  # PCA后分类
    'PLS-score logistic': (Pipeline(common_steps + [('score', PLSScoreTransformer()), ('model', logistic_model)]), {'score__n_components': [1, 2, 3], 'model__penalty': ['l2'], 'model__C': [0.1, 1, 10]}),  # PLS得分后分类
}  # 完成候选登记

2020 与 2021 两个验证年份依次作为外层评分块;每个外层拟合样本内部再用前向切分选择惩罚强度或成分数。Brier 的输入始终来自 predict_proba(),不是回归预测或硬标签。

def make_panel_forward_splits(frame, split_count=3):  # 按日期而非面板行号构造内层折
    assert frame['split'].isin(['train', 'validation']).all()  # 禁止封存测试行进入任何折叠或搜索
    unique_dates = np.array(sorted(frame['prediction_date'].unique()))  # 提取不重复预测日期
    date_blocks = np.array_split(unique_dates, split_count + 1)  # 首块只用于初始训练
    forward_splits = []  # 保存GridSearchCV需要的行索引
    for score_dates in date_blocks[1:]:  # 依次把后续日期块作为评分期
        score_start = pd.Timestamp(score_dates.min())  # 获取当前评分块起点
        fit_index = np.flatnonzero(frame['label_end_date'].lt(score_start))  # 只纳入标签窗已结束行
        score_index = np.flatnonzero(frame['prediction_date'].isin(score_dates))  # 同一日期全部进入同一评分块
        fit_has_two_classes = frame.iloc[fit_index]['drawdown_event_20d'].nunique() == 2  # 逻辑模型要求两类
        if len(fit_index) and len(score_index) and fit_has_two_classes:  # 只保留可估计折
            forward_splits.append((fit_index, score_index))  # 登记当前前向折
    assert len(forward_splits) >= 2  # 至少需要两个有效内层折
    return forward_splits  # 返回确定的面板日期切分
scoring_years = sorted(m06_development_sample.loc[m06_development_sample['split'] == 'validation', 'prediction_date'].dt.year.unique())  # 只取预先声明的验证年份
assert scoring_years == [2020, 2021]  # 核对外层评分不会滑入封存测试期
probability_records = []  # 保存逐折概率评价
probability_selection_records = []  # 保存L1逻辑回归逐折特征选择证据
probability_direction_records = []  # 保存PCA/PLS得分逻辑回归的完整载荷
for scoring_year in scoring_years:  # 逐年形成扩张窗口评分
    score_rows = m06_development_sample['prediction_date'].dt.year.eq(scoring_year)  # 锁定当前验证评分块
    score_start = m06_development_sample.loc[score_rows, 'prediction_date'].min()  # 获取评分起点
    fit_rows = m06_development_sample['label_end_date'].lt(score_start)  # 只使用标签窗已结束的开发期历史行
    fit_sample = m06_development_sample.loc[fit_rows].reset_index(drop=True)  # 建立当前拟合样本
    score_sample = m06_development_sample.loc[score_rows].reset_index(drop=True)  # 建立当前验证评分样本
    assert fit_sample['label_end_date'].max() < score_sample['prediction_date'].min()  # 核对标签时间边界
    assert score_sample['split'].eq('validation').all()  # 核对外层评分只发生在验证段
    base_probability = float(fit_sample['drawdown_event_20d'].mean())  # 只用拟合期事件率建立基线
    base_vector = np.repeat(base_probability, len(score_sample))  # 形成评分块基线概率
    label_boundary = fit_sample['label_end_date'].max().date().isoformat()  # 保存拟合标签最晚可得日
    probability_records.append({'fold': f'year-{scoring_year}', 'year': scoring_year, 'model': 'Training-rate baseline', 'fit_n': len(fit_sample), 'score_n': len(score_sample), 'label_boundary': label_boundary, 'brier': brier_score_loss(score_sample['drawdown_event_20d'], base_vector), 'auc': 0.5, 'best_params': '{}', 'components': np.nan, 'nonzero': np.nan, 'complexity': 'event-rate baseline'})  # 记录基线
    inner_cv = make_panel_forward_splits(fit_sample, split_count=3)  # 训练内部按日期与标签终点选参
    for model_name, (pipeline, parameter_grid) in classification_candidates.items():  # 评价每个概率分类器
        search = GridSearchCV(pipeline, parameter_grid, cv=inner_cv, scoring='neg_brier_score')  # 以内层Brier选参
        search.fit(fit_sample[m02_features], fit_sample['drawdown_event_20d'])  # 只在拟合期学习全部变换
        score_probability = search.predict_proba(score_sample[m02_features])[:, 1]  # 输出回撤事件概率
        score_auc = roc_auc_score(score_sample['drawdown_event_20d'], score_probability) if score_sample['drawdown_event_20d'].nunique() == 2 else np.nan  # 单类时保留缺失
        best_params, components, nonzero, direction, direction_rows = summarize_search_complexity(search, model_name)  # 提取参数与完整方向
        complexity = f'components={components}; nonzero={nonzero}'  # 形成统一复杂度摘要
        probability_records.append({'fold': f'year-{scoring_year}', 'year': scoring_year, 'model': model_name, 'fit_n': len(fit_sample), 'score_n': len(score_sample), 'label_boundary': label_boundary, 'brier': brier_score_loss(score_sample['drawdown_event_20d'], score_probability), 'auc': score_auc, 'best_params': best_params, 'components': components, 'nonzero': nonzero, 'direction': direction, 'complexity': complexity})  # 保存评价
        probability_direction_records.extend([{'route': 'probability', 'fold': f'year-{scoring_year}', **row} for row in direction_rows])  # 为每条载荷补入路线与外层折
        if model_name == 'L1 logistic':  # 只为稀疏概率模型登记逐特征入选状态
            fitted_coefficients = np.asarray(search.best_estimator_.named_steps['model'].coef_).reshape(-1)  # 读取L1系数
            probability_selection_records.extend([{'year': scoring_year, 'feature': feature_name, 'selected': abs(coefficient) > 1e-12} for feature_name, coefficient in zip(m02_features, fitted_coefficients)])  # 保存逐折选择
表 6.3: 第2章共享样本上的前向概率评价:基线、L1/L2与得分逻辑分类器
probability_result_table = pd.DataFrame(probability_records)  # 整理逐年评价记录
print(probability_result_table.to_string(index=False))  # 展示逐年误差、样本边界、参数与复杂度
print(probability_result_table.groupby('model')[['brier', 'auc']].agg(['mean', 'std']).to_string())  # 汇总跨年稳定性
probability_selection_frequency = pd.DataFrame(probability_selection_records).groupby('feature')['selected'].mean()  # 计算L1跨折选择频率
print(probability_selection_frequency.to_string())  # 输出逐特征选择稳定性
     fold  year                  model  fit_n  score_n label_boundary    brier      auc                                                         best_params  components  nonzero                  complexity                              direction
year-2020  2020 Training-rate baseline   7572      972     2019-12-31 0.217640 0.500000                                                                  {}         NaN      NaN         event-rate baseline                                    NaN
year-2020  2020            L2 logistic   7572      972     2019-12-31 0.205603 0.660394                           {'model__C': 0.1, 'model__penalty': 'l2'}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020            L1 logistic   7572      972     2019-12-31 0.205767 0.660282                             {'model__C': 1, 'model__penalty': 'l1'}         NaN      4.0   components=nan; nonzero=4                                       
year-2020  2020     PCA-score logistic   7572      972     2019-12-31 0.205713 0.659794 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2020  2020     PLS-score logistic   7572      972     2019-12-31 0.205937 0.658452 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
year-2021  2021 Training-rate baseline   8464      892     2020-12-31 0.240808 0.500000                                                                  {}         NaN      NaN         event-rate baseline                                    NaN
year-2021  2021            L2 logistic   8464      892     2020-12-31 0.208340 0.718515                           {'model__C': 0.1, 'model__penalty': 'l2'}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021            L1 logistic   8464      892     2020-12-31 0.208330 0.718713                           {'model__C': 0.1, 'model__penalty': 'l1'}         NaN      3.0   components=nan; nonzero=3                                       
year-2021  2021     PCA-score logistic   8464      892     2020-12-31 0.208375 0.719223 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2021  2021     PLS-score logistic   8464      892     2020-12-31 0.208342 0.718541 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
                           brier                 auc          
                            mean       std      mean       std
model                                                         
L1 logistic             0.207048  0.001812  0.689498  0.041317
L2 logistic             0.206971  0.001936  0.689454  0.041097
PCA-score logistic      0.207044  0.001882  0.689508  0.042023
PLS-score logistic      0.207140  0.001701  0.688496  0.042490
Training-rate baseline  0.229224  0.016383  0.500000  0.000000
feature
momentum_5d         1.0
return_1d           0.5
volatility_20d      1.0
volume_ratio_20d    1.0
表 6.4: PCR、PLS 与得分分类器的逐折逐成分完整载荷及符号对齐证据
component_direction_table = pd.DataFrame(continuous_direction_records + probability_direction_records)  # 合并两条路线的完整方向
direction_groups = component_direction_table.groupby(['route', 'fold', 'model', 'component'], sort=False)  # 以单个已选成分为核对单位
assert direction_groups.size().eq(len(m02_features)).all()  # 每个成分必须覆盖全部共享特征
assert np.allclose(component_direction_table['aligned_loading'], component_direction_table['raw_loading'] * component_direction_table['sign_multiplier'])  # 只允许整成分乘正负一
anchor_rows = component_direction_table.loc[component_direction_table['feature'].eq(component_direction_table['anchor_feature'])]  # 定位每个成分的锚点行
assert anchor_rows['aligned_loading'].gt(0).all() and component_direction_table[['raw_loading', 'aligned_loading']].map(np.isfinite).all().all()  # 锚点为正且全部载荷有限
print(component_direction_table.to_string(index=False))  # 默认展示可恢复完整方向的长表
      route      fold              model  component          feature  raw_loading   anchor_feature  sign_multiplier  aligned_loading
 continuous year-2020                PCR          1        return_1d     0.638558      momentum_5d              1.0         0.638558
 continuous year-2020                PCR          1      momentum_5d     0.653910      momentum_5d              1.0         0.653910
 continuous year-2020                PCR          1   volatility_20d    -0.001949      momentum_5d              1.0        -0.001949
 continuous year-2020                PCR          1 volume_ratio_20d     0.405760      momentum_5d              1.0         0.405760
 continuous year-2020                PCR          2        return_1d     0.096984   volatility_20d              1.0         0.096984
 continuous year-2020                PCR          2      momentum_5d     0.073026   volatility_20d              1.0         0.073026
 continuous year-2020                PCR          2   volatility_20d     0.956376   volatility_20d              1.0         0.956376
 continuous year-2020                PCR          2 volume_ratio_20d    -0.265720   volatility_20d              1.0        -0.265720
 continuous year-2020                PCR          3        return_1d    -0.323774 volume_ratio_20d              1.0        -0.323774
 continuous year-2020                PCR          3      momentum_5d    -0.223820 volume_ratio_20d              1.0        -0.223820
 continuous year-2020                PCR          3   volatility_20d     0.292100 volume_ratio_20d              1.0         0.292100
 continuous year-2020                PCR          3 volume_ratio_20d     0.871638 volume_ratio_20d              1.0         0.871638
 continuous year-2020                PLS          1        return_1d     0.002081   volatility_20d              1.0         0.002081
 continuous year-2020                PLS          1      momentum_5d     0.003956   volatility_20d              1.0         0.003956
 continuous year-2020                PLS          1   volatility_20d     0.997986   volatility_20d              1.0         0.997986
 continuous year-2020                PLS          1 volume_ratio_20d     0.063269   volatility_20d              1.0         0.063269
 continuous year-2020                PLS          2        return_1d    -0.484778 volume_ratio_20d              1.0        -0.484778
 continuous year-2020                PLS          2      momentum_5d    -0.499144 volume_ratio_20d              1.0        -0.499144
 continuous year-2020                PLS          2   volatility_20d    -0.042464 volume_ratio_20d              1.0        -0.042464
 continuous year-2020                PLS          2 volume_ratio_20d     0.716967 volume_ratio_20d              1.0         0.716967
 continuous year-2021                PCR          1        return_1d     0.633764      momentum_5d              1.0         0.633764
 continuous year-2021                PCR          1      momentum_5d     0.649938      momentum_5d              1.0         0.649938
 continuous year-2021                PCR          1   volatility_20d    -0.009911      momentum_5d              1.0        -0.009911
 continuous year-2021                PCR          1 volume_ratio_20d     0.419315      momentum_5d              1.0         0.419315
 continuous year-2021                PCR          2        return_1d     0.095446   volatility_20d              1.0         0.095446
 continuous year-2021                PCR          2      momentum_5d     0.070301   volatility_20d              1.0         0.070301
 continuous year-2021                PCR          2   volatility_20d     0.965849   volatility_20d              1.0         0.965849
 continuous year-2021                PCR          2 volume_ratio_20d    -0.230398   volatility_20d              1.0        -0.230398
 continuous year-2021                PCR          3        return_1d    -0.337000 volume_ratio_20d              1.0        -0.337000
 continuous year-2021                PCR          3      momentum_5d    -0.231945 volume_ratio_20d              1.0        -0.231945
 continuous year-2021                PCR          3   volatility_20d     0.258908 volume_ratio_20d              1.0         0.258908
 continuous year-2021                PCR          3 volume_ratio_20d     0.874985 volume_ratio_20d              1.0         0.874985
 continuous year-2021                PLS          1        return_1d     0.013122   volatility_20d              1.0         0.013122
 continuous year-2021                PLS          1      momentum_5d     0.034323   volatility_20d              1.0         0.034323
 continuous year-2021                PLS          1   volatility_20d     0.995704   volatility_20d              1.0         0.995704
 continuous year-2021                PLS          1 volume_ratio_20d     0.084994   volatility_20d              1.0         0.084994
 continuous year-2021                PLS          2        return_1d     0.707239        return_1d              1.0         0.707239
 continuous year-2021                PLS          2      momentum_5d     0.543283        return_1d              1.0         0.543283
 continuous year-2021                PLS          2   volatility_20d     0.010558        return_1d              1.0         0.010558
 continuous year-2021                PLS          2 volume_ratio_20d    -0.452267        return_1d              1.0        -0.452267
probability year-2020 PCA-score logistic          1        return_1d     0.638558      momentum_5d              1.0         0.638558
probability year-2020 PCA-score logistic          1      momentum_5d     0.653910      momentum_5d              1.0         0.653910
probability year-2020 PCA-score logistic          1   volatility_20d    -0.001949      momentum_5d              1.0        -0.001949
probability year-2020 PCA-score logistic          1 volume_ratio_20d     0.405760      momentum_5d              1.0         0.405760
probability year-2020 PCA-score logistic          2        return_1d     0.096984   volatility_20d              1.0         0.096984
probability year-2020 PCA-score logistic          2      momentum_5d     0.073026   volatility_20d              1.0         0.073026
probability year-2020 PCA-score logistic          2   volatility_20d     0.956376   volatility_20d              1.0         0.956376
probability year-2020 PCA-score logistic          2 volume_ratio_20d    -0.265720   volatility_20d              1.0        -0.265720
probability year-2020 PCA-score logistic          3        return_1d    -0.323774 volume_ratio_20d              1.0        -0.323774
probability year-2020 PCA-score logistic          3      momentum_5d    -0.223820 volume_ratio_20d              1.0        -0.223820
probability year-2020 PCA-score logistic          3   volatility_20d     0.292100 volume_ratio_20d              1.0         0.292100
probability year-2020 PCA-score logistic          3 volume_ratio_20d     0.871638 volume_ratio_20d              1.0         0.871638
probability year-2020 PLS-score logistic          1        return_1d     0.031788   volatility_20d              1.0         0.031788
probability year-2020 PLS-score logistic          1      momentum_5d     0.055938   volatility_20d              1.0         0.055938
probability year-2020 PLS-score logistic          1   volatility_20d     0.994418   volatility_20d              1.0         0.994418
probability year-2020 PLS-score logistic          1 volume_ratio_20d     0.083630   volatility_20d              1.0         0.083630
probability year-2020 PLS-score logistic          2        return_1d     0.742568        return_1d              1.0         0.742568
probability year-2020 PLS-score logistic          2      momentum_5d     0.610620        return_1d              1.0         0.610620
probability year-2020 PLS-score logistic          2   volatility_20d    -0.035131        return_1d              1.0        -0.035131
probability year-2020 PLS-score logistic          2 volume_ratio_20d    -0.272951        return_1d              1.0        -0.272951
probability year-2021 PCA-score logistic          1        return_1d     0.633764      momentum_5d              1.0         0.633764
probability year-2021 PCA-score logistic          1      momentum_5d     0.649938      momentum_5d              1.0         0.649938
probability year-2021 PCA-score logistic          1   volatility_20d    -0.009911      momentum_5d              1.0        -0.009911
probability year-2021 PCA-score logistic          1 volume_ratio_20d     0.419315      momentum_5d              1.0         0.419315
probability year-2021 PCA-score logistic          2        return_1d     0.095446   volatility_20d              1.0         0.095446
probability year-2021 PCA-score logistic          2      momentum_5d     0.070301   volatility_20d              1.0         0.070301
probability year-2021 PCA-score logistic          2   volatility_20d     0.965849   volatility_20d              1.0         0.965849
probability year-2021 PCA-score logistic          2 volume_ratio_20d    -0.230398   volatility_20d              1.0        -0.230398
probability year-2021 PCA-score logistic          3        return_1d    -0.337000 volume_ratio_20d              1.0        -0.337000
probability year-2021 PCA-score logistic          3      momentum_5d    -0.231945 volume_ratio_20d              1.0        -0.231945
probability year-2021 PCA-score logistic          3   volatility_20d     0.258908 volume_ratio_20d              1.0         0.258908
probability year-2021 PCA-score logistic          3 volume_ratio_20d     0.874985 volume_ratio_20d              1.0         0.874985
probability year-2021 PLS-score logistic          1        return_1d     0.058238   volatility_20d              1.0         0.058238
probability year-2021 PLS-score logistic          1      momentum_5d     0.115117   volatility_20d              1.0         0.115117
probability year-2021 PLS-score logistic          1   volatility_20d     0.983212   volatility_20d              1.0         0.983212
probability year-2021 PLS-score logistic          1 volume_ratio_20d     0.129039   volatility_20d              1.0         0.129039
probability year-2021 PLS-score logistic          2        return_1d     0.810746        return_1d              1.0         0.810746
probability year-2021 PLS-score logistic          2      momentum_5d     0.574090        return_1d              1.0         0.574090
probability year-2021 PLS-score logistic          2   volatility_20d    -0.114272        return_1d              1.0        -0.114272
probability year-2021 PLS-score logistic          2 volume_ratio_20d    -0.007359        return_1d              1.0        -0.007359
probability year-2021 PLS-score logistic          3        return_1d    -0.292957 volume_ratio_20d              1.0        -0.292957
probability year-2021 PLS-score logistic          3      momentum_5d     0.396515 volume_ratio_20d              1.0         0.396515
probability year-2021 PLS-score logistic          3   volatility_20d    -0.141732 volume_ratio_20d              1.0        -0.141732
probability year-2021 PLS-score logistic          3 volume_ratio_20d     0.858408 volume_ratio_20d              1.0         0.858408

表中的四个模型均产生概率,因而 Brier 比较具有相同含义。PCA-score 与 PLS-score 的名称说明了“先构造成分、再分类”的两阶段结构;它们不是 PCR 或 PLS 回归。某方法是否改善概率质量只能依据多个前向评分块与基线的实际差异判断。

6.10 综合案例:收缩与降维的同切分比较

本案例沿用第 5 章的前向验证原则,在第 2 章共享样本的训练段与验证段上完成两项比较:连续最大回撤路线比较训练均值、OLS、Ridge、Lasso、PCR 与 PLS;二元事件路线比较训练事件率、L2/L1 逻辑回归、PCA-score 逻辑分类器与 PLS-score 逻辑分类器。所有填补、标准化、成分数量和惩罚强度都只能在当折拟合数据中确定;2022—2024 年测试键始终封存。

学生提交一张逐折 RMSE 表、一张逐折 Brier/AUC 表、一张系数或完整得分方向解释表,以及对稳定性、稀疏性和监督/无监督降维差异的比较。方向表必须覆盖每个已选成分和全部四项特征;跨折只能解释锚点对齐后的载荷或绝对大小,不能把原始符号翻转当成经济方向变化。模型没有稳定优于基线时,应报告“未观察到增量预测证据”,而不是增加搜索次数直到出现优势。

6.11 方法比较小结

子集选择回答“保留哪些原始变量”,Ridge 与 Lasso 回答“如何收缩原始变量系数”,PCR 与 PLS 回答“如何先构造低维得分”。Cp、AIC、BIC 和调整 R²是复杂度诊断;嵌套前向评价才回答候选方法在后续时期的预测表现。连续响应使用 RMSE,二元概率使用 Brier 分数,两个尺度不能混排。

实践报告至少应列出:固定的候选空间、每个外层年份的基线与模型误差、内层所选超参数、Lasso 的非零系数或选择频率,以及未稳定优于基线时的否定性结论。

6.12 练习

6.12.1 概念题

  1. 岭回归与 Lasso 都属于正则化方法。它们对系数的收缩(shrinkage)方式有何本质差异?为什么 Lasso 可能产生稀疏解? [核心|难度:1|分值:5|任务:独立]

  2. 为什么在岭回归、Lasso、弹性网等方法中,通常必须对自变量做标准化(例如零均值单位方差)?如果不标准化会发生什么? [核心|难度:1|分值:5|任务:独立]

  3. 解释“自由度(degrees of freedom)”在正则化回归中的含义。岭回归的有效自由度随 \(\lambda\) 变化的直观规律是什么? [核心|难度:2|分值:5|任务:独立]

  4. 子集选择(best subset selection)在统计意义上很直观,但在高维时很少直接用。它的主要计算瓶颈与统计风险分别是什么?另设 \(n=80\)、TSS = 640、\(\widehat\sigma^2=4\),两个候选模型的“参数数(含截距),RSS”为 \((3,500)\)\((6,460)\);分别计算 \(C_p\)、AIC、BIC 和调整 \(R^2\),说明本题中四项准则的选择及其不能替代前向验证的原因。 [核心|难度:2|分值:10|任务:独立]

  5. 解释 stability selection 想解决什么问题,它相对一次性 Lasso 增加了什么信息。再根据 式 6.9式 6.11 说明 PCR/PLS 构造成分时是否使用响应、各自可能遗漏或过拟合哪类方向,以及成分数为什么必须在训练折内选择。 [核心|难度:2|分值:10|任务:独立]

6.12.2 应用题

  1. 在第 2 章共享样本的训练段与验证段上完成同一项双里程碑报告;每折只用拟合期选择惩罚强度或成分数量,并证明使用键与测试键交集为空。 [核心|难度:3|分值:25|任务:综合案例]

    • 6A(个人必交,15 分):以连续最大回撤为响应,比较训练均值、OLS、Ridge、Lasso、PCR 与 PLS 的逐年 RMSE,同时报告所选惩罚、成分数和 Lasso 非零数。
    • 6B(分组项目里程碑,10 分):以回撤事件为响应,比较训练事件率、L2/L1 逻辑回归、PCA-score 与 PLS-score 逻辑分类器的 Brier/AUC,同时报告所选参数、成分数和 L1 非零数。6B 的结果进入第6—9章贯穿项目,不要求个人重复提交第二份完整报告。
  2. 针对同一个任务,用嵌套交叉验证选择超参数(\(\lambda\)\((\lambda,\alpha)\)),并与“用同一层 CV 同时选模与估误差”的做法比较误差估计偏差。 [拓展|难度:3|分值:15|任务:独立]

  3. 选取一家长三角上市公司,使用其财务/估值/成交量等特征构造高维特征集(\(p\) 可大于样本数 \(n\))。比较 Lasso 的变量选择是否稳定: [拓展|难度:3|分值:15|任务:独立]

    • 方案 A:一次性 Lasso
    • 方案 B:stability selection(重复抽样 + 选择频率阈值)

6.12.3 理论题

  1. 推导岭回归闭式解,并说明其与最小二乘解的关系: [拓展|难度:2|分值:10|任务:独立]

\[ \hat\beta^{\text{ridge}}(\lambda)=\arg\min_\beta \|y-X\beta\|_2^2+\lambda\|\beta\|_2^2. \]

  1. 在“正交设计”情形(\(X^\top X=I\))下,说明 Lasso 的解等价于对最小二乘系数做 soft-thresholding,并写出阈值化公式。 [拓展|难度:2|分值:10|任务:独立]

  2. 解释为什么选择(selection)发生在数据之后时,直接对被选中的系数做经典 t 检验会产生偏乐观结论。用一句话概括 post-selection inference 要修正的核心偏差。 [拓展|难度:3|分值:5|任务:独立]

展开完整参考解答与评分键

6.13 练习参考解答

统一评分与验收:每题分值见题后元数据;概念题按“定义 40%—机制 40%—边界 20%”给分,推导题关键中间式占 60%。代码题的时间/标签断言是门槛;指标须为有限值,概率在 \([0,1]\) 内,重复计算误差容差 \(10^{-8}\)。对象未定义、跨期泄漏、测试集选模、预写胜者或把预测关联当因果均为失败模式。

6.13.1 概念题参考解答

  1. 本质差异:岭回归用 \(\ell_2\) 惩罚,倾向于把所有系数一起收缩但通常不会压到 0;Lasso 用 \(\ell_1\) 惩罚,几何上可与坐标轴“角点”相交,因此更容易产生稀疏解(部分系数恰为 0),从而具备变量选择效果。

  2. 必须标准化的原因:正则化对系数大小施加惩罚。如果自变量量纲不同(例如“市值(亿元)”与“换手率(%)”),未标准化会让大尺度变量在同样的系数变化下导致更大预测变化,从而在优化中被“不公平”地偏好或惩罚。标准化后,惩罚在各维上可比较。

  3. 自由度直觉:它刻画模型“可调参数的有效数量/灵活度”。岭回归可写成线性平滑器 \(\hat y=S_\lambda y\),其有效自由度常用 \(\mathrm{df}(\lambda)=\mathrm{tr}(S_\lambda)\)。当 \(\lambda\uparrow\) 时,收缩更强,\(\mathrm{df}(\lambda)\) 单调下降;当 \(\lambda\downarrow 0\) 时,回到 OLS,\(\mathrm{df}\to p\)(满秩时)。

  4. 瓶颈、风险与数值计算

    • 计算:best subset 的组合数是 \(2^p\),高维时不可行。
    • 统计:即便算得出,强搜索也更容易过拟合;若再用同一数据评估误差,会出现明显的乐观偏差。
    • 数值:模型 A 的 \(C_p=51\)、AIC \(=152.61\)、BIC \(=159.75\)、调整 \(R^2=0.198\);模型 B 的 \(C_p=47\)、AIC \(=151.91\)、BIC \(=166.20\)、调整 \(R^2=0.232\)。因此 \(C_p\)、AIC 和调整 \(R^2\) 选择 B,而 BIC 选择 A,正好说明复杂度惩罚可能导致排序冲突。四项都没有模拟未来时期,也不能替代前向验证。
  5. 稳定性与降维区别:stability selection 把“是否入模”变成“入模频率”:对样本扰动重复拟合,再看每个变量被选中的比例,从而显式量化一次性 Lasso 的偶然性。PCR 的 \(V_M\) 只由 \(X_s\) 的方差结构决定,可能遗漏低方差但强预测的方向;PLS 的 \(w_m\) 使用训练响应,可能找回这类方向,也可能把响应噪声带入成分。二者的 \(M\) 都控制灵活度,必须在训练折内部选择,不能根据外层误差或封存测试期回调。

6.13.2 应用题参考解答与评分标准

第 6 题规范答案。 在干净内核中重建第 2 章共享样本,再把训练段与验证段作为唯一开发集合;同一路线的全部候选共享相同的前向拟合期和验证评分期。测试键只用于空交集断言,不得进入折叠、搜索或阈值选择;也不得把 Ridge/Lasso/PCR/PLS 的连续回归输出裁剪到 \([0,1]\) 后冒充概率。

  1. 连续回归与二元概率的双路线比较

规范流程是:先断言开发键与 m02_test_keys 交集为空,再对每个前向折分别拟合填补器和标准化器。连续路线在拟合期内部选择 Ridge/Lasso 惩罚强度和 PCR/PLS 成分数,以训练响应均值和 OLS 为基线,报告外层 RMSE。二元路线在拟合期内部选择 L2/L1 逻辑回归的 \(C\) 以及 PCA/PLS 得分数,最后用逻辑回归的 predict_proba() 输出概率,以训练事件率为基线报告 Brier/AUC。

满分答案应提供:逐折日期、样本数和标签可得性断言(4 分);填补、标准化、降维和超参数均只在拟合期学习(5 分);六种连续候选的 RMSE 及跨折汇总(5 分);五种概率候选的 Brier/AUC 及跨折汇总(5 分);Lasso/L1 非零系数、PCR/PLS 或 PCA/PLS 得分数及解释(3 分);对不稳定性、非因果性和有限未来期的说明(3 分)。

连续结果表至少包含 foldmodelrmsecomplexity;概率结果表至少包含 foldmodelbrierauccomplexity。若 AUC 因评分块单类而不可定义,应保留 Brier 分数并把 AUC 标为缺失;不得删除该折后只报告有利结果。模型是否胜出取决于当次前向验证证据,不预设收缩或降维方法的优先顺序。小节 6.9 给出了可直接运行的核心结构。

下面的答案审计只读取该节已经生成的规范对象,不重复加载数据或重新拟合。它把题面要求的误差、样本边界、参数、成分和稀疏度放在同一可评分接口中。

表 6.5: 练习6双里程碑答案审计:规范字段与选择稳定性
required_continuous_fields = {'fold', 'model', 'fit_n', 'score_n', 'label_boundary', 'rmse', 'best_params', 'components', 'nonzero', 'direction', 'complexity'}  # 固定6A字段
required_probability_fields = {'fold', 'model', 'fit_n', 'score_n', 'label_boundary', 'brier', 'auc', 'best_params', 'components', 'nonzero', 'direction', 'complexity'}  # 固定6B字段
assert required_continuous_fields.issubset(continuous_result_table.columns)  # 核对6A输出合同
assert required_probability_fields.issubset(probability_result_table.columns)  # 核对6B输出合同
assert m06_test_overlap.empty  # 再次核对开发键不接触测试键
assert continuous_result_table['rmse'].map(np.isfinite).all()  # 核对连续误差有限
assert probability_result_table['brier'].between(0, 1).all()  # 核对概率误差范围
continuous_component_rows = continuous_result_table.loc[continuous_result_table['model'].isin(['PCR', 'PLS']), 'components'].sum()  # 汇总连续路线已选成分
probability_component_rows = probability_result_table.loc[probability_result_table['model'].isin(['PCA-score logistic', 'PLS-score logistic']), 'components'].sum()  # 汇总概率路线已选成分
expected_direction_rows = int((continuous_component_rows + probability_component_rows) * len(m02_features))  # 每成分要求全部特征载荷
assert len(component_direction_table) == expected_direction_rows  # 拒绝只保存主导特征的不完整方向证据
assert component_direction_table['sign_multiplier'].isin([-1.0, 1.0]).all()  # 跨折符号只按整成分正负一对齐
print(continuous_result_table.to_string(index=False))  # 打印6A完整评分表
print(probability_result_table.to_string(index=False))  # 打印6B完整评分表
print(pd.DataFrame({'lasso_frequency': continuous_selection_frequency, 'l1_logit_frequency': probability_selection_frequency}).to_string())  # 并列稀疏稳定性
print(component_direction_table.to_string(index=False))  # 打印逐成分逐特征方向以供评分
     fold  year                  model  fit_n  score_n label_boundary     rmse                best_params  components  nonzero                  complexity                              direction
year-2020  2020 Training-mean baseline   7572      972     2019-12-31 0.048898                         {}         NaN      NaN               mean baseline                                    NaN
year-2020  2020                    OLS   7572      972     2019-12-31 0.046541                         {}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020                  Ridge   7572      972     2019-12-31 0.046541     {'model__alpha': 0.01}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020                  Lasso   7572      972     2019-12-31 0.046528   {'model__alpha': 0.0001}         NaN      4.0   components=nan; nonzero=4                                       
year-2020  2020                    PCR   7572      972     2019-12-31 0.046522 {'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2020  2020                    PLS   7572      972     2019-12-31 0.046565 {'model__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
year-2021  2021 Training-mean baseline   8464      892     2020-12-31 0.046066                         {}         NaN      NaN               mean baseline                                    NaN
year-2021  2021                    OLS   8464      892     2020-12-31 0.041455                         {}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021                  Ridge   8464      892     2020-12-31 0.041455     {'model__alpha': 0.01}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021                  Lasso   8464      892     2020-12-31 0.041451   {'model__alpha': 0.0001}         NaN      4.0   components=nan; nonzero=4                                       
year-2021  2021                    PCR   8464      892     2020-12-31 0.041458 {'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2021  2021                    PLS   8464      892     2020-12-31 0.041464 {'model__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
     fold  year                  model  fit_n  score_n label_boundary    brier      auc                                                         best_params  components  nonzero                  complexity                              direction
year-2020  2020 Training-rate baseline   7572      972     2019-12-31 0.217640 0.500000                                                                  {}         NaN      NaN         event-rate baseline                                    NaN
year-2020  2020            L2 logistic   7572      972     2019-12-31 0.205603 0.660394                           {'model__C': 0.1, 'model__penalty': 'l2'}         NaN      NaN components=nan; nonzero=nan                                       
year-2020  2020            L1 logistic   7572      972     2019-12-31 0.205767 0.660282                             {'model__C': 1, 'model__penalty': 'l1'}         NaN      4.0   components=nan; nonzero=4                                       
year-2020  2020     PCA-score logistic   7572      972     2019-12-31 0.205713 0.659794 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2020  2020     PLS-score logistic   7572      972     2019-12-31 0.205937 0.658452 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 2}         2.0      NaN   components=2; nonzero=nan  complete_rows=8; sign=anchor-positive
year-2021  2021 Training-rate baseline   8464      892     2020-12-31 0.240808 0.500000                                                                  {}         NaN      NaN         event-rate baseline                                    NaN
year-2021  2021            L2 logistic   8464      892     2020-12-31 0.208340 0.718515                           {'model__C': 0.1, 'model__penalty': 'l2'}         NaN      NaN components=nan; nonzero=nan                                       
year-2021  2021            L1 logistic   8464      892     2020-12-31 0.208330 0.718713                           {'model__C': 0.1, 'model__penalty': 'l1'}         NaN      3.0   components=nan; nonzero=3                                       
year-2021  2021     PCA-score logistic   8464      892     2020-12-31 0.208375 0.719223 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
year-2021  2021     PLS-score logistic   8464      892     2020-12-31 0.208342 0.718541 {'model__C': 0.1, 'model__penalty': 'l2', 'score__n_components': 3}         3.0      NaN   components=3; nonzero=nan complete_rows=12; sign=anchor-positive
                  lasso_frequency  l1_logit_frequency
feature                                              
momentum_5d                   1.0                 1.0
return_1d                     1.0                 0.5
volatility_20d                1.0                 1.0
volume_ratio_20d              1.0                 1.0
      route      fold              model  component          feature  raw_loading   anchor_feature  sign_multiplier  aligned_loading
 continuous year-2020                PCR          1        return_1d     0.638558      momentum_5d              1.0         0.638558
 continuous year-2020                PCR          1      momentum_5d     0.653910      momentum_5d              1.0         0.653910
 continuous year-2020                PCR          1   volatility_20d    -0.001949      momentum_5d              1.0        -0.001949
 continuous year-2020                PCR          1 volume_ratio_20d     0.405760      momentum_5d              1.0         0.405760
 continuous year-2020                PCR          2        return_1d     0.096984   volatility_20d              1.0         0.096984
 continuous year-2020                PCR          2      momentum_5d     0.073026   volatility_20d              1.0         0.073026
 continuous year-2020                PCR          2   volatility_20d     0.956376   volatility_20d              1.0         0.956376
 continuous year-2020                PCR          2 volume_ratio_20d    -0.265720   volatility_20d              1.0        -0.265720
 continuous year-2020                PCR          3        return_1d    -0.323774 volume_ratio_20d              1.0        -0.323774
 continuous year-2020                PCR          3      momentum_5d    -0.223820 volume_ratio_20d              1.0        -0.223820
 continuous year-2020                PCR          3   volatility_20d     0.292100 volume_ratio_20d              1.0         0.292100
 continuous year-2020                PCR          3 volume_ratio_20d     0.871638 volume_ratio_20d              1.0         0.871638
 continuous year-2020                PLS          1        return_1d     0.002081   volatility_20d              1.0         0.002081
 continuous year-2020                PLS          1      momentum_5d     0.003956   volatility_20d              1.0         0.003956
 continuous year-2020                PLS          1   volatility_20d     0.997986   volatility_20d              1.0         0.997986
 continuous year-2020                PLS          1 volume_ratio_20d     0.063269   volatility_20d              1.0         0.063269
 continuous year-2020                PLS          2        return_1d    -0.484778 volume_ratio_20d              1.0        -0.484778
 continuous year-2020                PLS          2      momentum_5d    -0.499144 volume_ratio_20d              1.0        -0.499144
 continuous year-2020                PLS          2   volatility_20d    -0.042464 volume_ratio_20d              1.0        -0.042464
 continuous year-2020                PLS          2 volume_ratio_20d     0.716967 volume_ratio_20d              1.0         0.716967
 continuous year-2021                PCR          1        return_1d     0.633764      momentum_5d              1.0         0.633764
 continuous year-2021                PCR          1      momentum_5d     0.649938      momentum_5d              1.0         0.649938
 continuous year-2021                PCR          1   volatility_20d    -0.009911      momentum_5d              1.0        -0.009911
 continuous year-2021                PCR          1 volume_ratio_20d     0.419315      momentum_5d              1.0         0.419315
 continuous year-2021                PCR          2        return_1d     0.095446   volatility_20d              1.0         0.095446
 continuous year-2021                PCR          2      momentum_5d     0.070301   volatility_20d              1.0         0.070301
 continuous year-2021                PCR          2   volatility_20d     0.965849   volatility_20d              1.0         0.965849
 continuous year-2021                PCR          2 volume_ratio_20d    -0.230398   volatility_20d              1.0        -0.230398
 continuous year-2021                PCR          3        return_1d    -0.337000 volume_ratio_20d              1.0        -0.337000
 continuous year-2021                PCR          3      momentum_5d    -0.231945 volume_ratio_20d              1.0        -0.231945
 continuous year-2021                PCR          3   volatility_20d     0.258908 volume_ratio_20d              1.0         0.258908
 continuous year-2021                PCR          3 volume_ratio_20d     0.874985 volume_ratio_20d              1.0         0.874985
 continuous year-2021                PLS          1        return_1d     0.013122   volatility_20d              1.0         0.013122
 continuous year-2021                PLS          1      momentum_5d     0.034323   volatility_20d              1.0         0.034323
 continuous year-2021                PLS          1   volatility_20d     0.995704   volatility_20d              1.0         0.995704
 continuous year-2021                PLS          1 volume_ratio_20d     0.084994   volatility_20d              1.0         0.084994
 continuous year-2021                PLS          2        return_1d     0.707239        return_1d              1.0         0.707239
 continuous year-2021                PLS          2      momentum_5d     0.543283        return_1d              1.0         0.543283
 continuous year-2021                PLS          2   volatility_20d     0.010558        return_1d              1.0         0.010558
 continuous year-2021                PLS          2 volume_ratio_20d    -0.452267        return_1d              1.0        -0.452267
probability year-2020 PCA-score logistic          1        return_1d     0.638558      momentum_5d              1.0         0.638558
probability year-2020 PCA-score logistic          1      momentum_5d     0.653910      momentum_5d              1.0         0.653910
probability year-2020 PCA-score logistic          1   volatility_20d    -0.001949      momentum_5d              1.0        -0.001949
probability year-2020 PCA-score logistic          1 volume_ratio_20d     0.405760      momentum_5d              1.0         0.405760
probability year-2020 PCA-score logistic          2        return_1d     0.096984   volatility_20d              1.0         0.096984
probability year-2020 PCA-score logistic          2      momentum_5d     0.073026   volatility_20d              1.0         0.073026
probability year-2020 PCA-score logistic          2   volatility_20d     0.956376   volatility_20d              1.0         0.956376
probability year-2020 PCA-score logistic          2 volume_ratio_20d    -0.265720   volatility_20d              1.0        -0.265720
probability year-2020 PCA-score logistic          3        return_1d    -0.323774 volume_ratio_20d              1.0        -0.323774
probability year-2020 PCA-score logistic          3      momentum_5d    -0.223820 volume_ratio_20d              1.0        -0.223820
probability year-2020 PCA-score logistic          3   volatility_20d     0.292100 volume_ratio_20d              1.0         0.292100
probability year-2020 PCA-score logistic          3 volume_ratio_20d     0.871638 volume_ratio_20d              1.0         0.871638
probability year-2020 PLS-score logistic          1        return_1d     0.031788   volatility_20d              1.0         0.031788
probability year-2020 PLS-score logistic          1      momentum_5d     0.055938   volatility_20d              1.0         0.055938
probability year-2020 PLS-score logistic          1   volatility_20d     0.994418   volatility_20d              1.0         0.994418
probability year-2020 PLS-score logistic          1 volume_ratio_20d     0.083630   volatility_20d              1.0         0.083630
probability year-2020 PLS-score logistic          2        return_1d     0.742568        return_1d              1.0         0.742568
probability year-2020 PLS-score logistic          2      momentum_5d     0.610620        return_1d              1.0         0.610620
probability year-2020 PLS-score logistic          2   volatility_20d    -0.035131        return_1d              1.0        -0.035131
probability year-2020 PLS-score logistic          2 volume_ratio_20d    -0.272951        return_1d              1.0        -0.272951
probability year-2021 PCA-score logistic          1        return_1d     0.633764      momentum_5d              1.0         0.633764
probability year-2021 PCA-score logistic          1      momentum_5d     0.649938      momentum_5d              1.0         0.649938
probability year-2021 PCA-score logistic          1   volatility_20d    -0.009911      momentum_5d              1.0        -0.009911
probability year-2021 PCA-score logistic          1 volume_ratio_20d     0.419315      momentum_5d              1.0         0.419315
probability year-2021 PCA-score logistic          2        return_1d     0.095446   volatility_20d              1.0         0.095446
probability year-2021 PCA-score logistic          2      momentum_5d     0.070301   volatility_20d              1.0         0.070301
probability year-2021 PCA-score logistic          2   volatility_20d     0.965849   volatility_20d              1.0         0.965849
probability year-2021 PCA-score logistic          2 volume_ratio_20d    -0.230398   volatility_20d              1.0        -0.230398
probability year-2021 PCA-score logistic          3        return_1d    -0.337000 volume_ratio_20d              1.0        -0.337000
probability year-2021 PCA-score logistic          3      momentum_5d    -0.231945 volume_ratio_20d              1.0        -0.231945
probability year-2021 PCA-score logistic          3   volatility_20d     0.258908 volume_ratio_20d              1.0         0.258908
probability year-2021 PCA-score logistic          3 volume_ratio_20d     0.874985 volume_ratio_20d              1.0         0.874985
probability year-2021 PLS-score logistic          1        return_1d     0.058238   volatility_20d              1.0         0.058238
probability year-2021 PLS-score logistic          1      momentum_5d     0.115117   volatility_20d              1.0         0.115117
probability year-2021 PLS-score logistic          1   volatility_20d     0.983212   volatility_20d              1.0         0.983212
probability year-2021 PLS-score logistic          1 volume_ratio_20d     0.129039   volatility_20d              1.0         0.129039
probability year-2021 PLS-score logistic          2        return_1d     0.810746        return_1d              1.0         0.810746
probability year-2021 PLS-score logistic          2      momentum_5d     0.574090        return_1d              1.0         0.574090
probability year-2021 PLS-score logistic          2   volatility_20d    -0.114272        return_1d              1.0        -0.114272
probability year-2021 PLS-score logistic          2 volume_ratio_20d    -0.007359        return_1d              1.0        -0.007359
probability year-2021 PLS-score logistic          3        return_1d    -0.292957 volume_ratio_20d              1.0        -0.292957
probability year-2021 PLS-score logistic          3      momentum_5d     0.396515 volume_ratio_20d              1.0         0.396515
probability year-2021 PLS-score logistic          3   volatility_20d    -0.141732 volume_ratio_20d              1.0        -0.141732
probability year-2021 PLS-score logistic          3 volume_ratio_20d     0.858408 volume_ratio_20d              1.0         0.858408

评分时先核对 label_boundary 早于当年评分起点,再比较同一路线的误差。若 Lasso/L1 的选择频率跨年份不稳定,应把它写入结论;成分数或非零数较少也不自动意味着更可靠。方向解释必须引用 aligned_loading 或绝对载荷,并同时保留 raw_loading、锚点和正负一乘子作为审计证据。

  1. 嵌套与非嵌套误差估计

内层前向折只选择 \(C\) 或成分数,外层前向折才估计整套选择流程的 Brier 分数。非嵌套值是内层候选中的最小平均验证损失,它参与了选择,通常偏乐观;“通常”不是保证每次实现都更小。满分证据包括:内外层日期图 4 分;所有预处理位于管道内 4 分;外层逐折 Brier 4 分;与非嵌套最小值的差及不预设符号的解释 3 分。

下面是可复用的最小结构;把 classification_candidates 中一个候选及其网格代入即可。

candidate_pipeline, candidate_grid = classification_candidates['L2 logistic']  # 选一个待核对候选
outer_splits = make_panel_forward_splits(m06_development_sample, split_count=4)  # 只在开发期按面板日期构造外层折
nested_brier = []  # 保存整套选择流程的外层损失
for outer_fit, outer_score in outer_splits:  # 逐个评价未来日期块
    outer_train = m06_development_sample.iloc[outer_fit].reset_index(drop=True)  # 提取当前外层训练样本
    inner_splits = make_panel_forward_splits(outer_train, split_count=3)  # 只在外层训练样本内选参
    nested_search = GridSearchCV(candidate_pipeline, candidate_grid, cv=inner_splits, scoring='neg_brier_score')  # 定义内层搜索
    nested_search.fit(outer_train[m02_features], outer_train['drawdown_event_20d'])  # 拟合完整选择流程
    outer_probability = nested_search.predict_proba(m06_development_sample.iloc[outer_score][m02_features])[:, 1]  # 预测开发期外层块
    nested_brier.append(brier_score_loss(m06_development_sample.iloc[outer_score]['drawdown_event_20d'], outer_probability))  # 保存损失
full_inner_splits = make_panel_forward_splits(m06_development_sample, split_count=3)  # 构造只含训练与验证行的全开发内层折
non_nested_search = GridSearchCV(candidate_pipeline, candidate_grid, cv=full_inner_splits, scoring='neg_brier_score').fit(m06_development_sample[m02_features], m06_development_sample['drawdown_event_20d'])  # 得到开发期内层最小值
print({'outer_brier': nested_brier, 'outer_mean': np.mean(nested_brier), 'non_nested_min': -non_nested_search.best_score_})  # 并列报告而不预写方向
{'outer_brier': [0.1826987227270019, 0.10796154617370587, 0.24796903693771352, 0.2042175520971045], 'outer_mean': 0.18571171448388143, 'non_nested_min': 0.18608717762796526}
  1. L1 选择稳定性

先固定与第 6 题一致的特征和候选 \(C\),再用多个连续历史窗口或移动块重样本重复拟合 L1 逻辑回归。对每个特征报告非零频率、系数符号一致率和样本窗口;不得只展示一次入选集合。满分证据包括:扰动单位与时间范围 4 分;至少 20 次有效拟合 3 分;逐变量选择频率与符号 4 分;阈值敏感性 2 分;“稳定预测摘要不等于因果变量”的边界 2 分。若相关变量轮流入选,应报告组层面的不稳定性,而非挑选频率最高者作为唯一真实因子。

6.13.3 理论题参考解答(推导要点)

  1. 岭回归闭式解:目标函数

\[ L(\beta)= (y-X\beta)^\top(y-X\beta)+\lambda\beta^\top\beta. \]

\(\beta\) 求导并令其为 0:

\[ -2X^\top(y-X\beta)+2\lambda\beta=0\;\Rightarrow\;(X^\top X+\lambda I)\beta=X^\top y, \]

因此

\[ \hat\beta^{\text{ridge}}(\lambda)=(X^\top X+\lambda I)^{-1}X^\top y. \]

\(\lambda\to 0\)(且 \(X^\top X\) 可逆)时,回到 OLS:\((X^\top X)^{-1}X^\top y\);当 \(\lambda\uparrow\) 时,解被更强地向 0 收缩。

  1. 正交设计下的 soft-thresholding:在 \(X^\top X=I\) 时,Lasso

\[ \min_\beta \frac12\|y-X\beta\|_2^2+\lambda\|\beta\|_1 \]

可分解到每个坐标。设 OLS 系数 \(\hat\beta^{\text{ols}}=X^\top y\),则每个分量

\[ \hat\beta_j^{\text{lasso}}=\mathrm{sign}(\hat\beta_j^{\text{ols}})\,(|\hat\beta_j^{\text{ols}}|-\lambda)_+. \]

这解释了 Lasso 的稀疏性:当 \(|\hat\beta_j^{\text{ols}}|\le\lambda\) 时,系数被直接压到 0。

  1. 选择后推断的偏差:当你先用数据挑出“看起来显著/相关”的变量,再对这些变量做传统检验,相当于条件在“被挑中”这一事件上,会导致 p 值偏小、区间偏窄(选择偏差)。post-selection inference 的核心就是在推断时把“变量是如何被选出来的”也纳入条件与分布校正。

6.14 章末回顾

逐项自检:能否解释 Ridge、Lasso、PCR、PLS、PCA-score 与 PLS-score 的差异;能否在训练内前向折选参;能否分别用连续预测计算 RMSE、用 predict_proba() 计算 Brier;能否在同一评分样本上比较基线与候选。禁止把 L1 非零项称为因果驱动,也禁止在评分期上重调参。

不看正文回答:L1 为什么产生结构性稀疏?PCA-score 与 PLS-score 如何使用响应?为什么所有候选必须使用相同的前向折?迁移任务:为高维客户流失设计一个收缩候选。下一章在同一验证原则下加入可解释非线性。

展开检索答案 L1 在零点有折角;PCA 不用响应选方向而 PLS 使用训练响应提取方向;共用前向折才能把误差差异归因于方法而不是评分样本。

进入第 7 章前,应能说明三项证据:所有预处理在训练折内拟合;每折满足 max(fit.label_end_date) < min(score.prediction_date);Brier 的输入是概率而不是连续回归值或硬标签。需要补修时分别回看收缩路径、小节 6.9 与第 5 章的前向切分。

Hoerl, Arthur E., 和 Robert W. Kennard. 1970年. 《Ridge Regression: Biased Estimation for Nonorthogonal Problems》. Technometrics 12 (1): 55~67. https://doi.org/10.1080/00401706.1970.10488634.
Tibshirani, Robert. 1996年. 《Regression Shrinkage and Selection via the Lasso》. Journal of the Royal Statistical Society: Series B (Methodological) 58 (1): 267~88. https://doi.org/10.1111/j.2517-6161.1996.tb02080.x.