ICIR权重矩阵如何加权标准化为综合因子

计算出ICIR权重矩阵后,通常需要将权重矩阵标准化,加权合成综合因子,用于构建投资组合。

这里通过示例函数,尝试分析和说明如何用时间变化的权重矩阵(例如滚动 ICIR),

将多个标准化后的因子值线性组合成一个综合因子得分。

1 权重矩阵标准化

这里给出combine_factors_rolling_weight,函数利用时间变化的权重矩阵(例如滚动 ICIR),

将多个标准化后的因子值线性组合成一个综合因子得分。

其输出可作为选股信号(如多因子打分),用于后续的回测或实盘交易。

1.1 代码示例

combine_factors_rolling_weight将多个因子normalized_factors,

按照时间维度动态变化的权重weight_matrix合成为一个复合因子Composite Factor。

代码示例如下

复制代码
def combine_factors_rolling_weight(
    normalized_factors: Dict[str, pd.DataFrame],
    weight_matrix: pd.DataFrame,
) -> pd.DataFrame:
    """
    Time-varying weighted composite (datetime × stock).

    At each date T the factor weights are row T of `weight_matrix` (e.g. the
    rolling ICIRs from compute_rolling_icir_weights), sign-preserved and
    normalized by the row's sum of absolute weights --- the same convention as
    combine_factors_icir_weight, but re-estimated every day from past-only
    information.

    Dates where every weight is NaN (rolling warm-up) produce a NaN composite
    row; callers should trim the warm-up period from the backtest window.
    Per-stock NaN factor values are skipped in the weighted sum (min_count=1),
    matching the static combiners.
    """
    names = [n for n in normalized_factors if n in weight_matrix.columns]
    if not names:
        raise ValueError("weight_matrix columns do not match normalized_factors keys")

    w = weight_matrix[names].astype(float)
    row_abs = w.abs().sum(axis=1)
    # Rows with ~zero total |weight| become NaN (no signal) rather than
    # silently falling back to equal weight mid-sample.
    wn = w.div(row_abs.where(row_abs > 1e-12), axis=0)

    weighted_dfs = [normalized_factors[n].mul(wn[n], axis=0) for n in names]
    stacked = pd.concat(weighted_dfs, axis=1)
    result = stacked.T.groupby(level=0).sum(min_count=1).T
    return result

1.2 代码分析

1)应用场景

该计算常用于量化多因子选股。

例如,每个交易日使用过去一段时间的ICIR(IC 均值/IC 标准差)作为权重,

且权重随交易日滚动更新(避免未来信息)。

2) 输入参数

normalized_factors:

Dictstr, pd.DataFrame,键为因子名称,值为DataFrame,索引为日期,列为股票代码,

值为已标准化(通常为截面 Z-score 或排序分位数)的因子暴露。

各因子需对齐到相同日期和股票范围。

weight_matrix:

pd.DataFrame,索引为日期,列为因子名称,值为该日期的因子权重,

例如来自滚动ICIR权重的输出,其列名应与normalized_factors的键匹配。

3)输出形式

pd.DataFrame,是日期×股票的综合得分,索引为日期datetime对应权重矩阵的行索引,

列为股票代码,值为加权求和后的综合得分。

2 核心计算分析

2.1 核心算法

这里通过步骤分解示例

1)因子名称对齐

只取同时存在于weight_matrix列和normalized_factors键中的因子,避免键不匹配。

若没有共同因子则报错。

复制代码
names = [n for n in normalized_factors if n in weight_matrix.columns]
if not names:
    raise ValueError("weight_matrix columns do not match normalized_factors keys")
2)权重归一化(绝对值)
复制代码
w = weight_matrix[names].astype(float)
row_abs = w.abs().sum(axis=1)
wn = w.div(row_abs.where(row_abs > 1e-12), axis=0)

w = weight_matrixnames截取对应列。

计算每行(每个日期)的权重绝对值之和 `row_abs = w.abs().sum(axis=1)`。

将每一行的权重除以该行的绝对值总和,得到归一化后的权重wn。

对于row_abs接近 0 的行(≤ 1e-12),

用 where替换为 NaN,使得 wn` 整行为 NaN,表示该日无有效权重。

为什么用绝对值归一化?

1 这保留了权重的正负号,且使得正负权重的相对比例保持不变。

例如,权重为2, -1,绝对值之和为 3,归一化为2/3, -1/3,总和为 1/3(非零),但符号比例保留。这允许因子具有正负贡献,且绝对值总和反映总杠杆强度。

2 若权重全部为正,则归一化后和为 1;若混合正负,和可能接近 0,此时不应退化为等权,故采用绝对值归一化。

3 直接求和对冲(正负抵消)会丢失总信号强度信息,而 L1 归一化保留了权重的方向和相对大小,确保组合得分不会因为正负抵消而异常缩小。

3)加权组合
复制代码
weighted_dfs = [normalized_factors[n].mul(wn[n], axis=0) for n in names]
stacked = pd.concat(weighted_dfs, axis=1)
result = stacked.T.groupby(level=0).sum(min_count=1).T
return result

对每个因子,将normalized_factorsn(日期×股票)逐元素乘以对应的权重列wnn(日期向量),得到该因子的加权贡献weighted_dfs。

将所有加权后的DataFrame横向拼接(pd.concat(axis=1)),形成一个多列的大DataFrame,其中列是因子-日期组合,实际上,由于每个weighted_dfs的列名是股票代码,多个因子会存在重复列名(股票代码),所以concat后会有重复的列名。

然后使用stacked.T.groupby(level=0).sum(min_count=1).T

stacked.T.groupby(level=0).sum(min_count=1).T

计算详情过程如下,其实就是股票进行聚合。

  • stacked.T将原 DataFrame 转置,使得行变为股票,列变为因子

  • weighted_dfs中的每个 DataFrame 的列都是相同的股票代码,concat(axis=1)会生成一个具有重复列名的 DataFrame(列名是股票代码,但每个因子一个副本)。此时 stacked.T 转置后,行是股票代码(重复),列是日期

  • weighted_dfs列表中的每个 DataFrame 形状为 (n_dates, n_stocks)。

  • pd.concat(weighted_dfs, axis=1)将按列拼接,如果所有 DataFrame 的列索引(股票)相同,那么拼接后列索引将是重复的(例如股票A出现多次)。此时stacked的列索引不是 MultiIndex,而是普通的 Index 包含重复值。

  • 然后stacked.T转置,行索引变为原来的列(股票,包含重复),列索引变为原来的行索引(日期)。

  • 接着.groupby(level=0).sum(min_count=1):groupby(level=0)意味着按行索引的第一级分组(行索引是转置前的列名,即股票代码)。由于重复的股票名会被分为一组,sum会对每个股票下所有因子(对应不同列)的加权值求和,min_count=1表示若该股票在该日期至少有一个非 NaN 的加权值,则求和,否则结果为 NaN。这实际上是对每个日期和每个股票,将所有因子的加权贡献相加。

  • 最后 .T转置回来,恢复为 (日期 × 股票) 的格式。

4)注意事项

需要注意的是,

groupby(level=0).sum(min_count=1)中min_count=1表示在分组求和时,

若该组内所有值均为 NaN,则结果为 NaN;否则忽略 NaN 并求和(只对非 NaN 加总)。

这也是合理的,对于某股票某日期,只要至少有一个因子有值,就计算加权和;

若全部缺失,则结果为 NaN。

2.2 设计细节

1)权重来源&归一化

没有用全样本区间固定计算ICIR(可能引入未来信息) ,而是仅使用过去信息滚动计算(无未来偏差)

采用绝对值归一化,确保正负因子方向的保留,且使得权重总和可正可负。这与常见的"等权"或"市值加权"不同,更强调因子贡献的绝对值比例。

2)处理缺失值

在求和时使用 `min_count=1`,避免因少数因子缺失导致结果全为 NaN,但同时要求至少有一个有效因子。

3)时间对齐

weight_matrix和normalized_factors的日期索引必须匹配(或至少normalized_factors包含 weight_matrix的日期)。若某日期权重全 NaN,则结果全为 NaN(因为wn整行 NaN,加权后所有因子贡献全 NaN,最终求和也 NaN)。

4)计算场景

不是一次计算全局不变,而是每个交易日重新计算权重矩阵的行。

回测中避免前视,更符合真实交易场景,回测结果可信度更高。

5)潜在优化

对于大数据集,concat+groupby可能效率较低,可改用循环累加或使用pd.concat(..., keys=...)` 后按第一级分组求和。

2.3 边缘情况

1)所有权重为 0 或 NaN(热身期)

row_abs为 0 或NaN,wn全部变为NaN。加权因子全为 NaN,最终结果行全为NaN。调用方需自行裁剪(文档中已提示)。 |

2)某只股票在某个因子中缺失(NaN)|

该因子对该股票的贡献被忽略,但仍使用其他因子求和(因为 `min_count=1)。

3)某只股票在所有因子中均缺失

该股票当日最终结果为NaN。

4)权重矩阵列名与因子字典键不完全匹配

仅匹配的部分参与合成,其余忽略,不报错,除非一个都没匹配到,则抛出 ValueError。 |

5)日期索引未对齐

mul(axis=0)会按索引对齐,若日期不一致,会出现大量NaN或数据错位。这是致命隐患,调用者必须提前确保日期完全一致。

reference


滚动ICIR权重分析的探索和代码示例

https://blog.csdn.net/liliang199/article/details/164478989

相关推荐
带多刺的玫瑰3 小时前
Leecode#26刷题之删除有序数组中的重复项
数据结构·算法·leetcode
en.en..3 小时前
C语言核心解析:#define与typedef本质区别
开发语言·c++·算法
wabs6666 小时前
关于二叉树【429.N叉树的层序遍历的思考】
数据结构·c++·算法·leetcode·二叉树
hetao17338376 小时前
2026-09-08 hetao1733837 的刷题记录
c++·算法
C++ 老炮儿的技术栈6 小时前
MFC CPtrArray的用法
开发语言·数据结构·c++·算法·mfc·c
weixin_446260856 小时前
CABAL:用于追踪同行评审中合谋投标影响的多智能体仿真框架
人工智能·算法·机器学习
不会就选b6 小时前
算法日常・每日刷题--<贪心>6
数据结构·算法·leetcode
青山木7 小时前
Hot 100 --- 跳跃游戏 II
java·数据结构·算法·leetcode·贪心算法
Tisfy7 小时前
LeetCode 3870.统计范围内的逗号:模拟 或 一步计算
数学·算法·leetcode·题解·模拟·遍历
明月_清风7 小时前
字符串匹配四大经典算法:BF、RK、BM、KMP 到底有什么区别?
后端·算法