因子权重收缩Shrinkage算法,旨在基于历史表现计算的因子权重,向一个等权先验进行压缩。
这里示例因子权重收缩算法实现,并在代码层面逐层拆解,然后进一步分析背后的金融逻辑。
1 Shrinkage算法
如下代码实现了一个在量化金融领域非常经典的因子权重收缩Shrinkage算法。
这段代码是用统计收缩(Statistical Shrinkage)手段,将量化研究员对因子历史表现的狂热追崇,
强行拉回到一视同仁理性基准线上,防止多因子模型过拟合、提升策略稳健性的工业级标准实践。
def shrink_weights_to_equal(
weight_matrix: pd.DataFrame,
lam: float,
) -> pd.DataFrame:
"""
Row-wise shrinkage of factor weights toward the equal-weight design prior.
w(T) = lam * w_icir(T) / sum|w_icir(T)| + (1 - lam) * prior(T)
where prior(T) spreads 1 equally over the factors *supported* on that row
(non-NaN and non-zero weights) and is 0 elsewhere. For a dense rolling-ICIR
matrix the prior is the uniform +1/n "design sign" (all factors in this
library are constructed so higher = expected better); for a top-k selection
matrix (zeros = not selected) the prior is +1/k over the selected factors
only, so unselected factors stay out.
Rationale: rolling ICIR estimates are noisy and their signs flip in and
out of regimes; convex shrinkage toward the prior lets a factor deviate
from (or flip against) its design direction only when the trailing
evidence is strong enough. lam=1 reproduces the raw rolling weights,
lam=0 gives the pure equal-weight prior. All-NaN rows (warm-up) and
all-zero rows (no signal / no support) are preserved as NaN.
"""
if not 0.0 <= lam <= 1.0:
raise ValueError("lam must be in [0, 1]")
w = weight_matrix.astype(float)
row_abs = w.abs().sum(axis=1)
wn = w.div(row_abs.where(row_abs > 1e-12), axis=0)
support = w.notna() & (w.abs() > 1e-12)
cnt = support.sum(axis=1)
prior = support.astype(float).div(cnt.replace(0, np.nan), axis=0)
return lam * wn + (1.0 - lam) * prior
2 代码逻辑分析
1)输入与参数校验
weight_matrix是行索引通常是时间点(Date) ,列索引是各个因子(Factors)。
单元格数值代表该时点该因子的**原始权重(**例如基于滚动ICIR算出的权重)。
这里每个单元格是因子近期在全体股票上的整体效力,不是对某只股票的效力,它不含个股信息。
lam是收缩强度系数,取值范围 0, 1。1代表完全不收缩,0代表完全采用先验。
if not 0.0 <= lam <= 1.0:
raise ValueError("lam must be in [0, 1]")
w = weight_matrix.astype(float)
2)行内 L1 归一化
算每一行所有因子权重的绝对值之和。
将每个原始权重除以该行绝对值和,得到 wn。
数学表达为
这里用的是绝对值之和,而非简单求和。
这意味着保留了权重的符号(做多/做空方向),同时将总杠杆(L1范数)标准化为 1。
1e-12 是为了防止除以 0。
row_abs = w.abs().sum(axis=1)
wn = w.div(row_abs.where(row_abs > 1e-12), axis=0)
3)有效支持域与等权先验
support是一个布尔矩阵,标记了该时间点哪些因子是活跃有效的,即非空且权重绝对值不为 0。
cnt 统计每一行有效因子的个数 k。
prior 先验矩阵,对于每一行,将1平均分配给所有有效的因子(即权重为 1/k),无效因子则为 0。
数学表达:
若因子i有效
否则
若某行全部无效(cnt=0),则替换为 NaN 保留,避免除以零。
support = w.notna() & (w.abs() > 1e-12)
cnt = support.sum(axis=1)
prior = support.astype(float).div(cnt.replace(0, np.nan), axis=0)
4)凸组合(Convex Combination)收缩
最终的收缩权重是归一化后的原始权重wn与等权先验prior的加权平均。
完全等价于公式:
return lam * wn + (1.0 - lam) * prior
3 蕴藏金融理论
这段代码绝不仅是数学运算,它深刻嵌入了贝叶斯估计、因子投资组合构建和风险控制核心思想。
1)滚动 ICIR
滚动 ICIR即信息系数信息比率
在多因子量化模型中,通常用滚动 ICIR计算每个因子的IC,并除以其标准差得到 ICIR。
原始权重(weight_matrix)通常正比于ICIR(即ICIR 越高,赋予该因子的权重越高)。
然而 ICIR滚动估计噪声极大 ,且因子收益存在明显**风格切换(**Sign Flip),
今天有效的因子明天可能失效甚至反向。
2)收缩估计量
收缩估计量(Shrinkage Estimator)与偏差-方差权衡,
这本质上是 James-Stein 估计量 在投资组合权重中的应用。
原始权重(基于ICIR)是无偏但高方差的估计;等权先验是有偏但低方差的估计。
通过向先验收缩,显著降低了权重序列的波动率,减少了因估计误差导致的极端做多/做空头寸,从而提高样本外的夏普比率(Sharpe Ratio)。
3)设计先验(Design Prior)
构建等权先验是引入因子动量反转的防守机制,默认均值回归。
因子在构造时被标准化为方向性一致(数值越大,预期未来收益越高)。
等权先验(1/k)代表了无信息或中性的态度。
在没有充分的近期数据证据之前,默认所有有效因子的预测能力相同,给予相同的暴露。
这种先验属于保守型先验,防止模型过度依赖某个因子的近期虚假繁荣。
4)L1归一化与风险预算
绝对值求和归一化,目的是控制组合总杠杆(Gross Exposure),防止权重膨胀
除以 sum(|w|) 而不是 sum(w),是为了确保多空总杠杆恒定。
如果只除以普通求和,多空抵消后会放大净权重,导致实际杠杆失控。
归一化后,无论原始权重多大或多小,每一期的总多头暴露 + 总空头暴露之和恒为1(因为L1范数 = 1),这使得不同时间点的组合可比且风险预算可控。
5)稀疏性与Top-K选股支持域
识别非零有效因子目的是实现因子层面的贝叶斯模型平均,仅在当前活跃的因子池中进行配置
如果原始权重矩阵是稠密的,所有因子都有非零权重,则先验是等权分配给所有因子。
如果原始权重矩阵是稀疏的(即采用了 Top-K 筛选,未选中的因子直接赋为 0),则support自动排除这些零值,先验只在被选中的 K 个因子之间等权分配。
这保证了收缩过程不会复活那些被逻辑规则(如显著性检验)剔除出去的因子。如
果某个因子未进入筛选池,说明它当前没有信号支持,哪怕收缩也不应该给它仓位。
6)参数λ的经济学含义
凸组合收缩是经典的收缩估计,在偏差(Bias)和方差(Variance)间取得最优平衡
另外λ可以理解为对历史数据的信心强度。
当市场处于平稳期,因子规律稳定时,可设置较高的 λ(如 0.8)。
当市场处于高波动/风格急剧切换期,设置较低的 λ(如 0.3),强制权重回归等权,避免被短期的极端 ICIR 值误导,这是一种防守型风控手段。
reference
ICIR权重矩阵如何加权标准化为综合因子
https://blog.csdn.net/liliang199/article/details/164484760
基于信号强度的稀疏选股算法的探索分析