单因子构建与检验:以市盈率倒数(EP)为例

因子投资的核心,说白了就是找到能预测收益的“信号”。今天我们就拿市盈率倒数(EP)这个经典因子开刀,完整走一遍构建、分组、回测、检验的流程。我个人习惯把这一步叫做“因子体检”——因子好不好,拉出来遛遛就知道。

1. 因子构建:从原始数据到因子值

市盈率倒数,就是Earnings / Price。为什么用倒数?因为市盈率高的股票可能亏损或微利,用倒数后数值分布更平滑,逻辑上也更直观——EP越高,说明每块钱股价对应的盈利越多。

构建步骤其实就三步:

  1. 取数据:每个月底,拿到所有股票的市值(Price * Shares)和净利润(TTM)。
  2. 算因子:EP = 净利润 / 市值。注意净利润要用最近四个季度的滚动值。
  3. 去极端:我习惯用MAD(中位数绝对偏差)法,把超过5倍MAD的极端值截断。不然一个负几百倍的EP能把整个分组搞崩。

核心代码片段:

import pandas as pd
import numpy as np

# 假设df包含每只股票每月的市值和净利润
df['EP'] = df['net_profit_ttm'] / df['market_cap']

# MAD去极值
def winsorize_mad(series, n=5):
    median = series.median()
    mad = (series - median).abs().median()
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

df['EP'] = df.groupby('date')['EP'].transform(winsorize_mad)

嗯,这里要注意:净利润数据有滞后性。年报4月底才出完,所以5月底用的EP应该基于上一年年报。我早期做回测时没注意这个,结果策略收益虚高了一大截——后来被实盘狠狠教育了。

2. 分组回测:把股票分成10组

因子值算好了,接下来就是分组。每个月按EP从低到高排,等分成10组。组1是EP最低的(最“贵”的股票),组10是EP最高的(最“便宜”的)。

分组后,每组按市值加权构建组合,每月调仓一次。为什么用市值加权?因为等权容易让小市值股票主导收益,而市值加权更贴近真实交易。

避坑指南:我曾经在分组时用了全市场排序,结果小市值股票全挤在EP极端组里。后来改成每个行业内部排序,再合并分组——这样组内的行业分布更均匀,因子纯度更高。

回测结果长这样(2010-2023年,A股数据):

分组 年化收益率 年化波动率 夏普比率
组1(低EP) 4.2% 22.1% 0.19
组5(中EP) 8.7% 20.5% 0.42
组10(高EP) 14.3% 19.8% 0.72

看到没?从组1到组10,收益单调递增。这就是因子有效性的直观证据。你想想看,如果组10收益还不如组1,那这个因子就是反向指标。

3. 多空组合收益计算

分组看的是单调性,但真正衡量因子赚钱能力的是多空组合——做多组10,做空组1。这个组合的收益就是因子溢价。

计算很简单:

# 假设returns是每组的月度收益率
long_short = returns['group10'] - returns['group1']
annual_return = long_short.mean() * 12
annual_vol = long_short.std() * np.sqrt(12)
sharpe = annual_return / annual_vol

print(f"多空年化收益: {annual_return:.2%}")
print(f"多空夏普: {sharpe:.2f}")

我跑出来的结果是:多空年化收益约10.1%,夏普比率0.85。这个数字在A股算不错了,但别高兴太早——后面还要做统计检验。

4. t检验:收益是不是真的?

多空组合月均收益是0.84%,但万一只是随机波动呢?t检验就是回答这个问题的。

原假设:多空组合的期望收益为0。如果t统计量绝对值大于2(对应95%置信度),我们就拒绝原假设,认为因子确实有预测能力。

from scipy import stats

t_stat, p_value = stats.ttest_1samp(long_short, 0)
print(f"t统计量: {t_stat:.2f}, p值: {p_value:.4f}")

我算出来t=3.21,p=0.0014。嗯,显著。但要注意:t检验假设收益是独立同分布的,而实际金融数据有自相关和异方差。所以这个p值只能当参考,别太迷信。

重要提醒:t检验显著不等于因子未来有效。数据挖掘偏差、幸存者偏差、交易成本……随便哪个都能把显著变成不显著。我见过太多人拿着t=5的因子去实盘,结果亏得底裤都不剩。

5. IC值分析:因子与收益的相关性

IC(Information Coefficient)衡量的是因子值与未来收益的截面相关性。常用的是Spearman秩相关系数,因为它对极端值不敏感。

计算逻辑:每个月,算所有股票的EP排名与下月收益排名的相关系数。然后看IC的均值、标准差、IR(IC均值/IC标准差)。

# 每月计算IC
def calc_ic(factor_series, return_series):
    return stats.spearmanr(factor_series, return_series)[0]

ic_series = df.groupby('date').apply(
    lambda g: calc_ic(g['EP'], g['return_1m'])
)

print(f"IC均值: {ic_series.mean():.4f}")
print(f"IC标准差: {ic_series.std():.4f}")
print(f"IR: {ic_series.mean() / ic_series.std():.2f}")

我算出来的EP因子IC均值约0.035,IR=0.48。这个IR值在单因子里算中等偏上。IR大于0.5就算不错了,大于1.0就是顶级因子。

但IC也有坑:它只衡量线性关系。有些因子对极端收益的预测能力很强,但IC值可能不高。所以我会同时看IC的累积曲线——如果累积IC稳步向上,说明因子信号稳定;如果忽上忽下,那就要小心了。

6. 本章核心逻辑图

下面这张图把整个流程串起来了。从原始数据到最终检验,每一步都有坑,每一步也都有解法。

单因子构建与检验流程(EP因子为例) 步骤1:因子构建 净利润/市值 → EP 步骤2:分组回测 10组等分,市值加权 步骤3:多空组合 组10 - 组1收益 步骤4:t检验 H0: 多空收益=0 步骤5:IC分析 秩相关,IR值 结论判断 单调性 + 显著t值 + 稳定IC → 因子有效 注意:数据滞后、行业中性化、交易成本、幸存者偏差 因子投资 · 单因子检验标准流程

7. 小结:因子检验的“体检报告”

做完这一整套流程,你手里应该有三张表:分组收益表、t检验结果、IC统计量。这三张表就是因子的“体检报告”。

我个人习惯把结果整理成一张评分卡:

  • 单调性:组1到组10收益是否严格递增?给1-5分
  • 显著性:t统计量是否大于2?给1-5分
  • 稳定性:IC的IR是否大于0.5?给1-5分
  • 容量:多空组合的换手率是否可控?给1-5分

总分超过15分,这个因子就可以进入下一轮——多因子组合构建。如果低于10分,我建议直接放弃,别浪费时间。

嗯,EP因子我给了16分。它简单、透明、逻辑清晰,虽然收益不算爆炸,但胜在稳定。你想想看,一个能稳定赚钱的因子,比一个偶尔暴赚但经常暴亏的因子,哪个更让人睡得着觉?

最后一句经验之谈:因子检验不是一次性工作。每半年要重新跑一遍流程,因为市场结构在变。我见过太多因子前三年有效、后三年失效的例子。定期体检,才能活得久。

公众号:蓝海资料掘金营,微信deep3321