35 上市公司净利润增长率的计算
35.1 引言增长率的价值发现功能
净利润增长率的重要性:
净利润增长率是衡量上市公司盈利能力和成长性的核心指标,在价值投资和成长投资中都具有关键作用。本章从财务数据出发,完成增长率的计算、复核、可视化与投资解读。
35.2 本章学习目标
先修内容:阅读本章前,建议先完成第 章节 10 章(Series 与 DataFrame 基础)、第 章节 11 章(数据框索引与 loc 取数)、第 章节 14 章(读取 Excel 文件)与第 章节 33 章(均值、标准差等统计量)。
- 从在线 Excel 利润表中读取数据、设置索引并提取指定科目行的方法;
- 年份索引的字符串截取、类型转换与排序等时间序列预处理操作;
- 用
pct_change()计算同比增长率,及其与手动计算的一致性验证; - 复合年均增长率(CAGR)与单期平均增长率的区别及适用场景;
- 增长率指标在成长性评估、PEG 估值等投资分析中的应用。
35.3 数据读取与预处理
35.3.1 财务数据获取
补充说明:上市公司财务报告体系
中国上市公司财务报告披露周期:
- 季度报告(Q1, Q2, Q3):
- 每季度结束后1个月内披露
- 包含简化的资产负债表、利润表
- 不需要审计
- 年度报告(Q4/年报):
- 会计年度结束后4个月内披露
- 包含完整的三张报表
- 必须经过审计
- 附详细的管理层讨论与分析
- 业绩快报与预告:
- 业绩快报:实际数据但未经审计
- 业绩预告:预计数据范围
- 时间早于正式报告
利润表的层次结构:
一、营业收入
减:营业成本
营业税金及附加
销售费用
管理费用
财务费用
-----------------------------------
二、营业利润(Operating Profit) ← 本章使用(数据源行名为"四、营业利润(亿元)")
加:营业外收入
减:营业外支出
-----------------------------------
三、利润总额(Total Profit)
减:所得税费用
-----------------------------------
四、净利润(Net Profit) ← 更常用(数据源行名为"六、净利润(亿元)")
注:上表采用教材通行的简化编号;本章数据文件的实际行名为
"四、营业利润(亿元)""五、利润总额(亿元)""六、净利润(亿元)",
两套编号体系不同,用df.loc[]取数时以数据源实际行名为准。
# =============================================================================
# 题目: 读取上市公司财务数据
# =============================================================================
# 本代码块演示如何从Excel文件读取上市公司财务数据。上市公司财务数据
# 通常以Excel格式公开披露,包含多个年度的利润表、资产负债表和现金流量表。
# 数据读取是财务分析的第一步,需要正确处理表头、索引和数据类型。
# ==================== 导入必要的库 ====================
import pandas as pd # 导入pandas库,用于数据处理和分析
import numpy as np # 导入numpy库,用于数值计算
import matplotlib.pyplot as plt # 导入matplotlib库,用于可视化
# ==================== 设置中文字体 ====================
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备 # 设置中文字体为黑体,避免中文乱码
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题
# ==================== 读取Excel文件 ====================
url = 'https://huoran.oss-cn-shenzhen.aliyuncs.com/20230410/xls/1645298865123385344.xls' # 数据文件URL
df = pd.read_excel(url) # 使用pandas读取Excel文件,默认读取第一个sheet
# ==================== 输出原始数据预览 ====================
print('原始数据:') # 打印标题
print(df.head(10)) # 打印前10行数据,快速了解数据结构
print(f'\n数据类型: {type(df)}') # 确认数据类型为DataFrame
print(f'\n数据形状: {df.shape}') # 打印数据维度(行数,列数)代码深度解析:
pd.read_excel()参数:- 默认读取第一个sheet
header=0: 第一行为列名index_col: 指定索引列
- 在线数据读取:
- 支持HTTP/HTTPS URL
- 自动处理编码
- 适合快速原型开发
- 数据结构检查:
shape: (行数, 列数)head(): 前几行数据预览type(): 确认DataFrame类型
35.3.2 数据提取与转换
补充说明:时间序列的索引处理
时间序列分析的关键步骤:
- 索引设置:
set_index(): 将某列设为索引- 索引必须是唯一的
- 时间索引便于切片操作
- 索引类型:
- 字符串: ‘2020’, ‘2021’
- 整数: 2020, 2021
- Datetime: 2020-01-01(推荐)
- 索引排序:
sort_index(): 按索引排序- 升序或降序
- 确保时间顺序正确
口径说明:本章章名沿用平台任务名”净利润增长率”,但平台数据源(上述利润表文件)实际提供并被取用的是”四、营业利润(亿元)“这一行,因此本章演示的是同一增长率算法在营业利润上的应用。若要计算真正口径的净利润增长率,只需把平台代码中 df.loc[...] 的行名换成数据源中的净利润行(如”六、净利润(亿元)“)即可迁移;取数前应先核对数据源的实际行名。
任务要求:将下方代码原样输入教学平台并运行(注释可省略)。该代码读取远程利润表 Excel 文件,将首列设为索引后提取”四、营业利润(亿元)“所在行,把年份索引整理为四位整数并升序排序,最后用 pct_change() 计算增长率并输出。
# 注:该代码块读取教学平台的 OSS 镜像直链(联网即可在本地复现);变量名与下方讲解代码不匹配,注意对照
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
#读取excel文件
df = pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20230410/xls/1645298865123385344.xls')
#把index设置为Unnamed: 0
df.set_index('Unnamed: 0', inplace=True)
#提取index为“四、营业利润(亿元)”的行
df = df.loc['四、营业利润(亿元)']
#输出数据格式
print(type(df))
#修改index为当前index的前4个字符
df.index = df.index.str[:4]
#将index转换为int类型
df.index = df.index.astype(int)
#将index从小到大排序
df.sort_index(inplace=True)
#计算净利润增长率
result = df.pct_change()
#输出结果
print(result)预期输出(作者实测(OSS 直链复算)):平台先打印 <class 'pandas.core.series.Series'>,说明按行标签取数后数据已从 DataFrame 变为 Series;随后输出营业利润行 1998-2022 共 25 期、按年份升序排列的增长率序列。判读要点:第一个年份 1998 没有上期数据,其增长率为 NaN,属正常现象而非数据错误;其余每个数值是”当期相对上期的百分比变化”(小数形式,如 0.15 对应增长 15%);结合上文”口径说明”,该序列实际是营业利润口径的增长率。锚点数值:1999 年 +61.64%、2007 年 +81.95%(全序列最高)、2015 年 +0.25%(全序列最低的正增长)、2022 年 +17.56%(末年);24 个年度增长率全为正。
代码深度解析:
loc[]索引器:- 基于标签的索引
df.loc[row_label]: 选择行- 支持切片和布尔索引
- 字符串处理:
.str[:4]: 切片前4个字符.astype(int): 类型转换- 链式操作提高效率
- 数据清洗:
pd.to_numeric(): 转换为数值errors='coerce': 无法转换的设为NaN- 便于后续处理缺失值
35.4 计算增长率
35.4.1 增长率计算方法
理论背景:增长率的各种计算方式
- 简单增长率: \[ g_t = \frac{X_t - X_{t-1}}{X_{t-1}} \]
- 适用于:任意相邻两期
- 优点:计算简单
- 缺点:受基数影响
- 复合年均增长率(CAGR): \[ \text{CAGR} = \left(\frac{X_n}{X_0}\right)^{\frac{1}{n}} - 1 \]
- 适用于:长期平均增长
- 优点:平滑短期波动
- 缺点:忽略中间路径
- 对数差分增长率: \[ g_t = \ln(X_t) - \ln(X_{t-1}) \]
- 适用于:连续复利假设
- 优点:对称性,可加性
- 缺点:近似值
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 计算净利润增长率
# =============================================================================
# 本代码块演示如何使用pandas计算时间序列的增长率。增长率计算是财务分析
# 的核心技能,我们需要掌握不同的计算方法并理解它们的应用场景。
# ==================== 方法1:使用pct_change()计算增长率 ====================
# pct_change()计算百分比变化:(当前期 - 上一期) / 上一期
# 简单增长率公式: R_t = (X_t - X_{t-1}) / X_{t-1}
# periods参数默认为1,表示与前一期相比
growth_rate = profit.pct_change() # 计算百分比变化,返回增长率序列
# ==================== 输出增长率(小数形式) ====================
print('净利润增长率(小数形式):') # 打印标题
print(growth_rate) # 打印增长率序列,第一个值为NaN(因为没有上期数据)
# ==================== 转换为百分比形式 ====================
growth_rate_pct = growth_rate * 100 # 将小数转换为百分比形式
# ==================== 输出增长率(百分比形式) ====================
print('\n净利润增长率(百分比形式):') # 打印标题
for year, rate in growth_rate_pct.items(): # 遍历每一年的增长率
if not pd.isna(rate): # 跳过NaN值(第一年)
# 使用格式化字符串输出:+号表示正数,-号表示负数
# :+.2f表示带符号,保留2位小数
print(f'{year}年: {rate:+.2f}%') # 格式化输出增长率
# ==================== 方法2:手动计算验证 ====================
# 手动实现增长率计算,验证pct_change()的结果
growth_rate_manual = pd.Series(index=profit.index, dtype=float) # 创建空Series存储结果
for i in range(1, len(profit)): # 从第二个数据点开始(索引为1)
# 手动计算增长率: (本期 - 上期) / 上期
growth_rate_manual.iloc[i] = (profit.iloc[i] - profit.iloc[i-1]) / profit.iloc[i-1]
# ==================== 输出手动计算结果 ====================
print('\n验证-手动计算:') # 打印标题
for year, rate in growth_rate_manual.items(): # 遍历手动计算的结果
if not pd.isna(rate): # 跳过NaN值
print(f'{year}年: {rate*100:+.2f}%') # 格式化输出
# ==================== 验证两种方法一致性 ====================
# np.allclose()比较两个数组是否在容差范围内相等
# 这用于验证pct_change()和手动计算的结果是否一致
print(f'\n两种方法一致: {np.allclose(growth_rate.dropna(), growth_rate_manual.dropna())}') # 验证结果代码深度解析:
pct_change()函数:- 计算百分比变化
- 默认
periods=1 - 第一个值为NaN(无上期数据)
- 格式化输出:
{:+.2f}: 带符号,2位小数+表示正数-表示负数
- NaN处理:
- 第一个值为NaN
- 用
pd.isna()检查 - 用
dropna()删除
35.4.2 复合年均增长率(CAGR)
补充说明:长期增长率的度量
CAGR(Compound Annual Growth Rate)假设投资按恒定增长率复利增长,更符合长期投资视角。
\[ \text{CAGR} = \left(\frac{V_{\text{终值}}}{V_{\text{初值}}}\right)^{\frac{1}{n}} - 1 \]
其中:
- \(V_{\text{终值}}\): 期末值
- \(V_{\text{初值}}\): 期初值
- \(n\): 年数
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 计算复合年均增长率(CAGR)
# =============================================================================
# 本代码块演示如何计算复合年均增长率(CAGR)。CAGR是衡量长期投资表现
# 的标准指标,它假设投资按恒定增长率复利增长,消除了短期波动的影响。
# ==================== 删除NaN值 ====================
profit_valid = profit.dropna() # 删除缺失值,确保计算准确
# ==================== 计算CAGR ====================
if len(profit_valid) >= 2: # 确保至少有2个数据点
# 提取期初值和期末值
start_value = profit_valid.iloc[0] # 期初值(第一个数据点)
end_value = profit_valid.iloc[-1] # 期末值(最后一个数据点)
n_years = len(profit_valid) - 1 # 年份数=数据点数-1
# CAGR公式: (期末值 / 期初值)^(1/年份数) - 1
cagr = (end_value / start_value) ** (1 / n_years) - 1 # 计算复合年均增长率
# ==================== 输出CAGR计算结果 ====================
print('复合年均增长率(CAGR):') # 打印标题
print(f'期初值: {start_value:.2f}亿元') # 打印期初值,保留2位小数
print(f'期末值: {end_value:.2f}亿元') # 打印期末值,保留2位小数
print(f'年份数: {n_years}年') # 打印年份数
print(f'CAGR: {cagr*100:.2f}%') # 打印CAGR,转换为百分比
# ==================== 对比平均增长率 ====================
# 对比CAGR和简单平均增长率
avg_growth_rate = growth_rate.mean() # 计算简单平均增长率
print(f'\n简单平均增长率: {avg_growth_rate*100:.2f}%') # 打印简单平均
print(f'差异: {(cagr - avg_growth_rate)*100:.2f}个百分点') # 计算两者差异
# ==================== 解读CAGR ====================
print('\n解读:') # 打印标题
print('- CAGR假设每年按相同增长率复利增长') # CAGR的含义
print('- 简单平均忽略复利效应') # 简单平均的特点
print('- 通常CAGR < 简单平均(除非增长率恒定)') # 两者关系35.5 结果分析与可视化
35.5.1 增长率统计分析
净利润增长率的金融意义:
在解读统计结果之前,先明确增长率数字背后的四层含义:
- 成长性评估:
- 识别高增长企业(成长股)
- 区分周期性增长与趋势性增长
- 评估增长可持续性
- 估值依据:
- DCF模型的关键输入
- PEG比率的核心要素
- 相对估值的基础
- 投资决策:
- 价值投资:低增长+低估值
- 成长投资:高增长+合理估值
- GARP策略:合理价格增长
- 业绩预警:
- 增长率放缓可能预示业绩拐点
- 负增长需警惕企业衰退
- 波动率反映经营风险
下面的统计分析将依次回答”增长有多快、有多稳、是否可持续”这三个问题。
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 增长率统计分析
# =============================================================================
# 本代码块演示如何对增长率进行统计分析,包括均值、最大值、最小值、标准差
# 等。这些统计量帮助我们了解企业增长的稳定性和波动性。
# ==================== 统计分析 ====================
valid_years = growth_rate.dropna() # 删除NaN值,得到有效的增长率序列
print(f'有效年份: {len(valid_years)}') # 打印有效年份数量
print(f'平均增长率: {valid_years.mean()*100:.2f}%') # 打印平均增长率
print(f'最大增长率: {valid_years.max()*100:.2f}%') # 打印最大增长率
print(f'最小增长率: {valid_years.min()*100:.2f}%') # 打印最小增长率
print(f'增长率标准差: {valid_years.std()*100:.2f}%') # 打印增长率标准差(波动性)
# ==================== 增长率分布 ====================
print('\n增长率分布:') # 打印标题
# 统计正增长年份和负增长年份
positive_years = (valid_years > 0).sum() # 计算正增长年份数量
negative_years = (valid_years < 0).sum() # 计算负增长年份数量
print(f'正增长年份: {positive_years}年 ({positive_years/len(valid_years)*100:.1f}%)') # 打印正增长占比
print(f'负增长年份: {negative_years}年 ({negative_years/len(valid_years)*100:.1f}%)') # 打印负增长占比
# ==================== 高增长年份统计 ====================
# 高增长年份(>20%)
high_growth = (valid_years > 0.20).sum() # 统计增长率大于20%的年份数
print(f'高增长年份(>20%): {high_growth}年') # 打印高增长年份数
# ==================== 亏损年份统计 ====================
# 亏损年份(<0)
loss_years = (valid_years < 0).sum() # 统计负增长年份数
print(f'亏损年份: {loss_years}年') # 打印亏损年份数35.5.2 增长率可视化
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 营业利润与增长率双轴图
# =============================================================================
# 本代码块演示如何绘制双y轴图,同时展示营业利润和增长率。双y轴图适用于
# 两个变量量纲不同但需要对比分析的情况,例如绝对值(利润)和相对值(增长率)。
# ==================== 创建图形 ====================
fig, ax1 = plt.subplots(figsize=(14, 8)) # 创建图形,尺寸14×8英寸
# ==================== 左轴:营业利润 ====================
color1 = 'steelblue' # 定义左轴颜色
ax1.set_xlabel('年份', fontsize=12) # 设置x轴标签
ax1.set_ylabel('营业利润(亿元)', fontsize=12, color=color1) # 设置左y轴标签和颜色
# 绘制营业利润折线图
line1 = ax1.plot(profit.index, profit.values, color=color1, marker='o',
linewidth=2.5, label='营业利润')
ax1.tick_params(axis='y', labelcolor=color1) # 设置左y轴刻度标签颜色
ax1.grid(True, alpha=0.3, linestyle='--') # 显示网格,虚线样式
# ==================== 右轴:增长率 ====================
ax2 = ax1.twinx() # 创建共享x轴的第二个y轴
color2 = 'coral' # 定义右轴颜色
ax2.set_ylabel('增长率(%)', fontsize=12, color=color2) # 设置右y轴标签和颜色
# 绘制增长率折线图
line2 = ax2.plot(growth_rate_pct.index, growth_rate_pct.values, color=color2,
marker='s', linewidth=2.5, label='增长率')
ax2.tick_params(axis='y', labelcolor=color2) # 设置右y轴刻度标签颜色
ax2.axhline(y=0, color='gray', linestyle='--', linewidth=1, alpha=0.7) # 添加0参考线
# ==================== 添加阴影区域标记负增长 ====================
# fill_between()在两个y值之间填充颜色
ax2.fill_between(growth_rate_pct.index, 0, growth_rate_pct.values,
where=(growth_rate_pct.values < 0), # 条件:增长率小于0
color='red', alpha=0.2, label='负增长区间') # 红色半透明填充
# ==================== 标题和图例 ====================
plt.title('上市公司营业利润与增长率趋势', fontsize=16, pad=20) # 设置主标题
lines = line1 + line2 # 合并两条线的图例句柄
labels = [l.get_label() for l in lines] # 提取图例标签
ax1.legend(lines, labels, loc='upper left', fontsize=11) # 显示图例,位置在左上角
# ==================== 标注最高和最低点 ====================
max_year = growth_rate_pct.idxmax() # 找到最大增长率对应的年份
min_year = growth_rate_pct.idxmin() # 找到最小增长率对应的年份
max_value = growth_rate_pct[max_year] # 获取最大增长率值
min_value = growth_rate_pct[min_year] # 获取最小增长率值
# 标注最高点
ax2.annotate(f'最高: {max_value:.1f}%', # 注释文本
xy=(max_year, max_value), # 箭头指向的坐标
xytext=(max_year, max_value+5), # 文本位置
arrowprops=dict(facecolor='black', shrink=0.05, width=1.5, headwidth=8), # 箭头样式
fontsize=10, ha='center') # 字体大小和对齐方式
# 标注最低点
ax2.annotate(f'最低: {min_value:.1f}%', # 注释文本
xy=(min_year, min_value), # 箭头指向的坐标
xytext=(min_year, min_value-5), # 文本位置
arrowprops=dict(facecolor='black', shrink=0.05, width=1.5, headwidth=8), # 箭头样式
fontsize=10, ha='center') # 字体大小和对齐方式
fig.tight_layout() # 自动调整布局,避免元素重叠
plt.show() # 显示图形代码深度解析:
- 双y轴图(
twinx):ax1: 主y轴(利润)ax2: 次y轴(增长率)- 解决量纲不同的问题
- 填充区域(
fill_between):where: 条件判断- 标记负增长区域
- 视觉强调亏损年份
- 注释(
annotate):- 标注极值点
- 箭头指向
- 文字说明
35.5.3 增长趋势分解
理论背景:增长的趋势-周期分解
时间序列可以分解为:
\[ Y_t = T_t + C_t + S_t + \varepsilon_t \]
其中:
- \(T_t\): 趋势项(Trend): 长期方向
- \(C_t\): 循环项(Cycle): 多年波动
- \(S_t\): 季节项(Seasonal): 年内波动
- \(\varepsilon_t\): 随机项(Noise): 不可预测成分
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 增长趋势分解
# =============================================================================
# 本代码块演示如何使用移动平均分解增长率的趋势和波动成分。移动平均是
# 时间序列分析的基础工具,可以平滑短期波动,揭示长期趋势。
# ==================== 计算移动平均(平滑短期波动) ====================
ma_window = 3 # 设置移动平均窗口为3年
# rolling()创建滚动窗口,center=True使窗口居中
growth_ma = growth_rate_pct.rolling(window=ma_window, center=True).mean() # 计算3年移动平均
# ==================== 可视化趋势分解 ====================
fig, axes = plt.subplots(2, 1, figsize=(14, 10)) # 创建2×1子图布局
# 子图1:原始增长率vs移动平均
axes[0].plot(growth_rate_pct.index, growth_rate_pct.values,
marker='o', linewidth=2, label='原始增长率', alpha=0.7) # 绘制原始增长率
axes[0].plot(growth_ma.index, growth_ma.values,
linewidth=3, label=f'{ma_window}年移动平均', color='red') # 绘制移动平均线
axes[0].axhline(y=0, color='gray', linestyle='--', linewidth=1) # 添加0参考线
axes[0].set_title('增长率趋势分解', fontsize=14) # 设置标题
axes[0].set_ylabel('增长率(%)', fontsize=12) # 设置y轴标签
axes[0].legend(fontsize=11) # 显示图例
axes[0].grid(True, alpha=0.3) # 显示网格
# 子图2:利润趋势
axes[1].plot(profit.index, profit.values, marker='s', linewidth=2.5, color='steelblue') # 绘制营业利润
axes[1].set_title('营业利润趋势', fontsize=14) # 设置标题
axes[1].set_xlabel('年份', fontsize=12) # 设置x轴标签
axes[1].set_ylabel('营业利润(亿元)', fontsize=12) # 设置y轴标签
axes[1].grid(True, alpha=0.3) # 显示网格
# ==================== 添加趋势线 ====================
# 使用二次多项式拟合利润趋势
z = np.polyfit(profit.index, profit.values, 2) # 二次多项式拟合
p = np.poly1d(z) # 创建多项式函数
axes[1].plot(profit.index, p(profit.index), '--', color='red', linewidth=2,
label='趋势线(二次)') # 绘制趋势线
axes[1].legend(fontsize=11) # 显示图例
plt.tight_layout() # 调整布局
plt.show() # 显示图形
# ==================== 增长阶段识别 ====================
print('\n增长阶段分析:') # 打印标题
if len(growth_ma.dropna()) > 0: # 确保有有效的移动平均数据
recent_ma = growth_ma.dropna().iloc[-1] # 获取最近的移动平均值
print(f'最近{ma_window}年平均增长率: {recent_ma:.2f}%') # 打印近期平均增长率
# ==================== 判断增长阶段 ====================
if recent_ma > 20: # 平均增长率大于20%
stage = '高速增长期' # 判断为高速增长期
elif recent_ma > 10: # 平均增长率大于10%
stage = '稳定增长期' # 判断为稳定增长期
elif recent_ma > 0: # 平均增长率大于0
stage = '缓慢增长期' # 判断为缓慢增长期
else: # 平均增长率小于等于0
stage = '衰退期' # 判断为衰退期
print(f'当前阶段: {stage}') # 打印当前增长阶段35.6 增长率预测
35.6.1 简单趋势预测
补充说明:外推预测的局限性
基于历史增长率预测未来需要注意:
- 均值回归:
- 极高增长率难以持续
- 负增长可能反转
- 长期趋于GDP增速
- 基数效应:
- 基数小,增长率高
- 基数大,增长率低
- 需要结合绝对值
- 行业周期:
- 成长期:高增长
- 成熟期:稳定增长
- 衰退期:负增长
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 基于历史数据的增长率预测
# =============================================================================
# 本代码块演示如何基于历史增长率进行简单的外推预测。需要注意的是,
# 这种预测方法假设历史模式会延续,实际应用中需要结合行业分析和
# 宏观环境判断。
# ==================== 方法1:使用最近3年平均增长率预测 ====================
forecast_years = 3 # 预测未来3年
recent_growth = growth_rate_pct.dropna().tail(forecast_years).mean() # 计算最近3年平均增长率
# ==================== 方法2:使用整体CAGR预测 ====================
last_value = profit.dropna().iloc[-1] # 获取最后一年的利润值
forecast_values = [] # 创建空列表,存储预测值
print('未来' + str(forecast_years) + '年预测:') # 打印标题
print(f'\n方法1(最近{forecast_years}年平均):') # 打印方法说明
print(f' 预测增长率: {recent_growth:.2f}%') # 打印预测增长率
# ==================== 计算预测值 ====================
for i in range(1, forecast_years + 1): # 循环预测未来3年
# 使用复利公式: 预测值 = 期初值 × (1 + 增长率)^年数
forecast = last_value * (1 + recent_growth / 100) ** i # 计算第i年的预测值
forecast_values.append(forecast) # 将预测值添加到列表
print(f' 第{i}年: {forecast:.2f}亿元') # 打印第i年的预测值
# ==================== 可视化预测 ====================
fig, ax = plt.subplots(figsize=(12, 7)) # 创建图形
# 历史数据
ax.plot(profit.index, profit.values, marker='o', linewidth=2.5,
label='历史数据', color='steelblue') # 绘制历史数据折线图
# 预测数据
forecast_index = np.arange(profit.index[-1] + 1, profit.index[-1] + forecast_years + 1) # 生成预测年份索引
ax.plot(forecast_index, forecast_values, marker='s', linewidth=2.5,
linestyle='--', label='预测数据', color='coral') # 绘制预测数据折线图(虚线)
# 置信区间(假设±20%误差)
upper_bound = [v * 1.2 for v in forecast_values] # 计算上界(预测值×1.2)
lower_bound = [v * 0.8 for v in forecast_values] # 计算下界(预测值×0.8)
ax.fill_between(forecast_index, lower_bound, upper_bound,
color='coral', alpha=0.2, label='预测区间(±20%)') # 填充置信区间
ax.set_title('营业利润趋势外推预测', fontsize=14) # 设置标题
ax.set_xlabel('年份', fontsize=12) # 设置x轴标签
ax.set_ylabel('营业利润(亿元)', fontsize=12) # 设置y轴标签
ax.legend(fontsize=11) # 显示图例
ax.grid(True, alpha=0.3) # 显示网格
plt.tight_layout() # 调整布局
plt.show() # 显示图形
# ==================== 风险提示 ====================
print('\n警告:') # 打印标题
print('- 预测基于历史数据,未来可能不同') # 风险提示1
print('- 假设增长率恒定,实际会有波动') # 风险提示2
print('- 未考虑宏观环境和行业变化') # 风险提示3
print('- 仅供参考,不构成投资建议') # 免责声明35.7 投资应用案例
理论背景:企业价值与增长
增长率之所以值得投资,是因为它直接进入企业定价公式。根据戈登增长模型(Gordon Growth Model),股票价值为:
\[ P_0 = \frac{D_1}{r - g} \]
其中:
- \(P_0\): 当前股票价格
- \(D_1\): 下一期预期股息
- \(r\): 必要收益率(股权成本)
- \(g\): 股息永续增长率
关键洞见:
- 增长率越高,企业价值越大: g越接近r,股价越高
- 增长质量重于数量: 可持续的增长优于短期爆发
- 增长率递减: 随着企业成熟,增长率会逐步下降
带着这一框架,本节考察两个具体应用:用PEG比率判断估值,以及评估增长质量。
35.7.1 PEG比率分析
理论背景:PEG比率
PEG比率(Price/Earnings-to-Growth Ratio)由彼得·林奇提出,用于评估成长股的估值合理性:
\[ \text{PEG} = \frac{\text{PE}}{\text{增长率}} = \frac{P/E}{g} \]
解读标准:
- PEG < 1: 被低估
- PEG = 1: 合理估值
- PEG > 1: 被高估
优势:
- 考虑了增长因素
- 适合比较不同成长性的公司
- 避免”价值陷阱”
局限:
- 增长率预测困难
- 对负增长不适用
- 忽略了增长质量
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: PEG比率计算与分析
# =============================================================================
# 本代码块演示如何计算PEG比率并进行估值判断。PEG比率是成长股估值的核心
# 指标,它将市盈率与增长率结合起来,更全面地评估股票的估值水平。
# ==================== 假设当前股价和每股收益(EPS) ====================
stock_price = 50 # 假设当前股价为50元
current_eps = 2 # 假设每股收益为2元
# ==================== 计算PE比率 ====================
# PE比率 = 股价 / 每股收益
pe_ratio = stock_price / current_eps # 计算市盈率
# ==================== 使用CAGR作为预期增长率 ====================
expected_growth = cagr * 100 # 将CAGR转换为百分比形式
# ==================== 计算PEG比率 ====================
# PEG = PE比率 / 增长率
# 注意:当增长率为负时,PEG比率无意义
peg_ratio = pe_ratio / expected_growth if expected_growth > 0 else np.nan # 计算PEG比率
# ==================== 输出PEG分析结果 ====================
print('PEG比率分析:') # 打印标题
print(f'股价: {stock_price}元') # 打印股价
print(f'每股收益(EPS): {current_eps}元') # 打印每股收益
print(f'PE比率: {pe_ratio:.2f}') # 打印市盈率
print(f'预期增长率(CAGR): {expected_growth:.2f}%') # 打印预期增长率
# 如果PEG不是NaN,打印PEG值;否则打印"无定义"
print(f'PEG比率: {peg_ratio:.2f}' if not np.isnan(peg_ratio) else 'PEG比率: 无定义(负增长)')
# ==================== 估值判断 ====================
if not np.isnan(peg_ratio): # 确保PEG比率有效
print('\n估值判断:') # 打印标题
if peg_ratio < 0.8: # PEG小于0.8
valuation = '被低估' # 判断为低估
advice = '建议买入' # 建议买入
elif peg_ratio < 1.2: # PEG在0.8到1.2之间
valuation = '合理估值' # 判断为合理估值
advice = '建议持有' # 建议持有
else: # PEG大于等于1.2
valuation = '被高估' # 判断为高估
advice = '建议谨慎' # 建议谨慎
print(f'估值水平: {valuation}') # 打印估值水平
print(f'投资建议: {advice}') # 打印投资建议
# ==================== 情景分析 ====================
print('\n情景分析(不同增长率下的PEG):') # 打印标题
# 定义不同的增长率情景
growth_scenarios = [5, 10, 15, 20, 25, expected_growth] # 增长率情景(%)列表
for g in growth_scenarios: # 遍历各情景
peg_scenario = pe_ratio / g # 计算该情景下的PEG比率
# 根据PEG值判断估值状态
status = '低估' if peg_scenario < 1 else '合理' if peg_scenario < 1.2 else '高估' # 判断估值状态
print(f'增长率={g:5.1f}%: PEG={peg_scenario:.2f} ({status})') # 打印情景分析结果35.7.2 增长质量评估
补充说明:如何评估增长质量
高质量增长的特征:
- 可持续性: 来源于核心竞争力
- 盈利性: 增长带来利润率提升
- 现金流: 利润增长伴随现金流改善
- 稳定性: 增长率波动较小
- 内源性: 主要靠自身造血,而非并购
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 增长质量综合评估
# =============================================================================
# 本代码块演示如何从多个维度评估企业增长质量。增长质量比增长率本身更重要,
# 高质量的增长可持续,低质量的增长难以维持。
# ==================== 收集增长相关指标 ====================
growth_metrics = {
'平均增长率': growth_rate.mean() * 100, # 计算平均增长率
'增长率标准差': growth_rate.std() * 100, # 计算增长率标准差(波动性)
'正增长概率': (growth_rate > 0).sum() / len(growth_rate.dropna()) * 100, # 计算正增长年份占比
'CAGR': cagr * 100, # 复合年均增长率
'最新增长率': growth_rate_pct.iloc[-1] # 最近的增长率
}
# ==================== 转换为DataFrame ====================
df_metrics = pd.DataFrame(list(growth_metrics.items()), # 将字典转换为列表
columns=['指标', '值(%)']) # 设置列名
# ==================== 输出增长质量指标 ====================
print('增长质量评估:') # 打印标题
print(df_metrics.to_string(index=False)) # 打印指标表,不显示索引
# ==================== 计算综合得分 ====================
# 1. 稳定性得分(标准差越小,得分越高)
# 标准差最大按50考虑,标准差越小得分越高
stability_score = 100 - min(growth_metrics['增长率标准差'], 50) / 50 * 100 # 计算稳定性得分
# 2. 持续性得分(正增长概率)
consistency_score = growth_metrics['正增长概率'] # 正增长概率即持续性得分
# 3. 动能得分(最新增长率)
# 最新增长率最大按30考虑,超过30按30算
momentum_score = max(min(growth_metrics['最新增长率'], 30) / 30 * 100, 0) # 计算动能得分
# ==================== 综合得分(加权平均) ====================
quality_score = (stability_score * 0.3 + # 稳定性权重30%
consistency_score * 0.4 + # 持续性权重40%
momentum_score * 0.3) # 动能权重30%
# ==================== 输出得分 ====================
print(f'\n增长质量得分:') # 打印标题
print(f'稳定性得分: {stability_score:.1f}/100') # 打印稳定性得分
print(f'持续性得分: {consistency_score:.1f}/100') # 打印持续性得分
print(f'动能得分: {momentum_score:.1f}/100') # 打印动能得分
print(f'综合得分: {quality_score:.1f}/100') # 打印综合得分
# ==================== 评级 ====================
if quality_score >= 80: # 综合得分>=80
grade = '优秀 (A)' # 评级为优秀
elif quality_score >= 60: # 综合得分>=60
grade = '良好 (B)' # 评级为良好
elif quality_score >= 40: # 综合得分>=40
grade = '一般 (C)' # 评级为一般
else: # 综合得分<40
grade = '较差 (D)' # 评级为较差
print(f'增长质量评级: {grade}') # 打印最终评级35.8 本章小结
要点:
- 财务数据取数的链条是:读取 Excel—设索引—按科目行名取行—整理年份索引—排序,每一步都影响后续增长率口径;
pct_change()计算 \((X_t - X_{t-1}) / X_{t-1}\),首期为 NaN,与手动逐期计算结果一致;- 单期平均增长率会被高波动年份拉偏,衡量长期成长性应优先使用复合年均增长率 CAGR;
- 增长率的分析价值在于配套指标:正增长占比看持续性、标准差看稳定性、PEG 看估值匹配度;
- 取数口径(营业利润还是净利润)必须与指标名称严格对应,行名以数据源实际为准。
易错点:
- 忘记
sort_index()升序排序就计算pct_change(),把”较上年”算成了”较下年”; - 把首期的 NaN 当作缺失数据删除或填 0,曲解增长率序列;
- 未核对数据源行名就按教材编号取数,导致取到错误科目(见”口径说明”);
- 用算术平均增长率概括高波动序列,得出脱离 CAGR 的乐观(或悲观)结论。
35.9 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:某公司营业利润 2020-2023 年依次为 100、120、90、108 亿元。不运行代码,写出
pd.Series([100, 120, 90, 108]).pct_change()的完整输出,并计算这四年的 CAGR。参考答案(先写下你的预测再点开)
解题思路:
pct_change()逐期计算 \((X_t - X_{t-1})/X_{t-1}\),分母永远是上一期的实际值。2020 年为首期,没有上期数据,输出 NaN(不是 0,也不是缺失报错);2021 年 \((120-100)/100 = 0.2\);2022 年 \((90-120)/120 = -0.25\)——跌 30 亿元,但因基数只有 120,跌幅是 25%;2023 年 \((108-90)/90 = 0.2\)——同样涨 18 亿元,基数 90 使涨幅达到 20%。CAGR 用首末两期与增长期数计算:2020 到 2023 共 3 个增长期,\(\text{CAGR} = (108/100)^{1/3} - 1 \approx 2.60\%\)。注意 CAGR(2.6%)明显低于单期增长率的算术平均(\((0.2-0.25+0.2)/3 = 5\%\)),因为 2022 年的下跌改变了复利路径,这一点正是第 2 题的主题。# 验证脚本:pct_change输出与CAGR计算 import pandas as pd # 导入Pandas数据分析库 profit_series = pd.Series([100, 120, 90, 108]) # 2020-2023年营业利润(亿元) print(profit_series.pct_change()) # pct_change逐期计算增长率,首期为NaN print('CAGR: {:.2%}'.format((108 / 100) ** (1 / 3) - 1)) # 复合年均增长率,共3个增长期预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
0 NaN 1 0.20 2 -0.25 3 0.20 dtype: float64 CAGR: 2.60%回扣本章:对应本章小结“要点”第 2 条(
pct_change()首期为 NaN,与手动逐期计算一致)与“易错点”第 2 条(首期 NaN 不是缺失数据,不得删除口径曲解或填 0)。概念辨析:单期平均增长率((0.2 - 0.25 + 0.2)/3 = 5%)与 CAGR(约 2.6%)为何不相等?两者各自适合回答什么问题?
参考答案(点开前请先独立完成)
解题思路:不相等的根源在于增长率作用在不断变化的基数上,而算术平均无视这一点。单期平均把三个增长率等权相加:\((0.2 + (-0.25) + 0.2)/3 = 5\%\);CAGR 则按复利还原财富路径:\((1+0.2)\times(1-0.25)\times(1+0.2) = 1.08\),开三次方得 \(1.08^{1/3} - 1 \approx 2.6\%\)。二者相差约 2.4 个百分点,全部来自 2022 年那 25% 的下跌——跌 25% 之后需要涨 33.3% 才能回本,而后续只涨了 20%,算术平均对此毫无察觉,几何口径才如实反映。波动越大,这一“波动拖累”越严重(极限情形见第 5 题)。适用问题:单期平均适合回答“典型年份的增长幅度如何、各年增长波动多大”——它保留了逐期信息,可配套计算标准差、正增长占比等稳定性指标;CAGR 适合回答“整个期间的财富以多快的复合速度增长”——它是期初到期末的等效恒定增长率,是比较公司长期成长性、作长期预测与估值输入(如 PEG 中的 g)时的正确口径。实务报告应两者并列,并说明波动情况。
回扣本章:对应本章小结“要点”第 3 条(单期平均会被高波动年份拉偏,衡量长期成长性应优先使用 CAGR)与“易错点”第 4 条。
概念辨析:本章章名为”净利润增长率”,平台代码取用的却是营业利润行(见”口径说明”)。请说明两者的差异,并说明若要改为净利润口径,代码中哪一行必须修改、修改前应先做什么核对。
参考答案(点开前请先独立完成)
解题思路:两者的差异是利润表层次上的。营业利润是经营成果:营业收入减去营业成本、期间费用、税金及附加等,反映主营业务的盈利能力,不含营业外收支;净利润是最终归属口径:营业利润加营业外收支形成利润总额,再扣除所得税费用(及少数股东损益调整)后的余额。两者之间隔着营业外收支、减值损失与所得税三个环节,个别年份可能明显背离。口径不一致的来源是数据源:平台数据文件实际提供的行名是“四、营业利润(亿元)”,平台代码
df.loc[...]取的正是这一行,而章名沿用了“净利润增长率”。若要改为净利润口径,必须修改的一行是df = df.loc['四、营业利润(亿元)'],把行名换成数据源中的净利润行(“六、净利润(亿元)”)。修改前应先核对数据源的实际行名:读取后先查看df.index(或df.head())确认该文件对科目行的实际命名与编号——数据源的编号体系(“四、营业利润”“六、净利润”)与教材通行的简化编号(“二、营业利润”“四、净利润”)不同,不核对就按教材编号取数会取到错误科目甚至报错。回扣本章:对应本章小结“要点”第 5 条(取数口径必须与指标名称严格对应,行名以数据源实际为准)与“易错点”第 3 条(未核对行名就按教材编号取数)。
变式任务:在平台上把
df.loc[...]的行名改为数据源中的”六、净利润(亿元)“,重跑增长率序列,对比营业利润口径与净利润口径的逐年差异,并思考:哪些费用或营业外项目会造成两者背离?参考答案(点开前请先独立完成)
解题思路:改造只有一处——把 列表 35.2 中
df.loc[...]的行名换成“六、净利润(亿元)”,其余索引整理与pct_change()流程完全复用。以下为本机于 2026-08-27 通过平台 OSS 直链实拉数据的实测结果(该直链若日后失效,可在教学平台完成本题,或改用从同一直链下载留存的本地快照,方法与流程完全一致)。两口径的总体形态高度同步:净利润行 1998 年 1.47 亿元增至 2022 年 653.75 亿元,与营业利润行同样是 25 期、24 个年度增长率且全为正;但逐年数值有差异,锚点年份对比如下——1999 年净利润增长 46.94%,比营业利润口径的 61.64% 低约 14.7 个百分点(全序列最大背离);2007 年净利润 83.54% 反而高于营业利润的 81.95%,且两个口径的年度最高增长都出现在 2007 年;2015 年两口径同为全序列最低(净利润 1.14%、营业利润 0.25%);2022 年分别为 17.33% 与 17.56%,几乎一致。背离来源的思考:营业利润与净利润之间隔着营业外收支(处置收益、捐赠、赔款等一次性项目)、资产减值与信用减值损失、所得税费用及税率变化(如税收优惠到期、递延所得税调整)等环节——这些项目在多数年份影响温和,但在个别年份(如 1999 年)可能集中兑现,使两个口径的增长率明显分道扬镳;做成长性分析时应明确所谈的是哪一个口径,避免“章名净利润、数据营业利润”式的口径混同。# 变式脚本:净利润口径与营业利润口径的增长率对比(OSS直链本机实测) import pandas as pd # 导入Pandas数据分析库 oss_url = 'https://huoran.oss-cn-shenzhen.aliyuncs.com/20230410/xls/1645298865123385344.xls' # 平台利润表文件的OSS直链 df = pd.read_excel(oss_url) # 读取远程Excel利润表 df.set_index('Unnamed: 0', inplace=True) # 把首列科目名设为索引 for row_name in ['四、营业利润(亿元)', '六、净利润(亿元)']: # 营业利润与净利润两种口径 row = df.loc[row_name] # 按行名提取该科目行(得到Series) row.index = row.index.str[:4].astype(int) # 年份索引取前4个字符并转为整数 growth = row.sort_index().pct_change() # 按年份升序后计算同比增长率 print(row_name, '期数:', len(row)) # 输出该口径的期数 print(' 1999: {:+.2%} 2007: {:+.2%} 2015: {:+.2%} 2022: {:+.2%}'.format(growth.loc[1999], growth.loc[2007], growth.loc[2015], growth.loc[2022])) # 输出锚点年份增长率 print(' 最高: {:+.2%}(@{}) 最低: {:+.2%}(@{}) 负增长年数: {}'.format(growth.max(), growth.idxmax(), growth.min(), growth.idxmin(), int((growth < 0).sum()))) # 输出极值与负增长年数预期输出(本机 peter 环境于 2026-08-27 联网实拉 OSS 直链运行的结果,具体以平台运行结果为准):
四、营业利润(亿元) 期数: 25 1999: +61.64% 2007: +81.95% 2015: +0.25% 2022: +17.56% 最高: +81.95%(@2007) 最低: +0.25%(@2015) 负增长年数: 0 六、净利润(亿元) 期数: 25 1999: +46.94% 2007: +83.54% 2015: +1.14% 2022: +17.33% 最高: +83.54%(@2007) 最低: +1.14%(@2015) 负增长年数: 0注意:以上为本题变式的独立代码;列表 35.2 对应的平台原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 5 条与“易错点”第 3 条——行名以数据源实际为准、口径与指标名称严格对应;换口径只需改
df.loc[...]一行,算法本身完全复用。思考题:某公司近四年利润增长率依次为 +60%、-40%、+60%、-40%。算术平均增长率为 +10%,但股东财富实际上缩水了。请用几何平均(或 CAGR)解释这一”增长率幻觉”,并说明它在投资分析中的警示意义。
参考答案(点开前请先独立完成)
解题思路:先把财富路径摆出来。四个增长率作用于财富乘数 \(1.6\times0.6\times1.6\times0.6 = 0.9216\),即四年后只剩期初的 92.16%,累计缩水 7.84%;几何平均口径的年复合增长率为 \(0.9216^{1/4} - 1 \approx -2.02\%\)——每年实打实亏约 2%。而算术平均 \((+60\%-40\%+60\%-40\%)/4 = +10\%\) 给出的是正值,这就是“增长率幻觉”:涨跌在算术上对称(+60 对 -40 平均 +10),在财富上却不对称——跌 40% 之后需要约 +66.7% 才能回本,+60% 都不够;增长率作用在逐期变化的基数上,只有把 \((1+g)\) 连乘的几何平均(等价于 CAGR)才如实还原财富路径,波动越大,算术平均与几何平均的差距越大(这被称为波动拖累)。警示意义:其一,评估公司与基金的长期成长性必须看 CAGR 或终端财富,不能看算术平均增长率——高波动加正的算术平均完全可能对应负的真实回报;其二,以增长率为输入的估值与指标(如 PEG 的 g、DCF 的永续增长率)若误用算术平均,会系统性高估价值;其三,看到“平均增长 10%”的宣传数字时,应追问增长率的波动路径与口径(算术还是几何),这本身就是识别业绩包装的一道筛查。
回扣本章:对应本章小结“要点”第 3 条(单期平均会被高波动年份拉偏,长期成长性应优先使用 CAGR)与“易错点”第 4 条(用算术平均概括高波动序列会得出脱离 CAGR 的乐观结论)。