问题描述
解决方案
在多因子选股策略中,原始因子数据通常存在量纲不同、包含异常值以及受行业或市值因素主导等问题。为了得到纯净且具有可比性的因子暴露度,通常需要进行去极值(Winsorization)、**标准化(Standardization)和中性化(Neutralization)**处理。
聚宽(JoinQuant)的 jqfactor 库提供了现成的函数来完成这些步骤。
数据处理标准流程
通常的处理顺序为:
- 去极值:剔除或压缩异常数据,防止极端值影响后续统计。
- 标准化:将不同量纲的数据转化为标准正态分布(Z-Score),使其具有可比性。
- 中性化:剔除因子中的行业暴露和市值暴露,获取纯粹的因子Alpha。
核心函数说明
1. 去极值 (Winsorization)
推荐使用 中位数去极值法 (MAD),相比于标准差法(3-sigma),它对异常值更不敏感,更加稳健。
- API:
winsorize_med(data, scale=3, inclusive=True, axis=1) - 原理: 将超过
中位数 ± 3 * 绝对中位差的值替换为边界值。
2. 标准化 (Standardization)
通常使用 Z-Score 标准化。
- API:
standardlize(data, inf2nan=True, axis=1) - 原理: $X_{new} = \frac{X - \mu}{\sigma}$,处理后均值为0,标准差为1。
3. 中性化 (Neutralization)
通过回归分析剔除特定风险因子的影响(通常是行业和市值)。
- API:
neutralize(data, how=['jq_l1', 'market_cap'], date=date, axis=1) - 参数:
how列表指定要剔除的风险因子,jq_l1代表聚宽一级行业,market_cap代表市值。
完整代码示例
以下代码展示了如何获取沪深300成分股的市盈率(PE)因子,并依次进行去极值、标准化和中性化处理。
# -*- coding: utf-8 -*-
from jqdata import *
from jqfactor import get_factor_values, winsorize_med, standardlize, neutralize
import pandas as pd
import numpy as np
def initialize(context):
# 设定基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 每天开盘前运行
run_daily(process_factors, '09:00')
def process_factors(context):
# 1. 设定参数
date = context.previous_date # 使用前一交易日数据,避免未来函数
universe = get_index_stocks('000300.XSHG', date=date) # 获取沪深300股票池
print(f"正在处理 {date} 的因子数据...")
# 2. 获取原始因子数据
# 这里以市盈率(pe_ratio)为例,也可以通过 get_fundamentals 获取
# get_factor_values 返回的是一个字典,key是因子名,value是DataFrame
factor_data_dict = get_factor_values(
securities=universe,
factors=['pe_ratio'],
end_date=date,
count=1
)
# 提取 DataFrame,行是日期,列是股票代码
# 我们只需要最近一天的数据,并转置为 Series (index为股票代码) 以便处理
raw_factor = factor_data_dict['pe_ratio'].iloc[-1, :]
# 过滤掉 NaN 值
raw_factor = raw_factor.dropna()
if raw_factor.empty:
print("无有效因子数据")
return
# --- 数据处理流水线 ---
# 3. 去极值 (Winsorization)
# 使用中位数去极值法,将极值压缩到边界
factor_winsorized = winsorize_med(
raw_factor,
scale=3, # 3倍中位数绝对偏差
inclusive=True, # 边界外的值替换为边界值
inf2nan=True, # 无穷大转为NaN
axis=0 # 对 Series 处理时 axis=0
)
# 4. 标准化 (Standardization)
# Z-Score 标准化,使均值为0,标准差为1
factor_standardized = standardlize(
factor_winsorized,
inf2nan=True,
axis=0
)
# 5. 中性化 (Neutralization)
# 剔除行业(jq_l1)和市值(market_cap)的影响
# 注意:neutralize 需要 date 参数来获取当天的行业和市值数据进行回归
factor_neutralized = neutralize(
factor_standardized,
how=['jq_l1', 'market_cap'],
date=date,
axis=0
)
# --- 打印结果对比 ---
print("\n处理前前5只股票数据:")
print(raw_factor.head())
print("\n去极值后前5只股票数据:")
print(factor_winsorized.head())
print("\n标准化后前5只股票数据:")
print(factor_standardized.head())
print("\n中性化后(最终)前5只股票数据:")
print(factor_neutralized.head())
# 此时 factor_neutralized 即为可用于打分或回归的纯净因子
关键点总结
- 处理顺序:建议严格按照 去极值 -> 标准化 -> 中性化 的顺序进行。如果先做标准化再做去极值,极值会扭曲均值和方差的计算;如果先做中性化,异常值会影响回归系数的估计。
- 数据结构:
jqfactor的处理函数既支持pandas.Series也支持pandas.DataFrame。如果是 DataFrame(多行多列),通常设置axis=1表示在横截面(按天)上对所有股票进行处理。 - 中性化依赖:
neutralize函数内部会自动获取指定日期的行业分类和市值数据,因此必须传入准确的date参数。 - 缺失值:在处理前最好先剔除
NaN值,虽然聚宽的函数有inf2nan等参数,但保持输入数据的整洁可以避免很多潜在错误。
Q&A
Q: 为什么要进行行业和市值中性化?
A: 很多因子(如换手率、波动率)天然与市值大小或特定行业(如银行股通常低PE)相关。如果不进行中性化,选出的股票可能会集中在小市值或某些特定行业,导致组合风险暴露过大。中性化是为了剥离这些风险,保留因子纯粹的选股能力(Alpha)。
Q: winsorize 和 winsorize_med 有什么区别?
A: winsorize 通常指基于标准差(如3-sigma)或分位数(如1%-99%)的去极值;winsorize_med 基于中位数和绝对中位差(MAD)。由于金融数据往往不服从正态分布且存在厚尾,均值和标准差容易受极值影响,因此基于中位数的 winsorize_med 通常更加稳健。
Q: 处理后的因子数据如何使用?
A: 处理后的因子数据可以直接用于多因子打分模型(加权求和)、计算IC值(信息系数)进行因子有效性检验,或者作为输入放入机器学习模型中进行训练。