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 图
下图展示了从原始数据到最终可用因子的完整处理流水线:
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 等权合成),以分散单一因子的行业偏好与财务操纵风险。合成前务必确保各因子已经过相同的负值处理与极端值截断流程。