4、单因子构建方法:截面排序法、分位数分组法、回归法(Fama-MacBeth)、因子值的标准化与中性化处理

聊到单因子构建,我脑子里第一个蹦出来的念头就是——这活儿看着简单,做起来全是坑。

你想想看,我们做量化投资,第一步就是找到能预测收益的因子。但因子本身是原始数据,不能直接用。你得把它加工成能放进模型里的样子。这个过程,说白了就是单因子构建。

我个人习惯把单因子构建分成四个层次:排序、分组、回归、以及最后的清洗。咱们一个一个来拆。

4.1 截面排序法:最直观的因子处理

截面排序法,是所有方法里最朴素的。你拿到一个因子,比如市盈率PE,在每个时间截面上,把所有股票按PE从小到大排个序。然后呢?排名本身就是因子值。

为什么这么做?因为原始因子值可能分布很歪。比如有些股票PE是负的,有些是几千倍。直接拿原始值做回归,结果会被极端值牵着鼻子走。排序之后,所有股票都变成了1到N的整数,干净利落。

核心要点:
  • 排序方向要明确:是越大越好,还是越小越好?
  • 通常用升序排列,排名越小代表因子值越小
  • 如果因子是负向指标(比如PE越高越差),记得取倒数或者反向排序

我在项目中遇到过一个问题:有些股票因子值完全相同,比如停牌股。这时候排序会出现并列排名。处理方式有两种——要么取平均排名,要么随机打乱。我个人建议用平均排名,更稳定。

# Python示例:截面排序法
import pandas as pd
import numpy as np

def cross_sectional_rank(factor_df, date_col='date', asset_col='asset', factor_col='factor'):
    """
    截面排序法:每个时间截面对因子进行排序
    """
    df = factor_df.copy()
    # 按日期分组,对因子值进行排序
    df['rank'] = df.groupby(date_col)[factor_col].rank(method='average')
    # 可选:将排名归一化到[0,1]区间
    df['rank_pct'] = df.groupby(date_col)['rank'].transform(
        lambda x: (x - 1) / (len(x) - 1)
    )
    return df

4.2 分位数分组法:从排序到分组

排序法虽然简单,但有个问题:排名是连续的,你很难说排名第100和第101的股票到底有多大区别。这时候,分位数分组法就派上用场了。

说白了,就是把所有股票按因子值分成若干组。最常用的是五分组(quintile)和十分组(decile)。每组内的股票被视为「同类」,组间的差异才是我们关注的。

我的经验: 分组数不是越多越好。5组通常够用,10组已经很多了。分组太多,每组样本量太少,统计结果不稳定。我曾经试过20分组,结果回测曲线跟心电图似的,根本没法用。

分组之后,我们通常会做两件事:

  • 多空组合收益:做多最高分组,做空最低分组,看这个多空组合的收益曲线
  • 单调性检验:从第1组到第5组,收益是不是单调递增或递减?如果不是,说明因子可能有问题
# Python示例:分位数分组法
def quantile_grouping(factor_df, n_groups=5, date_col='date', factor_col='factor'):
    """
    分位数分组法:每个截面分成n_groups组
    """
    df = factor_df.copy()
    df['group'] = df.groupby(date_col)[factor_col].transform(
        lambda x: pd.qcut(x, q=n_groups, labels=False, duplicates='drop')
    )
    # 注意:duplicates='drop' 处理因子值相同的情况
    return df

4.3 回归法(Fama-MacBeth):从分组到回归

分组法虽然直观,但它有个硬伤:你没法控制其他因子的影响。比如你发现低PE的股票收益好,但低PE的股票可能同时是小市值股票。到底是PE的作用,还是市值的作用?

这时候,Fama-MacBeth回归法就登场了。这个方法分两步走:

  1. 第一步(截面回归):在每个时间点t,用当期因子对下一期收益做截面回归。得到每个因子在每个时间点的系数。
  2. 第二步(时间序列平均):把所有时间点的系数取平均,做t检验,看系数是否显著不为零。

为什么这个方法好?因为它同时解决了两个问题:

  • 截面回归可以加入多个因子,实现「控制变量」的效果
  • 时间序列平均和t检验,给出了统计显著性,而不是拍脑袋说「这个因子有效」
注意: Fama-MacBeth回归的t检验需要做Newey-West调整。因为不同时间点的回归系数可能存在自相关。我刚开始做的时候没注意这个,结果t值虚高,以为找到了超级因子,后来发现是伪回归。嗯,这个坑我替你们踩过了。
# Python示例:Fama-MacBeth回归(简化版)
import statsmodels.api as sm

def fama_macbeth_regression(panel_data, factor_cols, return_col='ret_1m'):
    """
    Fama-MacBeth两步法
    panel_data: 面板数据,包含日期、股票、因子、未来收益
    """
    dates = panel_data['date'].unique()
    coef_list = []
    
    # 第一步:每个时间点做截面回归
    for date in dates:
        cross_section = panel_data[panel_data['date'] == date].dropna()
        if len(cross_section) < 30:  # 样本太少就跳过
            continue
        X = sm.add_constant(cross_section[factor_cols])
        y = cross_section[return_col]
        model = sm.OLS(y, X).fit()
        coef_list.append(model.params)
    
    # 第二步:时间序列平均
    coef_df = pd.DataFrame(coef_list)
    results = {}
    for col in coef_df.columns:
        mean_coef = coef_df[col].mean()
        t_stat = coef_df[col].mean() / (coef_df[col].std() / np.sqrt(len(coef_df)))
        results[col] = {'mean': mean_coef, 't_stat': t_stat}
    
    return results

4.4 因子值的标准化与中性化处理

因子构建的最后一步,也是最容易被忽视的一步:标准化和中性化。

4.4.1 标准化:让因子站在同一起跑线

不同因子的量纲不一样。PE是几十到几百,换手率是百分之几,市值是几十亿到几万亿。如果不做标准化,你没法比较哪个因子「更有效」。

最常用的标准化方法是Z-score:

# Z-score标准化
def zscore_standardize(factor_series):
    return (factor_series - factor_series.mean()) / factor_series.std()

标准化之后,所有因子的均值都是0,标准差都是1。这样你就能直接比较回归系数的大小了。

4.4.2 中性化:剔除你不想要的「杂质」

中性化处理,是量化里最见功力的地方。说白了,就是剔除因子中你不想要的那部分信息。

最常见的两种中性化:

  • 市值中性化:很多因子天然和市值相关。比如低波动因子,大市值股票波动本来就小。如果你不做市值中性化,你分不清收益是来自低波动还是大市值。
  • 行业中性化:不同行业的因子分布差异很大。比如银行股的PE天然就低,科技股的PE天然就高。不做行业中性化,低PE因子可能只是在选行业,而不是在选股票。
中性化的做法: 用因子值对市值(或行业哑变量)做回归,取残差作为中性化后的因子值。
# 市值中性化示例
def market_cap_neutralize(factor_series, log_market_cap):
    """
    对因子做市值中性化
    回归:factor ~ log_market_cap,取残差
    """
    X = sm.add_constant(log_market_cap)
    model = sm.OLS(factor_series, X).fit()
    residual = model.resid
    return residual

# 行业中性化示例
def industry_neutralize(factor_series, industry_dummies):
    """
    对因子做行业中性化
    回归:factor ~ industry_dummies,取残差
    """
    X = sm.add_constant(industry_dummies)
    model = sm.OLS(factor_series, X).fit()
    residual = model.resid
    return residual

4.5 本章知识体系总览

说了这么多,我画了一张图帮你理清思路。单因子构建的四个方法,其实是一个从简单到复杂、从粗糙到精细的递进关系。

单因子构建方法知识体系 截面排序法 分位数分组法 Fama-MacBeth回归 标准化与中性化 排名代替原始值 处理极端值 5组/10组分组 多空组合检验 单调性检验 截面回归(第一步) 时间序列平均(第二步) Newey-West t检验 Z-score标准化 市值中性化 行业中性化 核心逻辑 原始因子 → 排序/分组/回归 → 标准化 → 中性化 → 干净因子 每一步都在剔除噪声,保留真正的预测信息

这张图里,从左到右代表方法的复杂度递增。截面排序法最简单,但信息损失也最大。Fama-MacBeth回归最复杂,但能控制多个变量。标准化和中性化是最后的「清洗」步骤,任何因子在进入模型之前都应该过这一关。

我的建议: 实际工作中,我通常这样组合使用:
  • 初筛阶段:用截面排序法快速看因子方向
  • 验证阶段:用分位数分组法做多空组合检验
  • 精细建模:用Fama-MacBeth回归做多因子分析
  • 最终清洗:标准化 + 市值/行业中性化
这套流程走下来,基本能筛掉80%的伪因子。

嗯,单因子构建这部分就聊到这儿。记住一句话:因子构建不是目的,找到真正能预测收益的信号才是。所有的方法和技巧,都是为了让信号更干净、更可靠。

专注资料整理