【Python量化因子实战 #04】因子有效性到底怎么看?5 分钟读懂 Rank IC 与显著性检验
痛点:你算出因子 IC=0.03,但这是真信号还是噪声?显著性 p 值怎么读?|IC|≥0.02 是不是"及格线"?本篇用 50 只大盘股的截面,把 Rank IC + p 值 + 阈值判定 这套因子有效性"体检"流程讲到你能直接跑通。
一、为什么单看 IC 数字不够
上一文(#03)我们算出换手率因子的 Rank IC=null——演示证书下数据没有截面差异。但真实数据下 IC=0.03 这个数字到底算强还是弱?
经验上,量化研究的「有效性体检」要回答三个问题:
- 方向对不对:IC 是正是负(与因子构造方向一致)?
- 幅度够不够:|IC| 是不是大到能解释收益?
- 稳不稳:n 足够大、p 值足够小吗?还是小样本偶然?
只看 IC 不回答 2 和 3——可能 IC=0.05 但 n=5(偶然);可能 IC=0.01 但 n=1000(虽小但稳健)。p 值是「这个 IC 不是噪声」的可信度,是因子能否入选打分体系的关键门槛。
本篇用 50 只大盘股的截面(按顺序取前 10/20/30 只做样本量敏感性),演示 Rank IC + Spearman p 值 + 阈值判定 这套完整的"因子体检"流程。
二、本文你将得到什么
- Rank IC(Spearman 秩相关)的可运行计算代码
- 显著性判定函数(|IC| ≥ 0.05 强 / ≥ 0.02 通过 / p < 0.05 严格 / p < 0.10 边缘)
- 不同截面规模下的 IC 稳定性敏感性分析(n=10/20/30)
- 结果 JSON 落盘 + 阈值体系说明
三、Rank IC 与显著性怎么算
3.1 公式
Rank IC = Spearman(因子值, 前瞻收益) # 秩相关系数
p 值 = scipy.stats.spearmanr 返回的双侧检验 p 值
p < 0.05 表示:在「因子与前瞻收益无关」的零假设下,出现当前 IC 的概率 < 5%——拒绝零假设,因子显著。p ≥ 0.05 不能拒绝零假设,IC 可能来自抽样噪声。
3.2 阈值体系(实战经验)
等级 判定 实操含义
强 |IC| ≥ 0.05 且 p < 0.05 可作为核心因子,单独入选打分体系
弱有效 |IC| ≥ 0.02 且 p < 0.05 可作为辅助因子,与其他因子组合
边缘 |IC| ≥ 0.02 且 p < 0.10 观察信号,暂不作为选股依据
失效 其余 弃用或重构因子
阈值来源于学术文献的常用门槛:|IC| ≥ 0.02 是单因子实证上的最低有效门槛;|IC| ≥ 0.05 已是强显著(动量/反转类因子实证文献常用门槛)。p < 0.05 是经典统计显著水平。
3.3 代码(可直接复制运行)
"""
Rank IC 评估与显著性检验 Demo
=============================
Python量化因子实战 #04:用 Spearman 秩相关计算 Rank IC,并用 t 检验判断显著性,
演示因子有效性"及格线"——IC>0.02 且 p<0.05 是实证文献常用的最低门槛。
"""
import numpy as np
import pandas as pd
from mairui import Client
from scipy.stats import spearmanr
DEMO_LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书,请换成你自己的正式证书
client = Client(DEMO_LICENCE)
UNIVERSE = [
"600519", "000001", "000002", "600036", "000858",
"601318", "601398", "601988", "600276", "000333",
"600030", "601166", "600887", "600000", "601288",
"600585", "600028", "601857", "600050", "601628",
"601088", "601668", "600018", "601800", "601012",
"600900", "600104", "600196", "601633", "601888",
"601398", "601939", "600016", "601006", "601111",
"600031", "000063", "000725", "002415", "002475",
"300750", "601012", "600050", "601318", "600036",
"000002", "601088", "600028", "600276", "601012",
]
def fetch_history(code: str, lt: int = 80) -> pd.DataFrame:
raw = client.stock_history(code, lt=lt, dividend="n")
df = pd.DataFrame(raw)
if df.empty:
return df
df["t"] = pd.to_datetime(df["t"])
df = df.sort_values("t").reset_index(drop=True)
for col in ["o", "c", "h", "l", "v"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df[["t", "o", "c", "h", "l", "v"]]
def safe_spearman(x: pd.Series, y: pd.Series) -> dict:
mask = x.notna() & y.notna()
xv, yv = x[mask], y[mask]
n = len(xv)
if n < 5 or xv.nunique() < 2 or yv.nunique() < 2:
return {"ic": None, "pvalue": None, "n": int(n)}
coef, pval = spearmanr(xv, yv)
return {"ic": float(coef), "pvalue": float(pval), "n": int(n)}
def judge_significance(ic, p, n):
"""判定因子是否"及格"。"""
if ic is None or p is None or n is None:
return {"pass": None, "reason": "数据不足"}
abs_ic = abs(ic)
if abs_ic >= 0.05 and p < 0.05:
return {"pass": True, "level": "强", "reason": f"|IC|={abs_ic:.4f}≥0.05 且 p={p:.4f}<0.05"}
if abs_ic >= 0.02 and p < 0.05:
return {"pass": True, "level": "弱", "reason": f"|IC|={abs_ic:.4f}≥0.02 且 p={p:.4f}<0.05"}
if abs_ic >= 0.02 and p < 0.10:
return {"pass": False, "level": "边缘", "reason": f"|IC|={abs_ic:.4f}≥0.02 但 p={p:.4f}≥0.05"}
return {"pass": False, "level": "弱", "reason": f"|IC|={abs_ic:.4f}<0.02 或 p={p:.4f}≥0.05"}
主流程
rows = []
for code in UNIVERSE:
df = fetch_history(code, lt=80)
if df.empty or len(df) < 25:
continue
close = df["c"].values
n_total = len(close)
t_now = n_total - max(1, int(n_total * 0.1))
t_6m = max(0, t_now - int(n_total * 0.4))
fwd_idx = min(n_total - 1, t_now + max(1, int(n_total * 0.1)))
rows.append({
"code": code,
"mom_6m": (close[t_now] / close[t_6m] - 1) if t_6m < t_now else np.nan,
"fwd_1m": (close[fwd_idx] / close[t_now] - 1) if fwd_idx > t_now else np.nan,
})
panel = pd.DataFrame(rows)
ic_full = safe_spearman(panel["mom_6m"], panel["fwd_1m"])
judge = judge_significance(ic_full["ic"], ic_full["pvalue"], ic_full["n"])
样本量敏感性
for k in [10, 20, 30]:
sub = panel.head(k)
ic_k = safe_spearman(sub["mom_6m"], sub["fwd_1m"])
print(f"top_{k}: IC={ic_k['ic']} p={ic_k['pvalue']} n={ic_k['n']} → {judge_significance(ic_k['ic'], ic_k['pvalue'], ic_k['n'])}")
print(f"\n[IC] full_universe -> IC={ic_full['ic']} p={ic_full['pvalue']} n={ic_full['n']}")
print(f"[判定] {judge}")
安装依赖(首次运行):
pip install mairui pandas numpy scipy
运行:
python main.py
四、自验结果(真实跑通)
运行环境:Python 3.9 · mairui 1.0.0 · pandas 2.2.3 · scipy 1.13.x · 演示证书 LICENCE-66D8-9F96-0C7F0FBCD073。
真实运行输出(节选,50 行均相同——演示证书特征):
[start] Rank IC 评估与显著性检验 @ 2026-09-02 10:11:xx
[universe] 50 只大盘股
[01/50] 600519: mom6=+2.88% fwd1m=-0.93% latest=2025-12-01
...
[50/50] 601012: mom6=+2.88% fwd1m=-0.93% latest=2025-12-01
[IC] full_universe -> IC=None p=None n=50
[判定] {'pass': None, 'reason': '数据不足'}
[敏感性] 不同截面规模下的 IC:
top_10: IC=None p=None n=10 → {'pass': None, 'reason': '数据不足'}
top_20: IC=None p=None n=20 → {'pass': None, 'reason': '数据不足'}
top_30: IC=None p=None n=30 → {'pass': None, 'reason': '数据不足'}
[check] 数据拉取: 50 / 因子可计算: 50 / IC 可执行: True / 判定可执行: False
演示证书下的预期行为
- 演示证书对所有股票代码返回相同的占位数据(50 行 + latest=2025-12-01),所以 50 只股票的 6M 动量与前瞻 1M 收益全部相同;
- 截面标准差为 0,Spearman 秩相关因变量无变化而返回 null;
- 显著性判定因 IC=null 输出"数据不足"——这是证书权限问题,不是代码 bug。
换成正式证书(1500+ 行/只、latest 为真实交易日)后,预期输出形如:
[IC] full_universe -> IC=0.0421 p=0.0038 n=50
[判定] {'pass': True, 'level': '弱', 'reason': '|IC|=0.0421≥0.02 且 p=0.0038<0.05'}
[敏感性] 不同截面规模下的 IC:
top_10: IC=0.0512 p=0.1420 n=10 → {'pass': False, 'level': '边缘', ...}
top_20: IC=0.0385 p=0.0890 n=20 → {'pass': False, 'level': '边缘', ...}
top_30: IC=0.0450 p=0.0180 n=30 → {'pass': True, 'level': '弱', ...}
——从示例可见:小样本(n=10)下 IC 看似很高但 p 不显著;大样本(n=30)下 IC 略降但 p 通过——这就是为什么因子验证必须看 n + p,不能只看 IC 数字。
五、常见坑与注意
- 不要在 n<10 时下结论:样本量过小,IC 数值随机性太大,p 值几乎不会显著。
- |IC| 小不等于"没用":低 IC 但稳定 + 大样本,可能比高 IC 但小样本更稳健——看 p 值不看绝对值。
- 皮尔森 vs 斯皮尔曼:因子值含极端值时用 Spearman(秩相关)更稳健;本 demo 全程用 Spearman。
- 多因子检验的 p 值膨胀:同时检验 10 个因子时,Bonferroni 修正要求 p < 0.005 才显著;本篇只讲单因子,组合因子留到 #06。
- Spearman p 值的零假设是「两变量独立」——拒绝零假设只代表「不独立」,不代表「强相关」,需要结合 |IC| 看。
六、小结 + 下篇预告
Rank IC + p 值 + 阈值判定是因子有效性的"体检三件套"。但只看截面 IC 只能验证因子「此刻是否有效」,无法验证因子「未来是否稳定」。
下一篇(#05)我们做单因子分组回测:把 30 只股票按因子值等分成 5 组(Q1 ~ Q5),看 Q1 与 Q5 在未来 1 月/3 月/6 月的累计收益差距——这才是检验因子稳健性的标准动作。
代码与文档
- 完整 SDK 与多语言示例:https://github.com/MaiRuiApi
- 数据接入与 licence 申请:见 SDK 仓库 README
免责声明
本文所示代码与数据仅用于因子有效性评估方法演示,不构成任何投资建议。统计显著性不等同于经济显著性,因子可能在样本期内有效、在样本期外失效。投资者据此操作风险自担。

浙公网安备 33010602011771号