阅读提示

这篇讲解因子预处理的三个核心步骤——去极值(Winsorize)、标准化(Standardize)、中性化(Neutralize)。如果你发现自己的因子 IC 很高但回测收益很差,通常是预处理出了问题。

导言

在金融数据里,极值、数据量纲差异、行业偏差是最常见的三类”脏数据”问题:

  • 极值:某只股票某天换手率突然暴增 100 倍(新股上市、乌龙指),如果不处理,它会主导整个因子值。
  • 量纲差异:市值(百万万亿)和股息率(0%10%)的数值范围差了几个数量级,直接拿来分组会导致因子被量纲大的变量绑架。
  • 行业偏差:银行股的市净率和科技股的市净率不在同一个尺度上,直接比较没有意义。

preprocess_factor 把这三步打包成了一行代码,但理解每一步在做什么,有助于你在遇到问题时有针对性地调整。

一、去极值(Winsorize)

为什么需要去极值

金融数据里的极端值(outlier)来源广泛:财务报表更正、股票简称变更、数据采集错误、真实的极端事件。去极值的目的是把超出合理范围的数值裁剪到某个分位点,防止极端值主导因子。

三种去极值方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from betalens.factor.preprocessing import preprocess_factor, winsorize_factor

# 方法一:直接用 winsorize_factor(底层函数)
from scipy.stats import mstats

# MAD 法(Median Absolute Deviation):中位数±n倍MAD以外的数值裁剪
cleaned = winsorize_factor(
raw,
metric="换手率(%)",
method="mad",
n_mad=3.0, # 中位数±3倍MAD以外的值裁剪
)

# 百分位法:头尾各裁剪1%
cleaned = winsorize_factor(
raw,
metric="换手率(%)",
method="percentile",
lower=0.01, upper=0.99,
)

# STD 法:均值±n倍标准差以外裁剪
cleaned = winsorize_factor(
raw,
metric="换手率(%)",
method="std",
n_std=3.0,
)

通过 preprocess_factor 一键完成

1
2
3
4
5
6
7
8
from betalens.factor.preprocessing import preprocess_factor

cleaned = preprocess_factor(
raw,
metric="换手率(%)",
winsorize_method="mad", # 或 "percentile" / "std"
n_mad=3.0, # MAD 参数
)

preprocess_factor 会**逐截面(每天)**做去极值,确保不泄露未来数据。

二、标准化(Standardize)

为什么需要标准化

不同因子的数值范围差异巨大:市值从几百万到几万亿,换手率从 0 到 100+,股息率从 0 到 20+。如果不标准化,在多因子模型里量纲大的因子权重会天然偏大。

三种标准化方法

1
2
3
4
5
6
7
8
9
10
from betalens.factor.preprocessing import standardize_factor

# Z-score:减均值除标准差(默认)
z_scored = standardize_factor(cleaned, metric="换手率(%)", method="zscore")

# Rank:截面排名归一化(0~1)
ranked = standardize_factor(cleaned, metric="换手率(%)", method="rank")

# MinMax:归一化到 0~1
minmax = standardize_factor(cleaned, metric="换手率(%)", method="minmax")

学术研究里常用 Rank 标准化,因为它不假设因子分布形态,对极端值也更稳健。

preprocess_factor 中一键完成

1
2
3
4
5
6
cleaned = preprocess_factor(
raw,
metric="股息率(报告期)",
winsorize_method="mad",
standardize_method="zscore", # 或 "rank" / "minmax"
)

三、中性化(Neutralize)

为什么需要中性化

假设你有一个”高ROE → 股价上涨”的因子逻辑。但事实是:大市值股票的 ROE 通常更稳定,小市值股票的 ROE 波动更大。如果不做中性化,你的因子本质上可能只是在做”小市值暴露”——不是 ROE 的能力,而是市值因子的代理。

中性化就是把目标因子对其他因子(如市值、行业)做 OLS 回归,取残差,让你只保留”不被其他因素解释”的纯粹部分。

行业中性化

1
2
3
4
5
6
7
cleaned = preprocess_factor(
raw,
metric="ROE(报告期)",
winsorize_method="mad",
standardize_method="zscore",
industry_scheme="申万一级行业", # 自动 PIT 查询行业标签
)

背后逻辑:

1
2
3
4
5
6
7
8
# 简化版等效代码
for each input_ts:
for each industry:
# 在该行业的股票内做市值回归,取残差
X = log 市值
y = ROE
residuals = y - beta * X
factor_values.loc[industry_mask] = residuals

行业中性化自动做 PIT——框架会在数据库里查每只股票在调仓日属于哪个行业,保证不用未来行业变更信息。

市值中性化

市值中性化需要准备 log 市值列:

1
2
3
4
5
6
7
8
9
10
11
# 先在 raw 数据里加上 log 市值列
raw["log_mktcap"] = np.log(raw["总市值(元)"])

cleaned = preprocess_factor(
raw,
metric="ROE(报告期)",
winsorize_method="mad",
standardize_method="zscore",
use_mktcap=True, # 需要 raw 中有 log 市值列
mktcap_metric="log_mktcap",
)

组合中性化

1
2
3
4
5
6
7
8
from betalens.factor.preprocessing import neutralize_factor_by_factor

# 对 ROE 做市值 + 行业双重中性化
neutralized = neutralize_factor_by_factor(
roe_data,
neutralize_by=["log_mktcap"],
industry_scheme="申万一级行业",
)

完整预处理流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from betalens.factor.preprocessing import preprocess_factor

# 原始数据:raw DataFrame, MultiIndex(input_ts, code), columns 包含因子列
raw = pre_query_characteristic_data(...)

# 一键预处理
cleaned = preprocess_factor(
raw,
metric="ROE(报告期)",
winsorize_method="mad",
n_mad=3.0,
standardize_method="zscore",
industry_scheme="申万一级行业",
use_mktcap=True,
mktcap_metric="log_mktcap",
)

# cleaned 可以直接输入 single_characteristic
labeled = single_characteristic(cleaned, "ROE(报告期)", {"ROE(报告期)": 10})

开发者侧:逐截面处理的实现

从源码角度,preprocess_factor 对每个 input_ts(调仓日)做独立处理,这是保证”不泄露未来数据”的关键:

1
2
3
4
5
6
7
8
9
def preprocess_factor(raw, metric, ...):
results = []
for ts in raw.index.get_level_values('input_ts').unique():
截面 = raw.loc[ts]
截面 = winsorize截面(截面, metric, method, ...)
截面 = standardize截面(截面, metric, method, ...)
截面 = neutralize截面(截面, metric, industry_scheme, ...)
results.append(截面)
return pd.concat(results)

重要:如果你的自定义因子脚本在调用 preprocess_factor 之前做了跨截面汇总(比如算某只股票的 20 日均值),就可能引入未来数据泄露。永远在 preprocess_factor 之后再做聚合,或者用 betalens.factor.factor 提供的滚动窗口函数。

常见错误

1. 去极值参数设反了

1
2
3
4
5
# 错:lower > upper 会报错
winsorize_factor(raw, metric="换手率", method="percentile", lower=0.99, upper=0.01)

# 对:lower < upper
winsorize_factor(raw, metric="换手率", method="percentile", lower=0.01, upper=0.99)

2. 行业中性化但 raw 里没有行业数据

1
2
3
4
5
6
# 报错或行业全是 NaN
cleaned = preprocess_factor(raw, ..., industry_scheme="申万一级行业")

# 检查:raw 里有申万行业列吗?
print(raw.columns)
# 如果没有,需要先通过 query_industry_panel 补上

3. 市值中性化但没有 log 市值列

1
2
3
4
5
6
# 报错:找不到 mktcap_metric 列
cleaned = preprocess_factor(raw, ..., use_mktcap=True)

# 正确:先算 log 市值
raw["log_mktcap"] = np.log(raw["总市值(元)"])
cleaned = preprocess_factor(raw, ..., use_mktcap=True, mktcap_metric="log_mktcap")

4. 多因子中性化顺序不对

正确的顺序是:先去极值 → 再标准化 → 最后中性化。如果先中性化再去极值/标准化,中性化的效果会被后续步骤破坏。

延伸阅读