3. 数据准备:获取股票池、因子数据与未来收益率
在进行因子IC分析之前,必须完成三项基础数据准备工作:确定分析标的(股票池)、获取待检验的因子值、以及计算未来N日的收益率。这三者构成了IC计算的数据三角,缺一不可。本章将逐一拆解每个环节的技术要点与常见陷阱。
3.1 股票池的选择与获取
股票池决定了因子分析的样本空间。不同的股票池对应不同的市场风格与风险暴露,选择不当会导致IC分析结果失真。
| 股票池类型 | 样本数量 | 适用场景 | 数据获取频率 |
|---|---|---|---|
| 全A股 | 约4500+ | 全市场选股、宽基因子检验 | 日频/周频 |
| 沪深300 | 300 | 大盘蓝筹因子、指数增强策略 | 日频 |
| 中证500 | 500 | 中盘成长因子、风格轮动 | 日频 |
| 中证1000 | 1000 | 小盘微盘因子、高频策略 | 日频/周频 |
实战建议:初学者建议从沪深300开始,样本量适中(300只),计算速度快,且成分股流动性好,因子数据质量较高。全A股虽然更全面,但需要处理ST股、次新股、停牌股等特殊状态,数据清洗工作量倍增。
获取股票池的典型代码实现(以Python为例):
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta
# 获取沪深300成分股列表(最新)
def get_hs300_stocks(date=None):
if date is None:
date = datetime.now().strftime('%Y%m%d')
df = ak.index_stock_cons_csindex("000300")
df['date'] = pd.to_datetime(date)
return df[['date', '成分券代码', '成分券名称']].rename(
columns={'成分券代码': 'stock_code', '成分券名称': 'stock_name'}
)
# 获取全A股列表(剔除ST、退市)
def get_all_a_stocks(date=None):
if date is None:
date = datetime.now().strftime('%Y%m%d')
df = ak.stock_zh_a_spot_em()
# 过滤ST、退市、北交所
df = df[~df['代码'].str.contains('ST|退|8[0-9]')]
df['date'] = pd.to_datetime(date)
return df[['date', '代码', '名称']].rename(
columns={'代码': 'stock_code', '名称': 'stock_name'}
)
3.2 因子数据的获取与对齐
因子数据是IC分析的核心输入。常见的因子类型包括估值因子(PE、PB)、动量因子(过去N日收益率)、波动率因子、成长因子等。获取因子数据时,必须注意时点对齐:因子值必须使用截面期(如每月末、每周末)的已知数据,不能使用未来信息。
获取常见因子数据的代码示例:
# 获取PE、PB因子(使用akshare)
def get_factor_pe_pb(stock_list, date):
"""获取指定日期截面上的PE、PB数据"""
factor_data = []
for code in stock_list:
try:
df = ak.stock_individual_info_em(symbol=code)
pe = df.loc[df['item'] == '市盈率-动态', 'value'].values[0]
pb = df.loc[df['item'] == '市净率', 'value'].values[0]
factor_data.append({
'stock_code': code,
'date': date,
'PE': float(pe) if pe != '-' else None,
'PB': float(pb) if pb != '-' else None
})
except:
continue
return pd.DataFrame(factor_data)
# 获取动量因子(过去20日收益率)
def get_momentum_factor(stock_list, end_date, window=20):
"""计算过去window个交易日的累计收益率"""
start_date = (pd.to_datetime(end_date) - timedelta(days=window*2)).strftime('%Y%m%d')
momentum_list = []
for code in stock_list:
try:
df = ak.stock_zh_a_hist(symbol=code, period="daily",
start_date=start_date, end_date=end_date, adjust="qfq")
if len(df) >= window:
close = df['收盘'].values[-window:]
ret = (close[-1] / close[0]) - 1
momentum_list.append({'stock_code': code, 'date': end_date, 'Momentum_20d': ret})
except:
continue
return pd.DataFrame(momentum_list)
3.3 未来N日收益率的计算
未来收益率是IC分析的因变量。计算时需注意:收益率窗口必须严格对齐因子截面期,且要处理停牌、退市等特殊情况。常见的窗口期包括5日、10日、20日、60日。
| 窗口名称 | 交易日数 | 对应自然日 | IC分析用途 |
|---|---|---|---|
| 短期窗口 | 5日 | 约1周 | 高频因子、反转因子检验 |
| 中期窗口 | 20日 | 约1个月 | 动量因子、估值因子标准检验 |
| 长期窗口 | 60日 | 约3个月 | 趋势因子、基本面因子验证 |
常见错误:使用未来收益率时,容易忽略“未来数据”的时间边界。例如,在T日截面使用因子值,计算未来20日收益率时,必须使用T+1到T+20的收盘价,不能包含T日当天的收益率。否则会引入前视偏差(look-ahead bias),导致IC虚高。
# 计算未来N日收益率
def compute_future_return(stock_code, trade_date, n_days=20):
"""
计算某只股票在trade_date之后n_days个交易日的累计收益率
返回:未来收益率(小数形式)
"""
start = pd.to_datetime(trade_date)
# 向后多取一些数据,确保有足够交易日
end = (start + timedelta(days=n_days * 2)).strftime('%Y%m%d')
start_str = (start + timedelta(days=1)).strftime('%Y%m%d') # T+1开始
try:
df = ak.stock_zh_a_hist(symbol=stock_code, period="daily",
start_date=start_str, end_date=end, adjust="qfq")
if len(df) >= n_days:
close_start = df['收盘'].iloc[0]
close_end = df['收盘'].iloc[n_days - 1]
future_ret = (close_end / close_start) - 1
return future_ret
else:
return None # 数据不足,返回空值
except:
return None
# 批量计算整个股票池的未来收益率
def batch_future_return(stock_pool_df, n_days=20):
"""输入:stock_pool_df包含stock_code和date列"""
results = []
for _, row in stock_pool_df.iterrows():
ret = compute_future_return(row['stock_code'], row['date'], n_days)
results.append({
'stock_code': row['stock_code'],
'date': row['date'],
f'FutureRet_{n_days}d': ret
})
return pd.DataFrame(results)
3.4 数据合并与最终截面表
将股票池、因子数据、未来收益率合并为一张完整的截面表,是IC分析前的最后一步。合并时以股票代码+日期为唯一键,采用左连接方式,确保因子和收益率一一对应。
最终截面表结构示例:
| stock_code | date | PE | PB | Momentum_20d | FutureRet_20d | is_trading |
|---|---|---|---|---|---|---|
| 000001 | 2024-01-31 | 8.52 | 1.23 | 0.035 | 0.021 | True |
| 000002 | 2024-01-31 | 6.18 | 0.95 | -0.012 | 0.008 | True |
| 000003 | 2024-01-31 | 15.30 | 2.41 | 0.067 | -0.015 | False |
# 合并所有数据为最终截面表
def build_cross_section(stock_pool, factor_df, future_ret_df, date):
"""
合并股票池、因子、未来收益率为一张表
参数:
stock_pool: DataFrame,包含stock_code列
factor_df: DataFrame,包含stock_code和因子列
future_ret_df: DataFrame,包含stock_code和未来收益率列
date: 截面日期
"""
# 左连接:以股票池为主表
merged = stock_pool.merge(factor_df, on='stock_code', how='left')
merged = merged.merge(future_ret_df, on='stock_code', how='left')
# 添加日期列
merged['date'] = date
# 标记交易状态(停牌股未来收益率为空)
merged['is_trading'] = merged['FutureRet_20d'].notna()
# 删除因子或收益率为空的无效行
merged = merged.dropna(subset=['PE', 'PB', 'Momentum_20d', 'FutureRet_20d'])
return merged[['stock_code', 'date', 'PE', 'PB', 'Momentum_20d', 'FutureRet_20d', 'is_trading']]
# 使用示例
hs300 = get_hs300_stocks('20240131')
pe_pb = get_factor_pe_pb(hs300['stock_code'].tolist(), '20240131')
momentum = get_momentum_factor(hs300['stock_code'].tolist(), '20240131', 20)
future_ret = batch_future_return(hs300, 20)
cross_section = build_cross_section(hs300, pe_pb, momentum, future_ret, '20240131')
print(f"截面表构建完成,有效样本数:{len(cross_section)}")
数据质量检查清单:
- 检查因子缺失率:PE/PB缺失率应低于5%,过高需排查数据源
- 检查未来收益率极端值:超过±50%的样本需核实是否为复权错误
- 检查日期对齐:确保所有数据的时间戳精确到交易日
- 检查停牌标记:停牌股票的未来收益率应为空,不应强行填充