专题:估值分位计算方法
一、为什么需要这个专题
在 01-估值指标公式与计算.md 中,我们解决了"PE/PB/PS 当前值是多少"的问题。但"当前 PE = 25 倍"这个数字本身没有意义——它到底是贵还是便宜,必须放到历史区间中比较。
估值分位(Valuation Percentile)就是把当前估值放在历史样本中的"百分位排名",回答"现在的估值,比历史上多少时候更贵"。
本专题解决的核心问题:
- 历史样本窗口选 5 年还是 10 年?为什么?
- 基准周期怎么选——日频、周频、月频?
- 亏损股、极端值如何处理,避免污染分位?
- 等权 PE 分位 vs 市值加权 PE 分位 哪个更"诚实"?
- 如何用 Python(numpy/pandas)从原始数据计算分位?
本专题是项目主线 /01-路线图/01-路线图 中"估值驱动定投"的量化基础——定投加减码的信号,全部来自分位阈值(如分位 < 20% 加码,> 90% 止盈)。
二、核心概念与公式
2.1 分位数的数学定义
给定一组历史样本 $X = {x_1, x_2, \dots, x_n}$(已排序),当前值 $x$ 的分位 $p$ 定义为:
$$
p = \frac{#{x_i \leq x}}{n} \times 100%
$$
即"历史样本中小于等于当前值的比例"。例如 PE 分位 = 15%,表示当前 PE 比历史上 85% 的时间都低。
2.2 分位计算的三种方法
定投实务中默认用线性插值法(numpy 的 percentile 函数即此实现)。
2.3 时间窗口选择
定投默认口径:10 年滚动窗口 + 全历史辅助参考。10 年覆盖一个完整周期,且数据质量较好。
2.4 基准周期选择
推荐周频:每周收盘 PE 构成样本,既过滤了日内噪音,又保证足够的样本量。
2.5 等权 vs 市值加权 PE 分位
这是估值分位中最容易被忽视、却影响巨大的一个选择。
市值加权 PE(指数官方口径):
$$
\text{PE}_{\text{市值加权}} = \frac{\sum_i \text{市值}_i}{\sum_i \text{净利润}_i}
$$
等权 PE:
$$
\text{PE}_{\text{等权}} = \frac{1}{n}\sum_i \text{PE}_i
$$
差异本质:市值加权 PE 受大市值成分股主导。例如沪深300中贵州茅台、宁德时代等少数巨头占权重极高,它们的估值变化会"绑架"整个指数的 PE。而等权 PE 给每只成分股相同权重,更反映"平均公司"的估值水平。
实际影响案例:
- 2021 年初,沪深300 市值加权 PE 分位 ~85%,但等权 PE 分位仅 ~60%。原因是茅台、宁德等少数龙头估值极高,拉高了市值加权 PE,而大部分成分股估值仍合理。
- 这种情况下,市值加权 PE 会给出"高估止盈"信号,但等权 PE 显示"仍有空间"。两者结合判断更稳健。
实务建议:两者都看。市值加权 PE 用于判断"跟踪指数本身"贵不贵;等权 PE 用于判断"市场整体广度"贵不贵。若两者背离严重,说明市场分化极端。
三、实操方法(含步骤与决策规则)
3.1 异常值处理
问题 1:亏损股 PE 为负
亏损股 PE 为负或无意义。处理方式:
- 剔除法(推荐):从样本中剔除亏损股,分母净利润合计也相应减少;
- 截断法:将 PE 为负的设为 NA,不参与统计。
问题 2:极端高 PE(如 > 500 倍)
某只成分股因净利润接近 0,PE 可能高达上千倍,会严重扭曲市值加权 PE。处理方式:
- 缩尾处理(Winsorize):将 PE > 99 分位的样本截断为 99 分位值;
- 剔除极端值:直接剔除 PE > 300 倍的样本。
问题 3:成分股变更
指数定期调整成分股,历史 PE 序列需要回溯调整。正规数据源(理杏仁、Wind)会提供"调整后历史 PE",直接使用即可。若自行计算,需注意成分股变更带来的口径跳变。
3.2 分位阈值与定投信号
决策规则:
- 分位每变化 20% 调整一次定投倍数,避免频繁操作;
- 分位信号以周频确认,避免日频噪音导致的误判;
- 若等权与市值加权分位背离 > 20%,取较高者作为止盈信号(更保守)。
3.3 Python 实现代码
import numpy as np
import pandas as pd
def calc_percentile(current_value, history_series):
"""
计算当前值在历史序列中的分位
:param current_value: 当前 PE/PB 值
:param history_series: 历史样本序列(pd.Series)
:return: 分位百分比(0-100)
"""
history = history_series.dropna().values
if len(history) < 50:
return np.nan # 样本不足
return np.percentile(history, current_value * 100 / current_value) # 占位,见下方修正
def calc_percentile_correct(current_value, history_series):
"""
正确实现:当前值在历史序列中的百分位
"""
history = history_series.dropna().values
if len(history) < 50:
return np.nan
# 方法1:用 sum 比例
rank = (history <= current_value).sum() / len(history)
return rank * 100
def calc_percentile_numpy(current_value, history_series):
"""
用 numpy.percentile 的反向计算更简洁
"""
history = history_series.dropna().values
if len(history) < 50:
return np.nan
# scipy.stats.percentileofscore 的等效实现
return 100.0 * np.sum(history <= current_value) / len(history)
def calc_index_pe_percentile(pe_series, window_years=10, freq='W'):
"""
计算指数 PE 的历史分位(完整流程)
:param pe_series: 日频 PE 序列,index 为日期
:param window_years: 滚动窗口年数
:param freq: 重采样周期 'W'=周 'M'=月
:return: 当前分位
"""
# 1. 重采样到周频(取每周收盘值)
pe_resampled = pe_series.resample(freq).last()
# 2. 取最近 window_years 年作为窗口
cutoff_date = pe_resampled.index.max() - pd.DateOffset(years=window_years)
window = pe_resampled[pe_resampled.index >= cutoff_date]
# 3. 异常值处理:剔除负值与极端值
window = window[(window > 0) & (window < 300)]
# 4. 计算当前值的分位
current_pe = pe_resampled.iloc[-1]
percentile = 100.0 * np.sum(window.values <= current_pe) / len(window)
return {
'current_pe': current_pe,
'percentile': percentile,
'window_samples': len(window),
'window_min': window.min(),
'window_max': window.max(),
'window_median': window.median()
}
# 等权 PE 分位计算(多成分股)
def calc_equal_weight_pe_pe_percentile(stock_pe_df, date_col='date',
code_col='code', pe_col='pe_ttm'):
"""
计算等权 PE 的历史分位
:param stock_pe_df: 包含 date/code/pe_ttm 的 DataFrame
:return: 每个日期的等权 PE 序列
"""
# 1. 按日期分组,计算每日等权 PE
daily_eq_pe = (
stock_pe_df.dropna(subset=[pe_col])
.groupby(date_col)[pe_col]
.mean() # 等权 = 算术平均
)
# 2. 重采样到周频
daily_eq_pe = daily_eq_pe.resample('W').last()
# 3. 计算分位(同上)
return calc_index_pe_percentile(daily_eq_pe)
# 使用示例
if __name__ == '__main__':
# 模拟 10 年周频 PE 数据
np.random.seed(42)
dates = pd.date_range('2014-01-01', '2024-01-01', freq='W')
pe_values = np.random.lognormal(mean=3.3, sigma=0.2, size=len(dates))
pe_series = pd.Series(pe_values, index=dates)
result = calc_index_pe_percentile(pe_series)
print(f"当前 PE: {result['current_pe']:.2f}")
print(f"历史分位: {result['percentile']:.1f}%")
print(f"窗口样本数: {result['window_samples']}")
print(f"区间: [{result['window_min']:.2f}, {result['window_max']:.2f}]")
print(f"中位数: {result['window_median']:.2f}")
3.4 完整定投决策流程
1. 每周五收盘后,获取目标指数 PE (TTM)
├─ 来源:理杏仁 / Wind / 中证指数官网
└─ 口径:市值加权 + 等权(两者均取)
2. 计算当前 PE 的 10 年周频分位
├─ 市值加权分位 P1
└─ 等权分位 P2
3. 分位信号判定
├─ 取 P = max(P1, P2)(保守原则)
├─ P < 20%:定投金额 × 1.5
├─ 20% ≤ P < 80%:正常定投
├─ 80% ≤ P < 90%:定投金额 × 0.5
└─ P ≥ 90%:启动分批止盈
4. 记录与复盘
└─ 每次操作记入定投日志,含 PE/分位/操作/原因
四、常见误区
误区 1:用日频数据算分位
日频数据噪音大,单日极端值(如 2015 年股灾单日暴跌)会扭曲分位。周频或月频更稳健。
误区 2:窗口选 1 年
1 年样本量不足(约 250 个交易日),且可能整段处于牛市或熊市,分位失真。至少 5 年,推荐 10 年。
误区 3:不处理亏损股
亏损股 PE 为负,若直接纳入算术平均,会拉低等权 PE,给出"低估"假象。必须剔除。
误区 4:只看市值加权 PE 分位
市值加权 PE 受龙头股主导,可能"指数高估但广度合理"。等权 PE 分位是必要的交叉验证。
误区 5:分位低 = 绝对便宜
分位是相对历史而言。若行业进入衰退期(如地产 2021 年后),PE 中枢永久下移,低分位可能是"价值陷阱"。必须结合行业生命周期(见 /03-中间段/02-行业分析/01-行业生命周期分析)。
误区 6:分位信号频繁触发
分位在 30%-70% 区间内小幅波动是正常的,不应每次都调整定投金额。建议以 20% 为阈值间隔,避免过度交易。
五、与项目其他文档的关联
01-估值指标公式与计算.md:本专题的输入(PE/PB 当前值)来自该专题的计算结果,形成"指标 → 分位 → 信号"的完整链路。
/01-路线图/01-路线图:定投加减码的核心信号来自分位阈值,路线图中的"估值驱动定投"以本专题为量化基础。
03-指数编制规则详解.md:指数的加权方式决定了"市值加权 PE"的计算口径,需配合理解。
12-定投止盈止损具体策略.md:止盈的核心信号"PE 分位 > 90%"直接使用本专题的分位计算结果。
13-数据源使用手册.md:理杏仁、Wind 等数据源提供"历史 PE 序列",本专题的代码基于此类数据。
/03-中间段/02-行业分析/01-行业生命周期分析:分位信号的解读需结合行业生命周期——衰退期的低分位是陷阱,不是机会。
/03-中间段/04-经济指标分析/01-宏观经济指标影响:宏观环境变化会影响整体估值中枢,分位需结合宏观背景判断。
小结:估值分位是把"当前 PE"转化为"贵还是便宜"信号的关键工具。10 年周频窗口、等权与市值加权双口径、异常值剔除,是分位计算的三项核心规范。Python 实现提供了从原始数据到定投信号的完整管道。下一专题将进入指数编制规则,理解指数本身是如何"构造"出来的。