4. 因子计算:以市盈率(PE)、市净率(PB)为例,计算基础估值因子

好,咱们进入实战环节了。前面聊了那么多因子逻辑和框架,现在该动手算一算了。我个人习惯,学任何量化策略,先从最基础的估值因子入手。为什么?因为估值因子最直观,也最不容易出错。

今天咱们就拿市盈率(PE)和市净率(PB)开刀。这两个指标,说白了就是看一家公司到底贵不贵。你想想看,买菜都知道挑便宜的,买股票其实也一样。

4.1 市盈率(PE)的计算与处理

市盈率的公式很简单:PE = 股价 / 每股收益。但实际项目中,坑可不少。

我记得第一次做多因子模型时,直接从数据库拉了一堆PE数据就跑回测。结果呢?回测曲线漂亮得不像话,一上实盘就崩。后来排查了半天,发现是PE数据里混进了负值——有些公司亏损,每股收益是负数,PE就成了负的。负PE在因子模型里就是个灾难。

⚠️ 避坑指南: 我曾经因为没处理负PE,导致模型选出了一堆亏损股。回测时这些亏损股因为"估值低"被选中,但实际上它们是在持续下跌。所以,负PE必须处理。

处理方式我一般有两种:

  • 直接剔除: 把PE为负的股票从样本中拿掉。适合样本量大的情况。
  • 截断处理: 设定一个上下限,比如PE在0到100之间。超出部分强制设为边界值。

来看代码实现:

import pandas as pd
import numpy as np

# 假设df是包含股票PE数据的DataFrame
def clean_pe(df, pe_col='pe_ttm'):
    """
    清洗PE数据
    """
    # 剔除负值
    df = df[df[pe_col] > 0].copy()
    
    # 截断处理:去掉极端值
    lower = df[pe_col].quantile(0.01)
    upper = df[pe_col].quantile(0.99)
    df[pe_col] = df[pe_col].clip(lower, upper)
    
    # 标准化:Z-score
    mean = df[pe_col].mean()
    std = df[pe_col].std()
    df['pe_zscore'] = (df[pe_col] - mean) / std
    
    return df
💡 个人经验: 我建议用TTM(滚动12个月)的PE,而不是静态PE。因为静态PE用的是上一年年报数据,滞后性太强。TTM能反映最新盈利状况。

4.2 市净率(PB)的计算与处理

PB的公式:PB = 股价 / 每股净资产。这个指标在金融、地产行业特别管用。为什么?因为这些行业资产重,净资产能真实反映公司价值。

但PB也有坑。我记得有一次做银行股因子,发现PB特别低的银行反而表现不好。后来一查,原来是这些银行的不良贷款率太高,净资产里水分很大。所以,PB不能只看数字,还得结合资产质量。

PB的处理逻辑和PE类似:

def clean_pb(df, pb_col='pb_lf'):
    """
    清洗PB数据
    """
    # PB一般不会为负,但也要检查
    df = df[df[pb_col] > 0].copy()
    
    # 行业中性化处理
    # 不同行业的PB差异巨大,直接比较没意义
    df['pb_industry_median'] = df.groupby('industry')[pb_col].transform('median')
    df['pb_relative'] = df[pb_col] / df['pb_industry_median']
    
    # 标准化
    mean = df['pb_relative'].mean()
    std = df['pb_relative'].std()
    df['pb_zscore'] = (df['pb_relative'] - mean) / std
    
    return df

核心要点: 估值因子一定要做行业中性化处理。不同行业的PE、PB天然不同,科技股PE普遍高,银行股PB普遍低。如果不做行业中性化,你的因子选出来的全是行业偏好,而不是真正的估值优势。

4.3 估值因子的合成

单因子不够稳定,我习惯把PE和PB合成一个综合估值因子。合成方式有很多种,这里介绍最常用的等权合成:

def combine_value_factor(df):
    """
    合成综合估值因子
    """
    # 等权合成
    df['value_factor'] = (df['pe_zscore'] + df['pb_zscore']) / 2
    
    # 再次标准化
    mean = df['value_factor'].mean()
    std = df['value_factor'].std()
    df['value_factor_std'] = (df['value_factor'] - mean) / std
    
    return df

你可能会问:为什么是等权?其实不一定。我在项目中试过动态权重,效果时好时坏。对于初学者,等权是最稳妥的选择。等权意味着我们不主观判断哪个因子更重要,让数据自己说话。

4.4 本章知识体系

下面这张图展示了估值因子计算的核心流程,我画得比较简洁,但每一步都很关键:

估值因子计算流程 获取原始数据 股价、每股收益、每股净资产 数据清洗 剔除负值、截断极端值 行业中性化 按行业中位数调整 Z-score标准化 均值为0,标准差为1 因子合成 等权或动态权重 输出估值因子 用于选股或打分 核心原则:先清洗,再中性化,最后标准化 PE和PB分别处理后,再合成综合估值因子

4.5 实战中的注意事项

嗯,这里要特别强调几点:

  1. 数据频率要统一。 我见过有人用日频的股价和周频的财报数据算PE,结果对不上。建议统一用日频数据,财报数据用最新一期。
  2. 注意财报发布日期。 财报数据是有滞后性的。比如4月底才发一季报,但你用3月份的股价去算PE,用的还是去年的数据。这会导致因子失真。
  3. 不要过度优化。 有些新手喜欢给因子加各种变换,什么对数变换、平方变换。其实对于估值因子,简单的Z-score就够了。过度优化容易过拟合。
💡 我的习惯: 每次算完因子,我都会画一张因子分布图。看看是不是正态分布,有没有异常值。如果分布图看起来怪怪的,那八成是数据有问题。

好了,估值因子的计算就讲到这里。PE和PB是最基础的,但也是最实用的。你把这个流程跑通了,后面学其他因子就会轻松很多。

专注资料整理