4. 估值类因子:市盈率(PE)、市净率(PB)、市销率(PS)、市现率(PCF)的构建与细节处理

估值因子是量化选股中最经典、最基础的一类特征。它们通过将市场价格与公司财务指标相除,衡量资产是“贵”还是“便宜”。本章系统讲解四大核心估值因子——PE、PB、PS、PCF 的数学定义、构建流程,以及在实际数据处理中必须解决的负值、极端值、TTM 与静态口径等关键细节。

4.1 四大估值因子定义与对比

下表从分子(价格)与分母(财务指标)两个维度,对比四个因子的经济含义与适用场景:

因子名称 英文缩写 分子 分母 经济含义 适用行业
市盈率 PE 总市值 / 股价 净利润 (Net Profit) 为每单位净利润支付的价格 盈利稳定的成熟行业
市净率 PB 总市值 / 股价 净资产 (Book Value) 为每单位净资产支付的价格 金融、周期、重资产行业
市销率 PS 总市值 / 股价 营业收入 (Revenue) 为每单位营收支付的价格 亏损期、高成长、零售行业
市现率 PCF 总市值 / 股价 经营活动现金流净额 (OCF) 为每单位经营现金流支付的价格 现金流稳定的公用事业、消费
核心原则: 估值因子越低,通常代表资产越“便宜”,但必须结合行业特征与财务质量综合判断。负值因子需要特殊处理,不能简单丢弃。

4.2 构建流程与口径选择:静态 vs TTM

构建估值因子时,分母财务数据存在两种主流口径:

  • 静态(Latest Annual, LYR): 使用最近一个完整财年的年报数据。优点是数据稳定、不易被修正;缺点是时效性差,年报发布后数月内数据可能过时。
  • TTM(Trailing Twelve Months): 使用最近四个季度的滚动合计数据。优点是时效性强,能及时反映最新经营变化;缺点是季度数据可能被后续修正,且需要处理季报披露节奏。

在量化实践中,TTM 口径是主流选择,因为它能更敏感地捕捉企业盈利拐点。以下以 PE 为例展示 TTM 净利润的计算逻辑:

# 伪代码:TTM 净利润计算
# 假设当前日期为 2024-11-15,最新完整季度为 2024Q3
# 需要获取最近四个季度:2023Q4, 2024Q1, 2024Q2, 2024Q3

ttm_net_profit = (
    net_profit_2023Q4 +
    net_profit_2024Q1 +
    net_profit_2024Q2 +
    net_profit_2024Q3
)

# 若当前日期在年报发布前(如 2024-04-01),
# 则最新完整四个季度为:2023Q1, 2023Q2, 2023Q3, 2023Q4
# 此时 TTM = 2023 全年净利润
工程注意: 在数据库层面,建议为每只股票维护一张“季度财务事实表”,包含每个季度的净利润、营收、净资产、经营现金流等字段。计算 TTM 时,按股票代码分组,取最近四个季度的 SUM 即可。注意处理季度数据缺失(如新股上市不足四个季度)的情况。

4.3 负值处理:不可忽视的细节

当分母为负值时,估值因子会变成负数。例如,亏损企业的 PE 为负。直接使用负 PE 进行排序或回归会带来严重问题:

  • 排序失真: 负 PE 的数值大小(如 -5 vs -100)与“便宜程度”无关,无法与正 PE 统一排序。
  • 统计偏误: 在横截面回归中,负值会扭曲因子载荷,导致模型失效。

业界常用的三种处理方案:

处理方法 操作 优点 缺点
剔除 直接删除分母为负的样本 简单、干净 丢失信息,可能引入生存偏差
截断/填充 将负值设为 NaN 或填充为行业均值 保留样本量 人为改变数据分布
排名转换 先对原始值做排名,再映射到 [-1, 1] 或 [0, 1] 消除负值影响,保留排序信息 丢失数值幅度信息
推荐做法: 在因子构建阶段,对负值样本做 行业中性化排名转换。即:在每个行业内,对原始 PE 做排名,然后将排名映射到 [-1, 1] 区间。这样既保留了“行业内相对贵贱”的信息,又避免了负值带来的数学问题。

4.4 极端值处理:MAD 与分位数截断

估值因子天然具有厚尾分布特征——少数公司可能因极端盈利或亏损产生离谱的 PE 值(如 PE > 1000 或 PE < -1000)。这些极端值会主导后续统计计算,必须处理。

两种主流方法:

  • MAD(Median Absolute Deviation)法: 以中位数为中心,剔除偏离中位数超过 N 倍 MAD 的样本。通常 N=3 或 N=5。
  • 分位数截断(Winsorization): 将低于 1% 分位数的值设为 1% 分位数,将高于 99% 分位数的值设为 99% 分位数。
# Python 示例:MAD 极端值处理
import numpy as np

def mad_winsorize(series, n=5):
    median = np.median(series)
    mad = np.median(np.abs(series - median))
    upper_bound = median + n * mad
    lower_bound = median - n * mad
    return np.clip(series, lower_bound, upper_bound)

# 示例:对 PE 因子做 MAD 截断
pe_series = np.array([15, 18, 22, 25, 30, 150, -200, 20, 19, 21])
pe_clean = mad_winsorize(pe_series, n=5)
print(pe_clean)
# 输出:极端值 150 和 -200 被截断到边界值
实践建议: 先做负值处理(行业排名转换),再做极端值截断(MAD 或分位数法)。顺序不可颠倒,因为排名转换后的值域已经归一化,极端值问题会大幅缓解。

4.5 估值因子构建全流程 SVG 图

下图展示了从原始数据到最终可用因子的完整处理流水线:

估值因子构建与处理全流程 原始数据输入 股价 + 财务指标 口径选择 静态 LYR TTM 滚动 因子计算 PE = P / E PB = P / B 负值处理 行业中性化排名转换 映射到 [-1, 1] 区间 极端值处理 MAD 截断 / 分位数 Winsorize N=3~5 倍 MAD 或 1%~99% 最终可用估值因子 PE_TTM_Rank / PB_LYR_Winsorized / PS_TTM_Rank / PCF_TTM_Rank

4.6 实战案例:PE_TTM 因子构建代码

以下代码片段展示从数据库取数到最终因子的完整 Python 实现:

import pandas as pd
import numpy as np
from scipy.stats import rankdata

def build_pe_ttm_factor(price_df, financial_df, date_col='trade_date', 
                         stock_col='stock_code', profit_col='net_profit'):
    """
    构建 TTM 口径的市盈率因子,包含负值排名转换与 MAD 截断
    
    参数:
        price_df: DataFrame,包含 ['trade_date', 'stock_code', 'close_price']
        financial_df: DataFrame,包含 ['quarter_end', 'stock_code', profit_col]
                      quarter_end 为季度末日期,如 '2024-09-30'
    
    返回:
        factor_df: DataFrame,包含 ['trade_date', 'stock_code', 'pe_ttm_rank']
    """
    # 1. 计算 TTM 净利润(最近四个季度滚动求和)
    financial_df = financial_df.sort_values(['stock_code', 'quarter_end'])
    financial_df['ttm_profit'] = (
        financial_df.groupby('stock_code')[profit_col]
        .rolling(window=4, min_periods=4)
        .sum()
        .reset_index(level=0, drop=True)
    )
    
    # 2. 对齐到交易日:将 TTM 数据映射到每个交易月
    # 假设每月末使用最新可得的 TTM 数据
    financial_df['year_month'] = financial_df['quarter_end'].dt.to_period('M')
    ttm_latest = financial_df.groupby(['stock_code', 'year_month']).last().reset_index()
    
    # 3. 合并价格数据
    price_df['year_month'] = price_df[date_col].dt.to_period('M')
    merged = pd.merge(price_df, ttm_latest, on=['stock_code', 'year_month'], how='inner')
    
    # 4. 计算原始 PE
    merged['pe_raw'] = merged['close_price'] / merged['ttm_profit']
    
    # 5. 负值处理:行业中性化排名转换
    # 假设 merged 已有 'industry' 列
    merged['pe_rank'] = merged.groupby('industry')['pe_raw'].transform(
        lambda x: rankdata(x, method='average') / len(x) * 2 - 1
    )
    
    # 6. 极端值处理:MAD 截断
    median_val = merged['pe_rank'].median()
    mad_val = np.median(np.abs(merged['pe_rank'] - median_val))
    upper = median_val + 5 * mad_val
    lower = median_val - 5 * mad_val
    merged['pe_ttm_rank'] = np.clip(merged['pe_rank'], lower, upper)
    
    return merged[[date_col, stock_col, 'pe_ttm_rank']]
工程要点:
  • TTM 计算需要至少 4 个季度的数据,新股或退市股会出现 NaN,建议直接剔除或填充行业均值。
  • 行业分类建议使用申万一级行业(28 个),行业太细会导致排名样本过少(< 10 只),太粗则中性化效果不足。
  • MAD 截断的 N 值建议在 3~5 之间,N=3 更激进(剔除更多极端值),N=5 更保守。

4.7 四大因子适用场景总结

因子 最佳使用场景 常见陷阱 推荐处理方式
PE 盈利稳定的蓝筹、消费、制造 亏损企业 PE 为负,周期股 PE 在顶点最低 TTM 口径 + 行业排名转换
PB 银行、保险、地产、周期 净资产为负(资不抵债)时 PB 为负 静态口径 + 剔除净资产为负样本
PS 亏损期成长股、电商、SaaS 营收为负极少见,但营收含金量不同 TTM 口径 + 分位数截断
PCF 公用事业、消费、现金流稳定行业 经营现金流波动大,季节性明显 TTM 口径 + MAD 截断
进阶建议: 在实际多因子模型中,建议同时使用多个估值因子的合成版本(如 PE + PB + PS 等权合成),以分散单一因子的行业偏好与财务操纵风险。合成前务必确保各因子已经过相同的负值处理与极端值截断流程。