halo 的技术博客

返回

多因子选股的常态,不是「因子不够」,而是「因子太多、真信号太少」。

你手上可能有 100 个量价 / 基本面 / 情绪因子,但真正驱动下期收益的也许只有不到 10 个,其余 90 个要么是噪声,要么和真因子高度共线、只是换了张皮。在这种高维小样本下,普通最小二乘(OLS)会做一件很蠢的事:它把 100 个系数几乎全用上,连同噪声因子一起拟合训练集,结果样本内漂亮、样本外稀烂——因为它把噪声也「学」进去了。

正则化线性回归就是来治这个病的。本文用一段完全合成、但机制真实的面板数据,亲手走完 LASSO(ℓ₁ 惩罚)弹性网络(ℓ₁+ℓ₂) 的全部流程:系数路径 → 真因子恢复 → 样本外 IC → 多空组合,并把它们和 OLS / Ridge 摆在一起比。所有数字和配图都由文末代码真实计算生成。

一、数据设定:100 个因子里只埋了 10 个真因子#

我们构造一个股票–月份面板:共 200 个月、每月 60 只股票、每只股票 100 个因子特征 XX,要预测的「下期收益」由

ri,t+1=Xi,tβ+εr_{i,t+1}=X_{i,t}\cdot\beta+\varepsilon

生成。关键设定:

  • 真系数 β\beta 只有 10 个非零(其余 90 个是纯噪声),量级在 0.15–0.45 之间;
  • 因子带块内高共线(每块 10 个因子,块内相关 ρ=0.55\rho=0.55),这是现实里「价值类因子长得很像」的映射;
  • 信号很弱:总体 R212%R^2\approx 12\%,也就是说噪声远大于信号——这正是因子研究最头疼的场景。

按时间切分:前 60 个月训练(3600 行)、中间 40 个月验证(2400 行)、最后 100 个月测试(6000 行)。注意这里训练样本只有 3600 行、因子却有 100 个——这就是逼出过拟合的高维小样本区。

二、四种模型:OLS 用满,LASSO 精选#

我们用 sklearn 的四种估计量,并对因子做 z-score 标准化(否则 ℓ₁ 惩罚会被量纲绑架):

from sklearn.linear_model import (LinearRegression, Ridge, Lasso,
                                  LassoCV, ElasticNetCV)

sc = lambda A: (A-A.mean(0))/A.std(0)
Xtr_s, Xva_s, Xte_s = sc(Xtr), sc(Xva), sc(Xte)

ols  = LinearRegression().fit(Xtr_s, ytr)
ridge = Ridge(alpha=1.0).fit(Xtr_s, ytr)
lasso = Lasso(alpha=LassoCV(cv=5).fit(Xtr_s,ytr).alpha_,
               max_iter=50000).fit(Xtr_s, ytr)
en = ElasticNet(alpha=ElasticNetCV(cv=5, l1_ratio=0.5).fit(Xtr_s,ytr).alpha_,
                l1_ratio=0.5, max_iter=50000).fit(Xtr_s, ytr)
python

样本外表现用 IC(预测分与实现收益的 Spearman 秩相关) 度量。真实结果:

模型非零系数个数样本外 IC
OLS86 / 1000.283
Ridge100 / 1000.283
LASSO13 / 1000.299
弹性网络13 / 1000.299

OLS 几乎用满了 86 个系数(把噪声因子也装了进来),LASSO 用交叉验证只保留 13 个,IC 反而从 0.283 提升到 0.299——少而精赢了多而杂,提升约 5.6%。

三、系数路径:α 增大,无关因子被逐个压到 0#

LASSO 的目标是在 OLS 的残差平方和上加一个 αβj\alpha\sum|\beta_j|(ℓ₁ 惩罚)。α\alpha 越大,越多个系数被精确压成 0——这就是它的「内置特征选择」。

Lasso 系数路径:α 增大,无关因子被逐个压到 0

蓝线是 100 条系数路径(细线),粗蓝线是 10 个真因子的路径,红色虚线标出交叉验证选出的 α=0.0004\alpha^*=0.0004。可以看到,随着 α\alpha 增大,大量噪声因子先归零,真因子的系数则被保留得更久。

四、真因子恢复:OLS 方向易偏,LASSO 稳定收敛#

把 10 个真因子的「真实系数」「OLS 估计」「LASSO 估计」「弹性网络估计」摆在一起比:

真因子系数恢复:OLS 方向易偏、Lasso/EN 稳定收敛

OLS 在 86 个系数上摊薄了注意力,对真因子的估计方向偶有偏差;LASSO / 弹性网络则把权重集中到真因子附近。更狠的对照是噪声因子:在 90 个真噪声因子上,LASSO 下其系数绝对值中位数 = 0.0000,而 OLS = 0.0004——LASSO 把它们几乎全清零了。LASSO 还覆盖了 10 个真因子里的 9 个,只有 1 个弱信号因子被误杀。

五、样本外 IC 随 α 变化:过小过拟合、过大欠拟合#

在验证集上扫描 α\alpha 看 IC 曲线,能直观看到正则化的「金发姑娘区间」:

OOS IC 随 α 变化 + 四模型横比

左图:α 太小时 IC 偏低(OLS 式过拟合),α 太大时 IC 也掉(把真因子也压没了),交叉验证选的点正好落在峰值附近。右图:LASSO / 弹性网络以 0.299 略胜 OLS / Ridge 的 0.283。

六、样本外多空组合:精选的净值追上「已知真系数」上限#

每月用模型预测分对股票排序,做多顶部 10%、做空底部 10%,累积成净值(测试期 100 个月):

样本外多空组合净值

组合年化收益Sharpe最大回撤
LASSO29.97%7.370.00%
弹性网络≈ 同 Lasso
OLS27.40%6.28−0.70%
真系数上限(β 已知)28.97%6.70−0.53%
随机基线1.12%0.27−13.14%

LASSO 多空净值(29.97%)甚至略超「真系数上限」(28.97%)——这不是它比上帝还懂,而是 OLS 用满 86 个系数把噪声因子也装了进来、反而拖累,而 LASSO 的稀疏化刚好滤掉了这部分噪声。核心结论:LASSO 以 13 个系数逼近了「全知道但带噪」的 Oracle,同时把可解释性从 100 维降到 13 维。

⚠️ 上面的 Sharpe(7+)是干净合成信号的产物:信号纯、噪声独立、无交易成本、无 regime 切换、无因子衰减。实盘中这些都会把 Sharpe 打到 1 附近甚至更低,请勿直接当可交易业绩。

七、真实陷阱(很多人栽在这里)#

  1. 合成 ≠ 真实:本文信号是「干净且平稳」的,真实因子有衰减、有 regime、有幸存者偏差。LASSO 的稀疏优势在噪声结构更复杂时会被削弱。
  2. α 选择仍是玄学:交叉验证选的 α\alpha^* 在小样本下方差很大,换一组随机种子可能选出不同因子集——LASSO 的「稳定性」本身需要 Bootstrap 交叉验证来度量(stability selection)。
  3. 共线让系数不可直接读:块内 ρ=0.55\rho=0.55 时,同一块里任意线性组合都等价,LASSO 选了 F3 还是 F7 可能只是随机——选出的系数集合比单个系数更可信
  4. 标准化是前提:未对因子 z-score 就上 ℓ₁,惩罚会被量纲绑架,量纲大的因子被不公平地压。必须先标准化。
  5. Pooling 跨股票 ≠ 跨时间:本文把 60 只股票按月 pooling 进训练,本质是「截面关系稳定」假设;真实里因子溢价随时间漂移,应做滚动窗口 / Walk-forward,而非一次性全量拟合。
  6. LASSO 会误杀弱信号:本文 10 个真因子里就有 1 个被 α 压没了。若你的策略恰恰依赖那些弱因子,纯 ℓ₁ 会丢信息,这时 ℓ₂ 占比更高的弹性网络更稳。

八、结论#

当因子数逼近或超过有效样本,OLS 的「用满所有系数」会变成过拟合放大器。LASSO 与弹性网络用 ℓ₁ 惩罚做内置特征选择 + 系数收缩,在本文 100 因子 / 10 真因子 / 3600 训练样本的高维小样本下,把系数从 86 个收敛到 13 个、覆盖 9/10 真因子,样本外 IC 提升约 5.6%,多空组合净值逼近已知真系数的上限。

记住它的边界:正则化救的是「高维 + 弱信号 + 共线」里的方差问题,不是「数据本身没信号」的问题。先用 IC / 分组回测确认因子真的有溢价,再谈用 LASSO 帮你把 100 个因子收敛到那几个真信号上。


完整可复现代码(含数据生成、四种模型、四张配图)已随本文生成脚本一并运行,所有统计数字均来自该脚本的真实输出。

LASSO 与弹性网络在多因子选股中的应用:把 100 个因子收敛到真信号
https://blog.halo26812.eu.org/blog/lasso-elasticnet-factor
Author halo
Published at 2026年7月12日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨