35  上市公司净利润增长率的计算

35.1 引言增长率的价值发现功能

净利润增长率的重要性:

净利润增长率是衡量上市公司盈利能力和成长性的核心指标,在价值投资和成长投资中都具有关键作用。本章从财务数据出发,完成增长率的计算、复核、可视化与投资解读。

35.2 本章学习目标

先修内容:阅读本章前,建议先完成第 章节 10 章(Series 与 DataFrame 基础)、第 章节 11 章(数据框索引与 loc 取数)、第 章节 14 章(读取 Excel 文件)与第 章节 33 章(均值、标准差等统计量)。

  1. 从在线 Excel 利润表中读取数据、设置索引并提取指定科目行的方法;
  2. 年份索引的字符串截取、类型转换与排序等时间序列预处理操作;
  3. pct_change() 计算同比增长率,及其与手动计算的一致性验证;
  4. 复合年均增长率(CAGR)与单期平均增长率的区别及适用场景;
  5. 增长率指标在成长性评估、PEG 估值等投资分析中的应用。

35.3 数据读取与预处理

35.3.1 财务数据获取

补充说明:上市公司财务报告体系

中国上市公司财务报告披露周期:

  1. 季度报告(Q1, Q2, Q3):
    • 每季度结束后1个月内披露
    • 包含简化的资产负债表、利润表
    • 不需要审计
  2. 年度报告(Q4/年报):
    • 会计年度结束后4个月内披露
    • 包含完整的三张报表
    • 必须经过审计
    • 附详细的管理层讨论与分析
  3. 业绩快报与预告:
    • 业绩快报:实际数据但未经审计
    • 业绩预告:预计数据范围
    • 时间早于正式报告

利润表的层次结构:

一、营业收入
减:营业成本
    营业税金及附加
    销售费用
    管理费用
    财务费用
-----------------------------------
二、营业利润(Operating Profit) ← 本章使用(数据源行名为"四、营业利润(亿元)")
加:营业外收入
减:营业外支出
-----------------------------------
三、利润总额(Total Profit)
减:所得税费用
-----------------------------------
四、净利润(Net Profit) ← 更常用(数据源行名为"六、净利润(亿元)")

注:上表采用教材通行的简化编号;本章数据文件的实际行名为
"四、营业利润(亿元)""五、利润总额(亿元)""六、净利润(亿元)",
两套编号体系不同,用df.loc[]取数时以数据源实际行名为准。
列表 35.1: 读取上市公司财务数据
# =============================================================================
# 题目: 读取上市公司财务数据
# =============================================================================
# 本代码块演示如何从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}')  # 打印数据维度(行数,列数)

代码深度解析:

  1. pd.read_excel()参数:
    • 默认读取第一个sheet
    • header=0: 第一行为列名
    • index_col: 指定索引列
  2. 在线数据读取:
    • 支持HTTP/HTTPS URL
    • 自动处理编码
    • 适合快速原型开发
  3. 数据结构检查:
    • shape: (行数, 列数)
    • head(): 前几行数据预览
    • type(): 确认DataFrame类型

35.3.2 数据提取与转换

补充说明:时间序列的索引处理

时间序列分析的关键步骤:

  1. 索引设置:
    • set_index(): 将某列设为索引
    • 索引必须是唯一的
    • 时间索引便于切片操作
  2. 索引类型:
    • 字符串: ‘2020’, ‘2021’
    • 整数: 2020, 2021
    • Datetime: 2020-01-01(推荐)
  3. 索引排序:
    • sort_index(): 按索引排序
    • 升序或降序
    • 确保时间顺序正确

口径说明:本章章名沿用平台任务名”净利润增长率”,但平台数据源(上述利润表文件)实际提供并被取用的是”四、营业利润(亿元)“这一行,因此本章演示的是同一增长率算法在营业利润上的应用。若要计算真正口径的净利润增长率,只需把平台代码中 df.loc[...] 的行名换成数据源中的净利润行(如”六、净利润(亿元)“)即可迁移;取数前应先核对数据源的实际行名。

任务要求:将下方代码原样输入教学平台并运行(注释可省略)。该代码读取远程利润表 Excel 文件,将首列设为索引后提取”四、营业利润(亿元)“所在行,把年份索引整理为四位整数并升序排序,最后用 pct_change() 计算增长率并输出。

列表 35.2: 平台原始代码
# 注:该代码块读取教学平台的 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 个年度增长率全为正。

代码深度解析:

  1. loc[]索引器:
    • 基于标签的索引
    • df.loc[row_label]: 选择行
    • 支持切片和布尔索引
  2. 字符串处理:
    • .str[:4]: 切片前4个字符
    • .astype(int): 类型转换
    • 链式操作提高效率
  3. 数据清洗:
    • pd.to_numeric(): 转换为数值
    • errors='coerce': 无法转换的设为NaN
    • 便于后续处理缺失值

35.4 计算增长率

35.4.1 增长率计算方法

理论背景:增长率的各种计算方式

  1. 简单增长率: \[ g_t = \frac{X_t - X_{t-1}}{X_{t-1}} \]
    • 适用于:任意相邻两期
    • 优点:计算简单
    • 缺点:受基数影响
  2. 复合年均增长率(CAGR): \[ \text{CAGR} = \left(\frac{X_n}{X_0}\right)^{\frac{1}{n}} - 1 \]
    • 适用于:长期平均增长
    • 优点:平滑短期波动
    • 缺点:忽略中间路径
  3. 对数差分增长率: \[ g_t = \ln(X_t) - \ln(X_{t-1}) \]
    • 适用于:连续复利假设
    • 优点:对称性,可加性
    • 缺点:近似值
列表 35.3: 计算净利润增长率
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 计算净利润增长率
# =============================================================================
# 本代码块演示如何使用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())}')  # 验证结果

代码深度解析:

  1. pct_change()函数:
    • 计算百分比变化
    • 默认periods=1
    • 第一个值为NaN(无上期数据)
  2. 格式化输出:
    • {:+.2f}: 带符号,2位小数
    • +表示正数
    • -表示负数
  3. 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\): 年数
列表 35.4: 计算复合年均增长率(CAGR)
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 计算复合年均增长率(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 增长率统计分析

净利润增长率的金融意义:

在解读统计结果之前,先明确增长率数字背后的四层含义:

  1. 成长性评估:
    • 识别高增长企业(成长股)
    • 区分周期性增长与趋势性增长
    • 评估增长可持续性
  2. 估值依据:
    • DCF模型的关键输入
    • PEG比率的核心要素
    • 相对估值的基础
  3. 投资决策:
    • 价值投资:低增长+低估值
    • 成长投资:高增长+合理估值
    • GARP策略:合理价格增长
  4. 业绩预警:
    • 增长率放缓可能预示业绩拐点
    • 负增长需警惕企业衰退
    • 波动率反映经营风险

下面的统计分析将依次回答”增长有多快、有多稳、是否可持续”这三个问题。

列表 35.5: 增长率统计分析
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 增长率统计分析
# =============================================================================
# 本代码块演示如何对增长率进行统计分析,包括均值、最大值、最小值、标准差
# 等。这些统计量帮助我们了解企业增长的稳定性和波动性。

# ==================== 统计分析 ====================
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 增长率可视化

列表 35.6
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 营业利润与增长率双轴图
# =============================================================================
# 本代码块演示如何绘制双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()  # 显示图形

代码深度解析:

  1. 双y轴图(twinx):
    • ax1: 主y轴(利润)
    • ax2: 次y轴(增长率)
    • 解决量纲不同的问题
  2. 填充区域(fill_between):
    • where: 条件判断
    • 标记负增长区域
    • 视觉强调亏损年份
  3. 注释(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): 不可预测成分
列表 35.7
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 增长趋势分解
# =============================================================================
# 本代码块演示如何使用移动平均分解增长率的趋势和波动成分。移动平均是
# 时间序列分析的基础工具,可以平滑短期波动,揭示长期趋势。

# ==================== 计算移动平均(平滑短期波动) ====================
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 简单趋势预测

补充说明:外推预测的局限性

基于历史增长率预测未来需要注意:

  1. 均值回归:
    • 极高增长率难以持续
    • 负增长可能反转
    • 长期趋于GDP增速
  2. 基数效应:
    • 基数小,增长率高
    • 基数大,增长率低
    • 需要结合绝对值
  3. 行业周期:
    • 成长期:高增长
    • 成熟期:稳定增长
    • 衰退期:负增长
列表 35.8: 基于历史数据的增长率预测
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 基于历史数据的增长率预测
# =============================================================================
# 本代码块演示如何基于历史增长率进行简单的外推预测。需要注意的是,
# 这种预测方法假设历史模式会延续,实际应用中需要结合行业分析和
# 宏观环境判断。

# ==================== 方法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: 被高估

优势:

  • 考虑了增长因素
  • 适合比较不同成长性的公司
  • 避免”价值陷阱”

局限:

  • 增长率预测困难
  • 对负增长不适用
  • 忽略了增长质量
列表 35.9: PEG比率计算与分析
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# =============================================================================
# 题目: 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 增长质量评估

补充说明:如何评估增长质量

高质量增长的特征:

  1. 可持续性: 来源于核心竞争力
  2. 盈利性: 增长带来利润率提升
  3. 现金流: 利润增长伴随现金流改善
  4. 稳定性: 增长率波动较小
  5. 内源性: 主要靠自身造血,而非并购
列表 35.10: 增长质量综合评估
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 增长质量综合评估
# =============================================================================
# 本代码块演示如何从多个维度评估企业增长质量。增长质量比增长率本身更重要,
# 高质量的增长可持续,低质量的增长难以维持。

# ==================== 收集增长相关指标 ====================
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 本章小结

要点:

  1. 财务数据取数的链条是:读取 Excel—设索引—按科目行名取行—整理年份索引—排序,每一步都影响后续增长率口径;
  2. pct_change() 计算 \((X_t - X_{t-1}) / X_{t-1}\),首期为 NaN,与手动逐期计算结果一致;
  3. 单期平均增长率会被高波动年份拉偏,衡量长期成长性应优先使用复合年均增长率 CAGR;
  4. 增长率的分析价值在于配套指标:正增长占比看持续性、标准差看稳定性、PEG 看估值匹配度;
  5. 取数口径(营业利润还是净利润)必须与指标名称严格对应,行名以数据源实际为准。

易错点:

  1. 忘记 sort_index() 升序排序就计算 pct_change(),把”较上年”算成了”较下年”;
  2. 把首期的 NaN 当作缺失数据删除或填 0,曲解增长率序列;
  3. 未核对数据源行名就按教材编号取数,导致取到错误科目(见”口径说明”);
  4. 用算术平均增长率概括高波动序列,得出脱离 CAGR 的乐观(或悲观)结论。

35.9 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 输出预测:某公司营业利润 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)。

  2. 概念辨析:单期平均增长率((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 条。

  3. 概念辨析:本章章名为”净利润增长率”,平台代码取用的却是营业利润行(见”口径说明”)。请说明两者的差异,并说明若要改为净利润口径,代码中哪一行必须修改、修改前应先做什么核对。

    参考答案(点开前请先独立完成)

    解题思路:两者的差异是利润表层次上的。营业利润是经营成果:营业收入减去营业成本、期间费用、税金及附加等,反映主营业务的盈利能力,不含营业外收支;净利润是最终归属口径:营业利润加营业外收支形成利润总额,再扣除所得税费用(及少数股东损益调整)后的余额。两者之间隔着营业外收支、减值损失与所得税三个环节,个别年份可能明显背离。口径不一致的来源是数据源:平台数据文件实际提供的行名是“四、营业利润(亿元)”,平台代码 df.loc[...] 取的正是这一行,而章名沿用了“净利润增长率”。若要改为净利润口径,必须修改的一行是 df = df.loc['四、营业利润(亿元)'],把行名换成数据源中的净利润行(“六、净利润(亿元)”)。修改前应先核对数据源的实际行名:读取后先查看 df.index(或 df.head())确认该文件对科目行的实际命名与编号——数据源的编号体系(“四、营业利润”“六、净利润”)与教材通行的简化编号(“二、营业利润”“四、净利润”)不同,不核对就按教材编号取数会取到错误科目甚至报错。

    回扣本章:对应本章小结“要点”第 5 条(取数口径必须与指标名称严格对应,行名以数据源实际为准)与“易错点”第 3 条(未核对行名就按教材编号取数)。

  4. 变式任务:在平台上把 df.loc[...] 的行名改为数据源中的”六、净利润(亿元)“,重跑增长率序列,对比营业利润口径与净利润口径的逐年差异,并思考:哪些费用或营业外项目会造成两者背离?

    参考答案(点开前请先独立完成)

    解题思路:改造只有一处——把 列表 35.2df.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[...] 一行,算法本身完全复用。

  5. 思考题:某公司近四年利润增长率依次为 +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 的乐观结论)。