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日收益率)、波动率因子、成长因子等。获取因子数据时,必须注意时点对齐:因子值必须使用截面期(如每月末、每周末)的已知数据,不能使用未来信息。

因子数据获取与对齐流程 股票池列表 沪深300 / 全A 交易日历对齐 T日截面时间戳 因子数据API PE / PB / 动量 数据清洗与对齐 去极值 / 缺失值 / 停牌 截面因子矩阵 T日 × N只股票 ⚠ 关键对齐规则: 因子值必须使用T日收盘后已知数据(如PE_TTM、过去20日收益率),不能混入T+1日信息。 停牌股票在截面期应保留,但需标记状态,避免未来收益率计算偏差。

获取常见因子数据的代码示例:

# 获取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%的样本需核实是否为复权错误
  • 检查日期对齐:确保所有数据的时间戳精确到交易日
  • 检查停牌标记:停牌股票的未来收益率应为空,不应强行填充