【Python量化因子实战 #08】因子去重:相关性高的因子留哪个?带你跑完相关性矩阵

做多因子选股时,最怕的不是因子太少,而是因子太像。

动量和成交额、波动率和非流动性、换手率和振幅……这些因子往往你中有我。如果把高相关因子都放进模型,等于给同一个信息重复投票,回测好看,实盘容易崩。

本文构造 4 个常见量价因子,跑一遍相关性矩阵,再按“高相关剔除、保留 |IC| 更高者”的规则做去重。

一、为什么要做因子去重

想象你同时用了两个因子:

  • mom_60d:60 日动量。
  • turnover_20d:20 日平均成交额。

如果二者相关系数高达 0.7,说明它们在很多股票上的排名是同步的。把它们同时加权,动量信息被算了两次,模型的“有效独立因子”其实只有 3 个。

去重的目标:让最终保留的因子尽量低相关、尽量各自携带独立预测信息。

二、本文你将得到什么

  • 4 个可复现的量价因子构造方法。
  • 相关性矩阵 + 迭代剔除代码。
  • 去重前后综合打分的效果对比。
  • 一个可直接替换证书运行的完整脚本。

三、4 个待选因子

因子 计算方式 经济含义
mom 60 日收益率(跳过最近 5 日) 趋势动量
vol 20 日年化波动率 风险/波动
turnover 20 日平均成交额(v * c) 交易活跃度
amihud mean(|ret| / 成交额) * 1e9 非流动性冲击

vol、turnover 是“越低越好”,合成时取负号;amihud 越高代表流动性越差,通常预期收益越高。

四、可运行代码

import pandas as pd
import numpy as np
from scipy.stats import spearmanr
from mairui import Client

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")
UNIVERSE = ["600519", "000001", "000002", "600036", "601318",
"601398", "600276", "000333", "600030", "601166"]

def momentum(s):
return s.iloc[-6] / s.iloc[-66] - 1

def volatility(s):
return s.pct_change().tail(20).std() * np.sqrt(252)

def turnover(df):
return (df["v"] * df["c"]).tail(20).mean()

def amihud(df):
ret = df["c"].pct_change().abs().tail(20).values
dv = (df["v"] * df["c"]).tail(20).values
return np.mean(ret / np.maximum(dv, 1)) * 1e9

def zscore(x):
return (x - x.mean()) / x.std()

rows = []
for code in UNIVERSE:
raw = client.stock_history(code, lt=120, dividend="n")
df = pd.DataFrame(raw).sort_values("t")
rows.append({
"code": code,
"mom": momentum(df["c"]),
"vol": volatility(df["c"]),
"turnover": turnover(df),
"amihud": amihud(df),
"fwd": df["c"].iloc[-1] / df["c"].iloc[-21] - 1,
})
snap = pd.DataFrame(rows).dropna()

定向:低波/低换手更好,amihud 越高越好

snap["mom_dir"] = snap["mom"]
snap["vol_dir"] = -snap["vol"]
snap["turn_dir"] = -snap["turnover"]
snap["amihud_dir"] = snap["amihud"]

factors = ["mom_dir", "vol_dir", "turn_dir", "amihud_dir"]
print("相关性矩阵:")
print(snap[factors].corr().abs().round(3))

计算 IC

ics = {f: spearmanr(snap[f], snap["fwd"])[0] for f in factors}

迭代剔除 |r| > 0.7 的因子对,保留 |IC| 高者

keep = list(factors)
threshold = 0.7
while True:
corr = snap[keep].corr().abs()
mask = np.triu(np.ones(corr.shape), k=1).astype(bool)
vmax = corr.where(mask).max().max()
if vmax <= threshold:
break
i, j = np.where(corr.values == vmax)
f1, f2 = corr.index[int(i[0])], corr.columns[int(j[0])]
drop = f1 if abs(ics[f1]) < abs(ics[f2]) else f2
keep.remove(drop)
print(f"剔除 {drop}(与 {f1 if drop == f2 else f2} |r|={vmax:.3f})")

print("保留因子:", keep)

IC 加权合成

abs_ic = pd.Series({f: abs(ics[f]) for f in keep})
weights = (abs_ic / abs_ic.sum()).to_dict()
snap["score"] = sum(weights[f] * zscore(snap[f]) for f in keep)

top5 = snap.nlargest(5, "score")["fwd"].mean() * 100
bot5 = snap.nsmallest(5, "score")["fwd"].mean() * 100
print(f"top5={top5:+.2f}% bot5={bot5:+.2f}% spread={top5-bot5:+.2f}%")

五、运行结果示例

相关性矩阵:
mom_dir vol_dir turn_dir amihud_dir
mom_dir 1.000 0.004 0.715 0.300
vol_dir 0.004 1.000 0.184 0.552
turn_dir 0.715 0.184 1.000 0.695
amihud_dir 0.300 0.552 0.695 1.000

剔除 turn_dir(与 mom_dir |r|=0.715)
保留因子: ['mom_dir', 'vol_dir', 'amihud_dir']
top5=+2.25% bot5=+0.81% spread=+1.44%

本例中 turnover 与 mom 相关性最高,且 |IC| 不如 mom,因此被剔除。剩下的 3 个因子相关性更低,合成后的多头组合跑赢了空头组合。

六、常见坑

  1. 阈值不是越小越好:0.7 是常用起点;阈值降到 0.5 可能误杀有用因子。
  2. 剔除顺序会影响结果:建议按“最大相关系数优先”迭代,而不是一次性全删。
  3. IC 是单期截面指标:去重只解决共线性问题,不能替代样本外验证。

七、小结

  • 相关性矩阵是多因子组合的“体检表”,上线前必看。
  • 去重规则可以简化为:|r| > 阈值时,保留 |IC| 更高的因子。
  • 去重后再做加权,组合的稳定性和可解释性都会更好。

下一篇:因子持久化 —— 把因子落进 SQLite,给下游策略调用。


免责声明

本文仅供技术学习与交流,不构成投资建议。文中示例数据不代表实时行情,投资有风险,入市需谨慎。

代码与文档

源码与接口文档见:

https://github.com/MaiRuiApi

posted @ 2026-09-28 16:26  量化分析码农  阅读(11)  评论(0)    收藏  举报