大多数金融模型都将分析师们的共识视为一个单一的预测性指标:收入预估、每股收益预估、息税折旧摊销前利润预估,或是某种形式的利润率预期。
这种做法确实有效,但也会使数据变得过于扁平化——平均预估值仅仅代表了所有预估数值的中心范围而已。实际上,在这一平均值背后,通常还存在较低的预估值、较高的预估值,以及参与形成这些预估意见的分析师人数。两家公司的平均预估值可能相同,但背后的共识程度却可能有很大差异。
因此,我想测试一个简单的想法:如果我们不再将分析师们的共识视为一个固定的数值,而是关注这些预估数值所呈现的具体分布形态,会怎么样呢?
目的并不是为了预测股票价格走势或生成交易信号,只是想看看这些预估数值所处的范围能否帮助我们了解分析师们在实际意见上存在哪些分歧。
目录
先决条件
要理解这个分析过程,你需要掌握Python的基础知识、pandas数据框的使用方法、字典的操作、循环语句,以及使用matplotlib进行简单的数据可视化。
此外,你还需要以下条件:
-
Python 3.9或更高版本
-
一个FMP API密钥
-
以下Python库:`requests`、`pandas`、`numpy`和`matplotlib`
-
对分析师的预估数据、收入数值、每股收益、市盈率等预测指标,以及分析师参与调研的情况有基本的了解
-
收入的最低值、平均值和最高值
-
每股收益的最低值、平均值和最高值
-
为收入预估提供意见的分析师人数
-
为每股收益预估提供意见的分析师人数
你并不需要具备高级的金融建模知识。我们的目标仅仅是展示:如何通过最低预估值、平均预估值、最高预估值以及参与预估的分析师人数,来揭示分析师们共识的实际形态,而不仅仅是将这些预估值视为一个固定的数值。
测试所需的数据
要想准确地进行这项测试,仅仅使用平均预估值是不够的,我们需要完整的预估范围数据。
对于每一家公司,我需要以下信息:
这样就能得到两种有用的信息。平均值反映了大家的预期中心;而最低值和最高值的估计则说明了预期范围的大小。分析师数量的统计则能大致说明大家意见的统一程度有多高。
我还希望使用一个包含多种类型公司的样本集。如果样本中只包含那些市值巨大的科技股,那么结果可能会过于“整齐”,因为这些公司通常都会受到大量投资者的关注。因此,我在样本中同时包含了市值巨大的科技股、半导体企业、能源行业公司、金融企业、医疗保健企业、消费品公司,以及那些不确定性较高的成长型企业。
对于数据来源,我选择了FMP提供的分析师估计数据,因为这些数据包含了本次分析所需的最低值、最高值、平均值以及分析师数量等信息。
在包含多种类型公司的样本集中获取分析师估计数据
首先,我导入了所需的基本库,并定义了股票样本集。
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from time import sleep
api_key = 'YOUR FMP API KEY'
base_url = 'https://financialmodelingprep.com/stable'
tickers = [
'AAPL', 'MSFT', 'NVDA', 'AMZN', 'META', 'GOOGL',
'TSLA', 'PLTR', 'COIN', 'RBLX', 'SNOW', 'UBER',
'AMD', 'INTC', 'MU', 'AVGO', 'QCOM',
'CAT', 'DE', 'BA', 'GE', 'XOM', 'CVX',
'WMT', 'COST', 'NKE', 'SBUX', 'MCD', 'TGT',
'JPM', 'BAC', 'GS', 'MS', 'V', 'MA',
'UNH', 'PFE', 'LLY', 'MRK', 'ABBV',
'ROKU', 'SHOP', 'SQ', 'PYPL', 'ZM'
]
接下来的步骤是获取每只股票的年度分析师估计数据。对于每一家公司,我选择了最接近的可用未来预测周期作为数据来源,因为有些公司的预测数据可能涵盖多个周期,而一些较远期的数据可能会缺失。
all_rows = []
today = pd Timestamp.today().normalize()
for ticker in tickers:
url = f'{base_url}/analyst-estimates'
params = {
'symbol': ticker,
'period': 'annual',
'limit': 10,
'apikey': api_key
}
response = requests.get(url, params=params)
data = response.json()
df = pd.DataFrame(data)
if len(df) == 0:
print(f'{ticker}: 没有数据')
continue
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
df = df[
(df['date'] > today) & \
(df['revenueAvg'].notna()) & \
(df['revenueLow'].notna()) & \
(df['revenueHigh'].notna()) & \
(df['epsAvg'].notna()) & \
(df['epsLow'].notna()) & \
(df['epsHigh'].notna())
].copy()
if len(df) == 0:
print(f'{ticker}: 没有可用的未来预测数据')
continue
row = df.iloc[0].copy()
all_rows.append(row)
print(f'{ticker} 数据获取完成')
sleep(0.2)
estimates = pd.DataFrame(all_rows)
estimates.head()
最终,我们得到了每家公司的每一条可用未来预测数据。
这个表格比单纯的平均估算数据要有用得多。它不仅提供了估算值的中位数,还展示了其波动范围,以及提出这些估算的分析师的数量。这些信息足以帮助我们了解市场共识的具体形态,而不仅仅是存储一个平均值而已。
将估算范围转化为差异指标
一旦获取到了这些估算数据,我就需要一种方法来比较不同公司之间的估算范围。
单纯的数值范围并不能用来进行有效对比。对于一家预计营收仅为50亿美元的公司来说,100亿美元的营收范围与一家预计营收高达500亿美元的公司而言,意味着完全不同的意义。因此,我选择用平均估算值来对这些范围进行标准化处理。
estimates['revenue_spread'] = ((estimates['revenueHigh'] - estimates['revenueLow']) / estimates['revenueAvg'])
estimates['eps_spread'] = ((estimates['epsHigh'] - estimates['epsLow']) / estimates['epsAvg'].abs())
shape_df = estimates[['symbol','date','revenueLow','revenueAvg','revenueHigh','revenue_spread','numAnalystsRevenue',
'epsLow','epsAvg','epsHigh','eps_spread','numAnalystsEps]].copy()
shape_df.head()

其逻辑非常简单。revenue_spread这个指标告诉我们,某个公司的营收估算范围相对于平均估算值而言有多宽;eps_spread则用于表示每股收益的估算范围。
但对于每股收益来说,还需要再进行一项额外的检查。如果平均每股收益接近于零,那么即使是一个正常的估算范围,也可能导致差异幅度非常大。但这并不一定意味着分析师们对这一数值存在极大的不确定性,有时候只是因为分母太小而已。
因此,我保留了原始的EPS差异指标,但同时也创建了一个更简洁、更适合用于数据绘制的版本。
shape_df['eps_spread_clean'] = shape_df['eps_spread']
shape_df.loc[shape_df['epsAvg'].abs() < 1, 'eps_spread_clean'] = np.nan
shape_df.loc[shape_df['eps_spread_clean'] > 3, 'eps_spread_clean'] = np.nan
完成这些处理之后,我进一步查看了那些差异范围最大或最小的公司。
shape_df.sort_values('revenue_spread', ascending=False)[
[
'symbol',
'revenueLow',
'revenueAvg',
'revenueHigh',
'revenue_spread',
'numAnalystsRevenue'
]
].head(10)
<这正是表明这个想法可能具有实用价值的第一个迹象。尽管有众多分析师对某些公司进行了分析,但这些公司的收入预估范围仍然相当广泛。特斯拉有35位分析师参与了对其收入的预估;英伟达有39位分析师参与预测,而英特尔则有31位分析师参与这一工作,不过这些公司的收入预估范围依然相当大。>
然后我查看了经过整理后的EPS差异数据。
shape_df.sort_values('eps_spread_clean', ascending=False)[
[
'symbol',
'epsLow',
'epsAvg',
'epsHigh',
'eps_spread_clean',
'numAnalystsEps'
]
].head(10)

这样的分析方式让结果变得更加有趣。营收数据和EPS的数据表现并不一致:特斯拉在这两方面的差异都很大;而SQ的EPS差异虽然较小,但数值仍然很高。这些现象暗示了一个有用的结论:分析师们之间的分歧可能体现在模型的不同层面。
初步观察:分析师覆盖范围并不能保证意见一致
首先我想验证的是,分析师对某家公司数据的关注程度越高,他们的一致性是否也会越高。
为此,我使用了两个简单的指标:
-
分析营收数据的分析师数量
-
营收预测数值的差异范围
然后我使用中位数作为阈值来划分数据。这种方法并不是一个正式的模型,只是用来区分不同类型的分析师共识情况罢了。
analyst_threshold = shape_df['numAnalystsRevenue'].median()
spread_threshold = shape_df['revenue_spread'].median()
analyst_threshold, spread_threshold

接着,我根据这些阈值创建了不同的分类标签:
shape_df['coverage_bucket'] = np.where(
shape_df['numAnalystsRevenue'] >= analyst_threshold,
'高覆盖率',
'低覆盖率'
)
shape_df['spread_bucket'] = npWHERE(
shape_df['revenue_spread'] <= spread_threshold,
'低差异范围',
'高差异范围'
)
通过这些分类,每家公司都可以被归入以下四个类别之一:
conditions = [
(shape_df['coverage_bucket'] == 'high coverage') & (shape_df['spread_bucket'] == 'low spread'),
(shape_df['coverage_bucket'] == 'high coverage') & (shape_df['spread_bucket'] == 'high spread'),
(shape_df['coverage_bucket'] == 'low coverage') & (shape_df['spread_bucket'] == 'low spread'),
(shape_df['coverage_bucket'] == 'low coverage') & (shape_df['spread_bucket'] == 'high spread')
]
labels = [
'意见高度一致',
'需要进一步观察',
'差异较小但趋势稳定',
'意见分歧较大'
]
shape_df['revenue_consensus_shape'] = np.select(conditions, labels)
结果的分割情况比我预期的要均衡得多:

这种分析方法很有用,因为这些标签并没有被归为同一个类别;实际上,市场存在多种不同的共识形态。
随后,我将分析师覆盖范围与收入差距进行了对比分析。
plt.figure(figsize=(12, 7))
for label in shape_df['revenue_consensus_shape'].unique():
temp = shape_df[shape_df['revenue_consensus_shape'] == label]
plt.scatter(
temp['numAnalystsRevenue'],
temp['revenue_spread'],
s=80,
label=label,
alpha=0.8
)
plt.axvline(analyst_threshold, linestyle='--', linewidth=1)
plt.axhline(spread_threshold, linestyle='--', linewidth=1)
for i, row in shape_df.iterrows():
if row['revenue_spread'] > spread_threshold or row['numAnalystsRevenue'] > analyst_threshold:
plt.text(
row['numAnalystsRevenue'] + 0.3,
row['revenue_spread'],
row['symbol'],
fontsize=9
)
plt.title('分析师覆盖范围与收入预估差距')
plt.xlabel('参与预测的分析师数量')
plt.ylabel('收入预估差距')
plt.legend()
plt.show()

这张图表清楚地表明:分析师覆盖范围越广,并不意味着他们的预测结果就越一致。
MSFT、AAPL、MA、WMT和META这些公司的预测结果更接近于“共识区”;它们的分析师覆盖范围较广,且收入预估的差距相对较小。
然而TSLA、AVGO、NVDA、INTC、AMD、MU和GOOGL等公司的分析师覆盖范围也很广,但它们的收入预估差距却较大。这些公司属于“受到关注但预测结果存在不确定性”的对象。市场并没有忽视它们,分析师们也在密切关注这些公司,但由于分析深度不足,因此预测范围仍然较宽。
那些处于“共识范围较低”区域的公司也具有一定的参考价值。CVX、XOM和COIN等公司的收入预估差距较大,且分析师覆盖范围相对较小。这种不确定性与单纯的观点分歧不同,其背后反映的是分析师们对这些公司了解得不够深入。
虽然最初的这种分析方法很有帮助,但它只关注了收入这一指标。接下来的问题更加有趣:这种不确定性是存在于收入方面,还是EPS方面,又或者两者都存在?
plot_df = shape_df.dropna(subset=['revenue_spread', 'eps_spread_clean']).copy()
plt.figure(figsize=(12, 7))
plt.scatter(
plot_df['revenue_spread'],
plot_df['eps_spread-clean'],
s=plot_df['numAnalystsRevenue'] * 3,
alpha=0.75
)
for i, row in plot_df.iterrows():
plt.text(
row['revenue_spread'] + 0.002,
row['eps_spread_clean'],
row['symbol'],
fontsize=9
)
plt.title('收入预估差距与EPS预估差距')
plt.xlabel('收入预估差距')
plt.ylabel('EPS预估差距')
plt.show()

这种视图确实更有实用性。
该图表显示,不同公司的共识不确定性程度各不相同。有些公司的收入和每股收益的波动范围都很小;而有些公司的这两个指标的波动幅度则很大;还有少数公司存在较为具体的分歧。
SQ就是最典型的例子。它的收入波动幅度很小,但每股收益的波动幅度却很大,这说明分析师们在收入方面的预测较为一致,而在每股收益方面的预测则存在较大分歧。
TSLA的情况则恰恰相反。无论是收入还是每股收益的波动幅度都很大,因此平均预测值实际上掩盖了模型中存在的多种分歧。
在这种情况下,我决定将这些数据进行简单的分类。需要再次强调的是,这并不是一个正式的风险评估模型。我使用中位数作为划分标准,只是为了更清晰地区分各种情况而已。
revenue_spread_threshold = plot_df['revenue_spread'].median()
eps_spread_threshold = plot_df['eps_spread_clean'].median()
plot_df['revenue_uncertainty'] = np.where(
plot_df['revenue_spread'] <= revenue_spread_threshold,
'低收入不确定性',
'高收入不确定性'
)
plot_df['eps_uncertainty'] = npWHERE(
plot_df['eps_spread_clean'] <= eps_spread_threshold,
'低每股收益不确定性',
'高每股收益不确定性'
)
随后,我将这两类情况合并为四种不同的预测类型。
conditions = [
(plot_df['revenue_uncertainty'] == '低收入不确定性') & (plot_df['epsUncertainty'] == '低每股收益不确定性'),
(plot_df['revenue_uncertainty'] == '低收入不确定性') & (plot_df['epsUncertainty'] == '高每股收益不确定性'),
(plot_df['revenue_uncertainty'] == '高收入不确定性') & (plot_df['epsUncertainty'] == '低每股收益不确定性'),
(plot_df['revenue_uncertainty'] == '高收入不确定性') & (plot_df['epsUncertainty'] == '高每股收益不确定性')
]
labels = [
'稳定预测类型',
'盈利能力不确定性',
'收入端不确定性',
'整体预测不确定性'
]
plot_df['forecast_shape'] = np.select(conditions, labels)
最终的分类结果如下所示:

这种分类方法比之前的更有用,因为它能够清楚地显示出分歧究竟出现在哪些方面。
“稳定预测类型”意味着收入和每股收益的波动范围都相对较小;“盈利能力不确定性”表示收入预测的准确性较高,但每股收益预测的不确定性较大;“收入端不确定性”则说明收入范围的波动幅度较大,而每股收益的波动幅度相对较小;“整体预测不确定性”则表示这两方面的预测结果都具有较大的不确定性。
plt.figure(figsize=(12, 7))
for label in plot_df['forecast_shape'].unique():
temp = plot_df[plot_df['forecast_shape'] == label]
plt.scatter(
temp['revenue_spread'],
temp['eps_spread_clean'],
s=temp['numAnalystsRevenue'] * 3,
label=label,
alpha=0.75
)
plt.axvline(revenue_spread_threshold, linestyle='--', linewidth=1)
plt.axhline(eps_spread_threshold, linestyle='--', linewidth=1)
for i, row in plot_df.iterrows():
if (
row['revenue_spread'] > revenue_spread_threshold or
row['eps_spread_clean'] > eps_spread_threshold
):
plt.text(
row['revenue_spread'] + 0.002,
row['eps_spread_clean'],
row['symbol'],
fontsize=9
)
plt.title('收入不确定性与每股收益不确定性的关系')
plt.xlabel('收入预测范围的波动幅度')
plt.ylabel('每股收益预测范围的波动幅度')
plt.legend()
plt.show()

这个图表成为了分析的主要依据。
平均预测值掩盖了人们的真实预期,但这个图表展示了围绕这一平均值的各种不同观点。在预测分析中,这一点非常重要。一个模型不应该将那些预测结果高度一致的案例与那些存在较大分歧的案例视为具有相同程度的可靠性。
一些特定的公司使得这种模式变得显而易见
一旦将这些公司按照它们的预测特征进行分类,这种规律性就更容易被发现了。
plot_df[
[
'符号',
'收入预测范围',
'每股收益预测范围',
'参与预测的分析师数量',
'预测分析师的数量',
'预测类型'
]
].sort_values(['预测类型', '每股收益预测范围'], ascending=[True, False])
虽然完整的表格很有用,但对于这篇文章来说,每个分类中的具体案例才更为重要。
broad_uncertainty = final_view[
final_view['预测类型'] == '预测不确定性较大'
].sort_values('每股收益预测范围百分比', ascending=False)
broad_uncertainty.head(10)

TSLA显然是其中的一个异常值。它的收入预测范围约为21.8%,而每股收益预测范围则超过了104%。这种差异不仅仅体现在某个具体指标上,而是贯穿于公司的营收和净利润这两个方面。
CVX和XOM也值得关注,但原因有所不同。它们的收入预测范围非常广,而且参与预测的分析师数量也比样本中的许多科技公司要少。因此,它们们的预测结果所呈现出的“共识形态”与TSLA这样的公司是不同的——尽管TSLA的分析师覆盖范围更广,但预测结果之间仍然存在较大分歧。
接着,我研究了那些盈利能力存在不确定性的公司。
profitability_uncertainty = final_view[
final_view['预测类型'] == '盈利能力不确定性'
].sort_values('每股收益预测范围百分比', ascending=False)
profitability_uncertainty

从概念上来说,这个分类是最有意义的。
SQ公司的收入预测范围仅为1.1%,但每股收益预测范围却达到了近73.8%。这与TSLA的情况截然不同。在SQ公司,分析师们对收入预测的共识程度较高,但对每股收益的预测则存在较大分歧。
对于一个预测模型来说,这一点非常重要。如果我只存储平均收入预测值和平均每股收益预测值,就会忽略这种差异。模型就无法了解到:虽然收入预测的不确定性相对较小,但每股收益预测却存在着较大的分歧。
SNOW和PLTR的表现模式相似,但程度没有那么极端。收入预期值彼此相差不大,但每股收益预期值的波动范围要大一些。这表明在盈利能力、利润率或收益转化率方面存在不确定性,而并非纯粹的收入增长问题。
那些预测值波动较小的案例形成了鲜明的对比。
stable_shape = final_view[
final_view['forecast_shape'] == 'stable forecast shape'
].sort_values(['revenue_spread_pct', 'eps_spread pct'])
stable_shape.head(10)

MSFT就是最典型的例子。它的收入预期值波动幅度约为0.4%,而每股收益预期值的波动幅度约为3.0%。MA、BAC、ABBV和TGT也属于这一类别,它们的收入和每股收益预期值波动范围都相对较小。
这并不意味着这些预测值一定是正确的,只是说明分析师们的预测值聚集在了一个相对狭窄的范围内而已。
最后,那些预测值不确定性较大的案例数量较少。
topline_uncertainty = final_view[
final_view['forecast_shape'] == 'top-line uncertainty'
].sort_values('revenue_spread pct', ascending=False)
topline_uncertainty

这一类的案例虽然数量较少,但它们确实反映了某种特定的情况:在这些案例中,收入预测的不确定性比每股收益预测的不确定性更为明显。
归根结底,关键在于这样一个事实:分析师们的预测结果并不总是呈现出一种固定的模式。平均数值会掩盖这一点,而预测值的实际波动范围才能真正反映人们在这些预测指标上存在哪些分歧。
这对预测工作流程会产生什么影响?
实际来说,并不是每个模型都需要建立一个复杂的不确定性评估系统。事情其实比这简单得多。
如果一个模型已经记录了分析师们的预测值,那么它也应该同时记录这些预测值的波动范围。
而不是只保留以下这些信息:
symbol | estimated_revenue | estimated_eps
我更倾向于保留如下这些信息:
symbol | estimated_revenue | estimated.eps | revenue_spread | eps_spread | analyst_count | forecast_shape
这样,模型就能更好地了解自己所使用的各种预测数据的具体情况。
为了便于使用这些数据,我制作了一个最终的表格,其中包含了预测周期、收入预测值的波动范围、每股收益预测值的波动范围、参与预测的分析师数量、分析师们对各项指标的预测共识以及整体的预测模式等信息。
final_df = plot_df[
[
'symbol',
'date',
'revenueAvg',
'revenueLow',
'revenueHigh',
'revenue_spread',
'epsAvg',
'epsLow',
'epsHigh',
'eps_spread_clean',
'numAnalystsRevenue',
'numAnalystsEps',
'revenue_consensus_shape',
'forecast_shape'
]
].copy()
final_df = final_df.rename(
columns={
'date': 'estimate_period',
'revenueAvg': 'revenue_avg',
'revenueLow': 'revenue_low',
'revenueHigh': 'revenue_high',
'epsAvg': 'eps_avg',
'epsLow': 'eps_low',
'epsHigh': 'eps_high',
'eps_spread_clean': 'eps_spread',
'numAnalystsRevenue': 'revenue_analysts',
'numAnalystsEps': 'eps_analysts'
}
)
final_df['revenue_spread pct'] = final_df['revenue_spread'] * 100
final_df['eps_spread pct'] = final_df['eps_spread'] * 100
final_view = final_df[
[
'symbol',
'estimate_period',
'revenue_spreadpct',
'eps_spread_pct',
'revenue_analysts',
'eps_analysts',
'revenue_consensus_shape',
'forecast_shape'
]
].copy()
final_view = final_view.sort_values('eps_spread pct', ascending=False)
final_view.head(15)
输出结果如下:

这个表格主要用于揭示平均预测值在哪些方面存在最大的分歧。
TSLA的情况最为明显。无论是收入还是每股收益的预测范围都相当广泛,因此如果只使用平均预测值,就会使得预测结构变得过于平缓。
SQ的情况则不同。它的收入预测范围仅约为1.1%,但每股收益的预测范围却高达73.8%。这说明,其中的分歧主要体现在盈利能力或收益转化率方面,而非收入本身。
SNOW和PLTR也表现出类似的趋势,不过程度没有那么极端。它们的收入预测范围相对较窄,而每股收益的预测范围则要宽得多。对于任何以这些预测值为输入数据的模型来说,这种区别都是非常有参考价值的。
关键并不在于判断哪个预测值是正确的,而是要避免将所有的平均预测值都视为具有相同程度的可靠性。平均值只代表了中心趋势,而预测范围的大小则反映了围绕这一中心存在多少分歧。
我不会过度夸大这些结论
我不应该将这些分类结果视为最终的模型预测结果。
这里所选取的股票样本是经过精心挑选的,并非整个市场的数据;划分标准也只是简单的中位数阈值,并非基于统计置信度的分析方法。这些分类方式有助于将数据分成易于理解的组别,但它们不应被视作绝对的界限。
在处理每股收益的预测范围时也需要格外小心。如果平均每股收益接近零,那么预测范围的分布可能会出现失真,因此我在绘制图表之前已经去掉了那些极端的数据。
最重要的是,这些数据并不能告诉我们哪个预测值是正确的。预测范围较广并不一定意味着这家公司的经营状况不佳,而预测范围较窄也不代表预测结果就一定准确。
其实更有意义的是:这种分析方式让人们不再假装所有的平均预测值都具有相同的可靠性。
最终结论:共识预测具有内在结构
平均预测值仍然具有一定的参考价值,我也不会从预测模型中剔除它。
但是,在综合考虑了最低值、最高值、平均值以及分析师的数量之后,如果只使用平均预测值进行分析,就会觉得结果不够全面。
共识预测实际上具有内在的结构:有些预测值的范围较窄,有些则较宽;有时分歧主要体现在收入方面,有时则体现在每股收益上,还有时候这两种因素都会导致分歧的出现。
一个更好的预测工作流程应该保留这种结构,而不是将其抹平。这样的流程并不需要过于复杂,只需添加一些额外的指标,比如收入预测范围的差异、每股收益预测范围的差异、分析师的数量以及预测结果的分布形态等,就能使预测结果更加客观可靠。