35.上市公司营业利润增长率的计算

本章概要

  • 学习材料:上海汽车年末累计营业利润及披露版本。
  • 本章任务:运行 lst-ch35-cagr-demo,固定稳定正值端点计算CAGR和相邻端点敏感性。
  • 完成后你将得到:起止值、完整年数、CAGR、反推末值和敏感性表。
  • 自我检查:用(1+g)^n反推并核对营业利润口径;端点非正或版本冲突时先检查原因。
  • 拓展练习:把营业利润增长拓展应用到另一家长三角制造企业。

本章学习目标

  • 理解营业利润增长率在经营趋势分析中的核心作用
  • 掌握 pct_change() 计算增长率的方法
  • 学会计算复合年均增长率(CAGR)
  • 区分营业利润增长率与净利润、EPS 增长率及 PEG 所需口径
  • 能够对增长率进行可视化趋势分解

为什么关注营业利润增长率?

营业利润增长率反映利润表“营业利润”项目的变化,是观察经营盈利能力成长趋势的起点;该项目还可能包含投资收益、公允价值变动、减值与资产处置等非核心影响。

  • 成长性评估: 识别高增长企业(成长股)
  • 经营诊断: 结合收入、成本与费用变化识别利润驱动因素
  • 投资决策: 区分价值股 vs 成长股
  • 业绩预警: 增长率放缓可能预示业绩拐点

戈登增长模型:增长率如何决定股价

根据戈登增长模型(Gordon Growth Model):

\[ \large P_0 = \frac{D_1}{r - g} \]

  • \(P_0\): 当前股票价格
  • \(D_1\): 下一期预期股息
  • \(r\): 必要收益率(股权成本)
  • \(g\): 股息永续增长率(不是营业利润增长率,二者不可替代)

增长率的三个核心认知

认知 含义
股息增长率越高,模型价值越大 \(g<r\) 等前提下,股息增长率 \(g\) 越接近 \(r\),戈登模型中 \(P_0\) 越高
增长质量重于数量 可持续的增长优于短期爆发
增长率递减 成熟企业常见但非必然的生命周期倾向,须用企业与行业依据检验

营业利润增长率的四大分析意义

维度 应用
成长性评估 区分周期性增长与趋势性增长
经营诊断 结合收入、成本与期间费用解释利润变化
投资决策 价值投资 / 成长投资 / GARP策略
业绩预警 负增长需警惕企业衰退

中国上市公司财务报告披露体系

报告类型 披露时间 特点
第一/第三季度报告 季度结束后约1个月内 简化披露,通常不检查
半年度报告 上半年结束后约2个月内 独立半年度报告,不应与Q1/Q3合并称季度报告
年度报告 会计年度结束后4个月内 完整三张报表,必须检查
业绩快报 早于正式报告 实际数据但未经检查
业绩预告 早于正式报告 预计数据范围

利润表的层次结构

一、营业收入
减:营业成本、税金及附加、销售/管理/研发/财务费用
加减:其他收益、投资收益、公允价值变动、信用/资产减值、资产处置等
───────────────────────────
二、营业利润 (Operating Profit) ← 本章使用
加:营业外收入  减:营业外支出
───────────────────────────
三、利润总额 (Total Profit)
减:所得税费用
───────────────────────────
四、净利润 (Net Profit) ← 更常用指标

口径边界:本章及平台练习提取的是营业利润。营业利润不等于“核心经营利润”、净利润或 EPS;识别核心经营盈利须展示从毛利到营业利润的完整调节并剔除经核验的非核心项目,因此本章结果不能直接支持净利润增长口径的 PEG 推断。

第一步:读取上市公司财务数据

Listing 1: 读取上市公司财务数据
展开真实财务数据要求与 请检查数据 检查代码
import pandas as pd  # 导入 Pandas 以读取和核验财务数据表
import numpy as np  # 导入 NumPy 以支持后续增长率数值计算
import matplotlib.pyplot as plt  # 导入绘图库以生成营业利润分析图
import os  # 导入操作系统接口,用于前置核验财务文件读取权限
from pathlib import Path  # 导入路径工具,用于检查规定的本地数据挂载

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']  # 保证业务图表中文标签可读
plt.rcParams['axes.unicode_minus'] = False  # 保证增长率负号正常显示

# 真实财务数据要求:构建阶段只检查挂载,不联网、不构造公司财务数值
financial_path = Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5')  # 声明可用财务报表文件路径
required_fields = ['ts_code', 'end_date', 'operate_profit']  # 声明后续真实计算所需字段

status = '请检查数据'  # 只有全部数据要求通过后才开始计算
reason = 'unverified'  # 初始化便于核对的停止原因
usable_count = 0  # 初始化非空营业利润记录数,避免填造公司数据
missing_fields = []  # 初始化缺失字段清单,供最终状态统一输出
Listing 2: 验证上市公司财务数据要求
展开真实财务数据状态机
if not financial_path.is_file():  # 缺失路径或非文件挂载均停止分析
    reason = 'missing_or_not_file'  # 记录精确的路径状态原因
elif not os.access(financial_path, os.R_OK):  # 文件无读取权限时在解析前停止
    reason = 'not_readable'  # 记录权限前置检查失败且不披露敏感内容
else:  # 前置检查通过后直接读取,使解析错误显式令构建失败
    financial_data = pd.read_hdf(financial_path, key='financial_data', columns=['order_book_id','quarter','profit_from_operation']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','profit_from_operation':'operate_profit'})  # 只读取必需财报字段并标准化
    financial_data['end_date'] = pd.PeriodIndex(financial_data['end_date'].str.upper(), freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
    missing_fields = [field for field in required_fields if field not in financial_data.columns]  # 列出缺失字段名
    if missing_fields:  # 缺少必需字段时停止真实增长率计算
        reason = 'missing_fields'  # 记录字段要求未满足
    else:  # 字段齐全时继续核验营业利润可用记录
        usable_count = int(financial_data['operate_profit'].notna().sum())  # 统计非空营业利润记录数
        if usable_count > 0:  # 至少一条可用记录时才允许进入准备状态
            status = 'READY'  # 标记真实数据要求已满足
            reason = 'ready'  # 记录可继续受控分析的原因
        else:  # 没有可用营业利润记录时停止分析
            reason = 'no_usable_operate_profit_records'  # 记录无可用业务记录
Listing 3: 输出上市公司财务数据要求状态
展开约定状态输出
print({  # 统一输出状态、约定依据与禁止伪造策略
    'status': status,  # 报告是否满足真实数据准备条件
    'reason': reason,  # 报告当前状态对应的便于核对原因
    'path': str(financial_path),  # 报告规定数据挂载路径
    'required_fields': required_fields,  # 报告营业利润分析的字段要求
    'missing_fields': missing_fields,  # 报告需要由数据管理补齐的字段
    'usable_operate_profit_records': usable_count,  # 报告可用于后续分析的非空记录数
    'result_policy': '仅在数据检查完成后使用真实记录计算;数据有误时不输出公司财务数值或业务结论',  # 说明结果使用条件
})  # 完成一次且仅一次的数据要求状态输出
{'status': 'READY', 'reason': 'ready', 'path': '/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5', 'required_fields': ['ts_code', 'end_date', 'operate_profit'], 'missing_fields': [], 'usable_operate_profit_records': 304496, 'result_policy': '仅在数据检查完成后使用真实记录计算;数据有误时不输出公司财务数值或业务结论'}

pd.read_excel() 核心参数

参数 说明 默认值
sheet_name 指定读取的Sheet 第一个Sheet
header 指定列名所在行 0(第一行)
index_col 指定索引列 None
  • 支持 HTTP/HTTPS URL 直接读取远程文件
  • 自动处理编码问题
  • 返回 DataFrame 对象

运行前预测|平台任务代码

  • 输入预测:运行前先写出 dfresult 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到result 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务代码

展开完整代码(投影默认折叠)
# 注:该代码块读取远程平台数据文件,且变量名与下方代码不匹配,渲染时无法正确执行
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
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)

任务复盘|平台任务代码

运行后核对:核对 dfresult 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

平台代码逐步解析(1/3)

数据索引设置与行提取

# 将第一列设为索引
df.set_index('Unnamed: 0', inplace=True)

# 按标签提取"营业利润"行
df = df.loc['四、营业利润(亿元)']
  • set_index(): 将某列设为 DataFrame 的索引
  • loc[]: 基于标签的索引器,精确定位某一行
  • 提取后 df 变为 Series 对象

平台代码逐步解析(2/3)

索引清洗与类型转换

# 截取前4个字符(如 '2018年' → '2018')
df.index = df.index.str[:4]

# 转换为整数类型
df.index = df.index.astype(int)

# 按年份从小到大排序
df.sort_index(inplace=True)
  • .str[:4]: 字符串切片,提取年份数字部分
  • .astype(int): 类型转换,便于后续数值操作
  • sort_index(): 确保时间序列按正确顺序排列

平台代码逐步解析(3/3)

增长率计算

result = df.pct_change()
  • pct_change() 计算百分比变化: \((X_t - X_{t-1}) / X_{t-1}\)
  • 默认 periods=1,即与前一期相比
  • 第一个值为 NaN(因无上期数据)

增长率的三种计算方法

方法 公式 适用场景
简单增长率 \(g_t = \frac{X_t - X_{t-1}}{X_{t-1}}\) 相邻两期对比
CAGR \(\left(\frac{X_n}{X_0}\right)^{1/n} - 1\) 长期平均增长
对数差分 \(\ln(X_t) - \ln(X_{t-1})\) 连续复利假设

简单增长率:pct_change() 的原理

\[ \large g_t = \frac{X_t - X_{t-1}}{X_{t-1}} \]

Listing 4: 增长率计算演示
展开 pct_change 演示代码
# 构造演示数据
demo_profit = pd.Series(
    [100, 120, 108, 130, 150],
    index=[2019, 2020, 2021, 2022, 2023],
    name='营业利润(亿元)'
)
growth_demo = demo_profit.pct_change() * 100

for year, rate in growth_demo.items():
    if not pd.isna(rate):
        print(f'{year}年增长率: {rate:+.2f}%')
2020年增长率: +20.00%
2021年增长率: -10.00%
2022年增长率: +20.37%
2023年增长率: +15.38%

手动验证 pct_change() 的结果

Listing 5: 手动计算验证
# 手动逐年计算增长率
manual_growth = pd.Series(index=demo_profit.index, dtype=float)
for i in range(1, len(demo_profit)):
    manual_growth.iloc[i] = (
        (demo_profit.iloc[i] - demo_profit.iloc[i-1])
        / demo_profit.iloc[i-1]
    )

# 验证两种方法一致性
print(f'两种方法一致: {np.allclose(growth_demo.dropna()/100, manual_growth.dropna())}')
两种方法一致: True

复合年均增长率(CAGR)

CAGR 假设投资按恒定增长率复利增长:

\[ \large \text{CAGR} = \left(\frac{V_{\text{终值}}}{V_{\text{初值}}}\right)^{\frac{1}{n}} - 1 \]

适用边界:本章采用的常见实数形式要求期初值、期末值均为正且 \(n>0\);端点为零或负数时不能直接套用该公式。

为什么用:CAGR 平滑短期波动并贴合长期复利视角;除非各期增长率恒定,通常低于简单平均增长率。

CAGR 计算实例

Listing 6: CAGR计算演示
start_value = demo_profit.iloc[0]   # 期初值
end_value = demo_profit.iloc[-1]    # 期末值
n_years = len(demo_profit) - 1      # 年份数

cagr_demo = (end_value / start_value) ** (1 / n_years) - 1
avg_growth = (growth_demo.dropna() / 100).mean()

print(f'期初值: {start_value:.0f}亿元')
print(f'期末值: {end_value:.0f}亿元')
print(f'年份数: {n_years}年')
print(f'CAGR: {cagr_demo*100:.2f}%')
print(f'简单平均增长率: {avg_growth*100:.2f}%')
print(f'差异: {(cagr_demo - avg_growth)*100:.2f}个百分点')
期初值: 100亿元
期末值: 150亿元
年份数: 4年
CAGR: 10.67%
简单平均增长率: 11.44%
差异: -0.77个百分点

增长率统计分析

Listing 7: 增长率统计分析
valid_growth = growth_demo.dropna()
positive_count = (valid_growth > 0).sum()
negative_count = (valid_growth < 0).sum()

print(f'有效年份: {len(valid_growth)}')
print(f'平均增长率: {valid_growth.mean():.2f}%')
print(f'最大增长率: {valid_growth.max():.2f}%')
print(f'最小增长率: {valid_growth.min():.2f}%')
print(f'标准差: {valid_growth.std():.2f}%')
print(f'正增长年份: {positive_count}年 ({positive_count/len(valid_growth)*100:.0f}%)')
print(f'负增长年份: {negative_count}年 ({negative_count/len(valid_growth)*100:.0f}%)')
有效年份: 4
平均增长率: 11.44%
最大增长率: 20.37%
最小增长率: -10.00%
标准差: 14.47%
正增长年份: 3年 (75%)
负增长年份: 1年 (25%)

营业利润与增长率双轴图

展开双轴图完整绘制代码
fig, ax1 = plt.subplots(figsize=(8, 4.2))  # 压缩双轴图高度以适配 1280×720 投影视口

# 左轴:营业利润
ax1.set_xlabel('年份', fontsize=16)
ax1.set_ylabel('营业利润(亿元)', fontsize=16, color='steelblue')
ax1.plot(demo_profit.index, demo_profit.values,
         color='steelblue', marker='o', linewidth=2.5, label='营业利润')
ax1.tick_params(axis='y', labelcolor='steelblue')
ax1.grid(True, alpha=0.3, linestyle='--')
ax1.xaxis.set_major_locator(MaxNLocator(nbins=5, integer=True))  # 将年份轴限制为稀疏整数刻度

# 右轴:增长率
ax2 = ax1.twinx()
ax2.set_ylabel('增长率(%)', fontsize=16, color='coral')
ax2.plot(growth_demo.index, growth_demo.values,
         color='coral', marker='s', linewidth=2.5, label='增长率')
ax2.tick_params(axis='y', labelcolor='coral')
ax2.axhline(y=0, color='gray', linestyle='--', linewidth=1, alpha=0.7)

# 标记负增长区间
ax2.fill_between(growth_demo.index, 0, growth_demo.values,
                 where=(growth_demo.values < 0),
                 color='red', alpha=0.2, label='负增长区间')

plt.title('营业利润与增长率趋势', fontsize=16, pad=15)
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
fig.tight_layout()
plt.show()
图中展示营业利润与增长率趋势双轴图;读者应依据坐标、图例与注释比较主要模式。
Figure 1: 营业利润与增长率趋势双轴图

双轴图的关键技术

技术点 说明
ax1.twinx() 创建共享x轴的第二个y轴
fill_between() 条件填充,标记负增长区域
annotate() 标注极值点(最高/最低)
get_legend_handles_labels() 合并两个轴的图例

适用场景: 两个变量量纲不同但需对比分析时

增长趋势分解:理论基础

时间序列可以分解为:

\[ \large Y_t = T_t + C_t + S_t + \varepsilon_t \]

成分 含义 说明
\(T_t\) 趋势项 (Trend) 长期方向
\(C_t\) 循环项 (Cycle) 多年波动
\(S_t\) 季节项 (Seasonal) 年内波动
\(\varepsilon_t\) 随机项 (Noise) 不可预测成分

使用移动平均分解趋势

展开移动平均与趋势拟合代码
fig, axes = plt.subplots(2, 1, figsize=(8, 4.2))  # 在单页内完整呈现两幅趋势子图

# 子图1:原始增长率 vs 移动平均
growth_ma = growth_demo.rolling(window=3, center=True).mean()
axes[0].plot(growth_demo.index, growth_demo.values,
             marker='o', linewidth=2, label='原始增长率', alpha=0.7)
axes[0].plot(growth_ma.index, growth_ma.values,
             linewidth=3, label='3年移动平均', color='red')
axes[0].axhline(y=0, color='gray', linestyle='--', linewidth=1)
axes[0].set_title('增长率趋势分解', fontsize=16)
axes[0].set_ylabel('增长率(%)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].xaxis.set_major_locator(MaxNLocator(nbins=5, integer=True))  # 稀疏化上图年份刻度避免重叠

# 子图2:利润趋势 + 二次拟合线
axes[1].plot(demo_profit.index, demo_profit.values,
             marker='s', linewidth=2.5, color='steelblue')
z = np.polyfit(demo_profit.index, demo_profit.values, 2)
p = np.poly1d(z)
axes[1].plot(demo_profit.index, p(demo_profit.index),
             '--', color='red', linewidth=2, label='趋势线(二次)')
axes[1].set_title('营业利润趋势', fontsize=16)
axes[1].set_xlabel('年份')
axes[1].set_ylabel('营业利润(亿元)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].xaxis.set_major_locator(MaxNLocator(nbins=5, integer=True))  # 稀疏化下图年份刻度避免重叠

plt.tight_layout()
plt.show()
图中展示增长率趋势分解;读者应依据坐标、图例与注释比较主要模式。
Figure 2: 增长率趋势分解

增长阶段识别

移动平均增长率 判定阶段
> 20% 高速增长期
10% ~ 20% 稳定增长期
0% ~ 10% 缓慢增长期
< 0% 衰退期
  • 均值回归: 极高增长率难以持续
  • 基数效应: 基数小时增长率容易偏高
  • 行业周期: 成长期 → 成熟期 → 衰退期

外推预测的基本思路

展开外推预测与区间绘制代码
forecast_years = 3
recent_growth_rate = (growth_demo.dropna().tail(3).mean()) / 100
last_value = demo_profit.iloc[-1]

# 预测未来3年
forecast_values = [last_value * (1 + recent_growth_rate) ** i
                   for i in range(1, forecast_years + 1)]
forecast_index = np.arange(demo_profit.index[-1] + 1,
                           demo_profit.index[-1] + forecast_years + 1)

fig, ax = plt.subplots(figsize=(8, 4.2))  # 压缩预测图高度以保留标题与折叠摘要
ax.plot(demo_profit.index, demo_profit.values,
        marker='o', linewidth=2.5, label='历史数据', color='steelblue')
ax.plot(forecast_index, forecast_values,
        marker='s', linewidth=2.5, linestyle='--',
        label='预测数据', color='coral')

# 课堂情景带 (±20%),不表示统计置信区间或预测区间
upper = [v * 1.2 for v in forecast_values]  # 构造上行情景边界
lower = [v * 0.8 for v in forecast_values]  # 构造下行情景边界
ax.fill_between(forecast_index, lower, upper,
                color='coral', alpha=0.2, label='课堂情景带(±20%)')  # 标明该带宽仅用于情景演示

ax.set_title('营业利润趋势外推预测', fontsize=16)
ax.set_xlabel('年份')
ax.set_ylabel('营业利润(亿元)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
图中展示营业利润趋势外推预测;读者应依据坐标、图例与注释比较主要模式。
Figure 3: 营业利润趋势外推预测

外推预测的局限性

  • 假设历史模式延续: 未来可能截然不同
  • 增长率恒定假设: 实际增长率会波动
  • 忽略外部因素: 宏观经济、政策变化、行业竞争
  • 区间含义: 图中 ±20% 仅是课堂情景带,不是置信区间或预测区间
  • ⚠️ 仅供参考,不构成投资建议

口径边界:营业利润增长率不能直接用于 PEG

PEG 通常把市盈率与净利润或 EPS 增长预期配对:

\[ \large \text{PEG} = \frac{\text{PE}}{\text{增长率}} = \frac{P/E}{g} \]

PEG 值 估值判断
< 1 常见启发式中的“相对较低”;不是低估结论
≈ 1 启发式参照点;不是合理价值证明
> 1 常见启发式中的“相对较高”;不是高估结论

增长率必须使用与 PE 匹配的预期 EPS 增长口径并明确百分数单位;本章营业利润增长率不得代入。

PEG 课堂分类的限制

教学边界:三档只是课堂启发式;营业利润增长率不可代入 PEG,也不支持净利润估值结论。

PEG 公式的适用边界

能回答 不能忽略
在同口径成长公司间,把 PE 与预期 EPS 增长率并列比较 负增长、预测误差、现金流质量与行业差异
检查估值是否依赖过高增长假设 营业利润增长率不能替代净利润或 EPS 增长率

结论:PEG 是情景比较工具,不是独立买卖信号;必须报告增长率口径与敏感性。

PEG 公式敏感性演示(非营业利润结论)

展开 PEG 公式敏感性计算代码
stock_price = 50  # 设置课堂假设股价,不代表真实公司估值
current_eps = 2  # 设置课堂假设每股收益,不代表预测口径
pe_ratio = stock_price / current_eps  # 计算课堂假设市盈率

growth_scenarios = [5,10,15,20,25,30]  # 设置待检验的预期EPS增长率情景
peg_rows = []  # 初始化启发式敏感性结果
for growth_percent in growth_scenarios:  # 遍历增长率情景而不作估值结论
    peg_value = pe_ratio/growth_percent  # 按百分数口径计算课堂PEG
    heuristic_band = '启发式较低' if peg_value<1 else ('启发式中间' if peg_value<1.2 else '启发式较高')  # 仅标记数值区间
    peg_rows.append({'增长率(%)':growth_percent,'PE':pe_ratio,'PEG':round(peg_value,2),'区间':heuristic_band})  # 保存敏感性结果

pd.DataFrame(peg_rows)  # 输出情景表并禁止解释为高估或低估
Table 1: PEG公式敏感性示例(不得代入营业利润增长率)
增长率(%) PE PEG 区间
0 5 25.0 5.00 启发式较高
1 10 25.0 2.50 启发式较高
2 15 25.0 1.67 启发式较高
3 20 25.0 1.25 启发式较高
4 25 25.0 1.00 启发式中间
5 30 25.0 0.83 启发式较低

增长质量的五大特征

特征 说明
可持续性 来源于核心竞争力,而非一次性收益
盈利性 增长带来利润率提升
现金流 利润增长伴随现金流改善
稳定性 增长率波动较小
内源性 靠自身造血,而非外延并购

增长质量综合评估框架

下列 30%/40%/30% 权重与 80/60/40 评级界限均为课堂假设,用于演示综合评价框架,不是经样本校准的通用行业标准。

Listing 8: 增长质量综合评估
展开增长质量评分与评级代码
growth_pct = growth_demo.dropna()

# 三个维度的评分
stability = 100 - min(growth_pct.std(), 50) / 50 * 100
consistency = (growth_pct > 0).sum() / len(growth_pct) * 100
momentum = max(min(growth_pct.iloc[-1], 30) / 30 * 100, 0)

# 加权综合得分
total = stability * 0.3 + consistency * 0.4 + momentum * 0.3

grade = ('优秀(A)' if total >= 80 else
         '良好(B)' if total >= 60 else
         '一般(C)' if total >= 40 else '较差(D)')

print(f'稳定性得分: {stability:.1f}/100  (权重30%)')
print(f'持续性得分: {consistency:.1f}/100  (权重40%)')
print(f'动能得分:   {momentum:.1f}/100  (权重30%)')
print(f'综合得分:   {total:.1f}/100')
print(f'增长质量评级: {grade}')
稳定性得分: 71.1/100  (权重30%)
持续性得分: 75.0/100  (权重40%)
动能得分:   51.3/100  (权重30%)
综合得分:   66.7/100
增长质量评级: 良好(B)

本章核心知识点回顾

概念 要点
增长率计算 pct_change() 计算简单增长率
CAGR 长期平均增长率,消除短期波动
双轴图 twinx() 同时展示利润与增长率
趋势分解 移动平均 + 多项式拟合
口径边界 营业利润增长率 ≠ 净利润/EPS 增长率,不直接支持 PEG
增长质量 稳定性 + 持续性 + 动能综合评估

随堂练习

  • 问题 1|怎样完成本章分析?:从上海汽车年末累计营业利润中固定首尾稳定正值年度,写出起止值、完整年数和CAGR;用 \((1+g)^n\times 起值\) 反推末值并报告相邻端点敏感性。
  • 问题 2|结果说明什么?:用 2—3 句话解释“为什么关注营业利润增长率?”中的核心机制,并说明营业利润为何不能直接替代净利润或 EPS 来形成 PEG 结论。
  • 问题 3|换一个情境,怎样继续应用?:将营业利润增长分析应用到一个中国或长三角场景,写出需要替换的营业利润输入、预期变化与验证办法。
  • 作答提示:三项均有完整回答;代码能够正常运行,解释明确营业利润口径及 PEG 边界,拓展应用不把课堂阈值或相关关系误作普遍因果结论。

教师参考解答

教师参考解答|答案与说明 1

  • 所用数据与字段:规定资产 financial_statement;上海汽车 600104.SH;字段 ts_code/end_date/operate_profit;CAGR 使用年末累计营业利润并固定起止年
  • 解答示例|营业利润 CAGR、同比与核对(平台保护块之外):

教师参考解答|代码 1

展开代码(代码区可独立滚动)
def select_latest_disclosure(frame,value_fields):  # 统一财报披露版本选择约定
    required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns]  # 固定键与可接受版本字段
    if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
    work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce')  # 清理必需字段
    for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce')  # 解析披露日期
    keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum())  # 统计选择前重复
    if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无版本依据时停止
    work['_version']=work[versions].max(axis=1) if versions else pd.NaT  # 合并双版本字段为排序键
    latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index)  # 为每个公司报告期定位最晚披露时点
    latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy()  # 仅保留单例或并列最晚版本
    tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum())  # 统计并列最晚版本行
    conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0  # 统计并列且数值冲突的披露键
    if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 并列最新值冲突时请先检查数据后再继续
    selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore')  # 仅合并数值完全一致的并列行
    after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions}  # 返回并列与冲突检查
    if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 选择后仍重复则停止
    return selected,audit  # 返回唯一版本与检查依据

教师参考解答|代码 2

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具
import pandas as pd  # 导入表格库
import numpy as np  # 导入有限值与安全除法工具
path=Path('/home/ubuntu/r2_data_mount/data/stock/financial_statement.h5')  # 指定财务报表
if not path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 缺失即停止
financials=pd.read_hdf(path,key='financial_data').rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','profit_from_operation':'operate_profit'})  # 按已核验key读取并标准化营业利润字段
financials['ts_code']=financials['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
financials['end_date']=pd.PeriodIndex(financials['end_date'].str.upper(),freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
company,version_audit=select_latest_disclosure(financials.loc[financials['ts_code'].eq('600104.SH')],['operate_profit'])  # 筛选上海汽车最晚披露版本
if company.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空结果时停止
company=company.sort_values('end_date').copy()  # 固定报告期时间顺序
if not np.isfinite(company['operate_profit']).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 非有限营业利润时停止
company['fiscal_year']=company['end_date'].dt.year  # 构造财政年键
company['fiscal_quarter']=company['end_date'].dt.quarter  # 构造财政季度键
if company.duplicated(['fiscal_year','fiscal_quarter']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 期间键不唯一时停止
prior_profit=company[['fiscal_year','fiscal_quarter','operate_profit']].rename(columns={'operate_profit':'prior_year_profit'}).copy()  # 建立上年同季营业利润查找表
prior_profit['fiscal_year']=prior_profit['fiscal_year']+1  # 将历史年键前移一年以匹配当期
company=company.merge(prior_profit,on=['fiscal_year','fiscal_quarter'],how='left',validate='one_to_one')  # 按年与季度精确连接同比分母
denominator_scale=max(float(company['operate_profit'].abs().max()),1.0)  # 用样本量级定义接近零最低要求
near_zero_cutoff=denominator_scale*1e-6  # 设定课堂数值稳定性阈值

教师参考解答|代码 3

展开代码(代码区可独立滚动)
company['profit_change']=company['operate_profit']-company['prior_year_profit']  # 对所有可比期发布营业利润绝对变化
company['comparison_status']='CONVENTIONAL_GROWTH'  # 默认仅两期均为稳定正值时使用常规增长
company.loc[company['prior_year_profit'].isna(),'comparison_status']='MISSING_PRIOR_QUARTER'  # 标记缺少上年同季
company.loc[company['prior_year_profit'].notna() & company['prior_year_profit'].abs().le(near_zero_cutoff),'comparison_status']='ZERO_OR_NEAR_ZERO_BASE'  # 标记零或接近零基期
company.loc[company['prior_year_profit'].lt(-near_zero_cutoff) & company['operate_profit'].lt(0) & company['profit_change'].gt(0),'comparison_status']='LOSS_NARROWING'  # 负基期且亏损绝对额下降时标记亏损收窄
company.loc[company['prior_year_profit'].lt(-near_zero_cutoff) & company['operate_profit'].lt(0) & company['profit_change'].lt(0),'comparison_status']='LOSS_WIDENING'  # 负基期且亏损绝对额上升时标记亏损扩大
company.loc[company['prior_year_profit'].lt(-near_zero_cutoff) & company['operate_profit'].lt(0) & company['profit_change'].eq(0),'comparison_status']='LOSS_UNCHANGED'  # 标记亏损持平
company.loc[company['prior_year_profit'].lt(-near_zero_cutoff) & company['operate_profit'].ge(0),'comparison_status']='SIGN_CHANGE_LOSS_TO_NONLOSS'  # 标记亏损转非亏损
company.loc[company['prior_year_profit'].gt(near_zero_cutoff) & company['operate_profit'].le(0),'comparison_status']='SIGN_CHANGE_PROFIT_TO_NONPROFIT'  # 标记盈利转非盈利
company['denominator_issue']=company['comparison_status'].where(company['comparison_status'].ne('CONVENTIONAL_GROWTH'),'')  # 将非常规比较状态同步到异常清单
valid_yoy=company['comparison_status'].eq('CONVENTIONAL_GROWTH')  # 仅将两期均为稳定正值的精确同季对比纳入增长率
company['quarter_yoy']=np.where(valid_yoy,company['operate_profit']/company['prior_year_profit']-1,np.nan)  # 计算可解释的精确同季同比
abnormal_denominators=company.loc[~valid_yoy,['end_date','operate_profit','prior_year_profit','profit_change','comparison_status','denominator_issue']].copy()  # 生成含绝对变化与亏损/符号状态的异常清单
annual=company.loc[company['end_date'].dt.month.eq(12)].set_index(company.loc[company['end_date'].dt.month.eq(12),'end_date'].dt.year)['operate_profit']  # 提取年末累计营业利润
annual=annual.sort_index()  # 按年度固定CAGR端点顺序
annual_scale=max(float(annual.abs().max()),1.0)  # 用年度样本量级定义端点稳定性最低要求
annual_endpoint_cutoff=annual_scale*1e-6  # 设定接近零端点的课堂数值阈值
interpretable_annual=annual.loc[annual.gt(annual_endpoint_cutoff)]  # 排除非正或接近零的CAGR端点
if len(interpretable_annual)<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 可解释端点不足时停止
start_year,end_year=int(interpretable_annual.index[0]),int(interpretable_annual.index[-1])  # 固定首尾稳定正值年度

教师参考解答|代码 4

展开代码(代码区可独立滚动)
start_profit,end_profit=interpretable_annual.iloc[0],interpretable_annual.iloc[-1]  # 取得CAGR首尾营业利润
cagr=(end_profit/start_profit)**(1/(end_year-start_year))-1  # 按完整年数计算复合增长率
manual_check=(1+cagr)**(end_year-start_year)*start_profit  # 反推末值进行另行核对
assert abs(manual_check-end_profit)<max(1e-6,abs(end_profit)*1e-10)  # 验证CAGR可还原末值
endpoint_cases=[('基准',start_year,end_year),('起点后移一年',start_year+1,end_year),('终点前移一年',start_year,end_year-1)]  # 定义基准与相邻端点情景
endpoint_rows=[]  # 初始化端点敏感性输出
for label,case_start,case_end in endpoint_cases:  # 逐个检查相邻年端点的可解释性
    endpoints_available=case_start in annual.index and case_end in annual.index and case_end>case_start  # 检查两个指定年度均存在且间隔为正
    endpoints_stable=endpoints_available and annual.loc[case_start]>annual_endpoint_cutoff and annual.loc[case_end]>annual_endpoint_cutoff  # 检查两端均为稳定正值
    case_cagr=(annual.loc[case_end]/annual.loc[case_start])**(1/(case_end-case_start))-1 if endpoints_stable else np.nan  # 端点稳定时重算CAGR
    endpoint_rows.append({'scenario':label,'start_year':case_start,'end_year':case_end,'status':'OK' if endpoints_stable else 'UNINTERPRETABLE','cagr':case_cagr})  # 保存情景结果与停止状态
endpoint_sensitivity=pd.DataFrame(endpoint_rows)  # 形成可执行的相邻端点敏感性表
print(version_audit,company[['end_date','operate_profit','prior_year_profit','profit_change','comparison_status','quarter_yoy']].tail(),{'start_year':start_year,'end_year':end_year,'cagr':cagr,'manual_check':manual_check},abnormal_denominators,endpoint_sensitivity)  # 输出版本、绝对变化、状态、同比、CAGR与敏感性依据

教师参考解答|答案与说明 2

  • 敏感性答案:分别将起点后移一年、终点前移一年重算 CAGR;若端点非正、接近0或发生符号翻转,则报告“不可解释”而非百分比排名。营业利润反映主营经营成果,但不能直接替代归母净利润、EPS 或其预期增长进入 PEG。
  • 拓展应用答案:替换为另一家长三角公司并保持年末累计口径;预期端点选择会改变 CAGR;用反推末值、季度同比和相邻端点敏感性三重验证。
  • 参考结果:报告期、营业利润、绝对变化和比较结果;仅当两期均为正值时计算季度同比,否则说明亏损收窄、扩大、持平、正负转换或基期为零;另外给出 CAGR 起点与终点、反算结果、异常项目和起止年份变化后的比较。所需财务数据可从课程数据下载入口取得。
  • 边界 / 局限:分母近0或符号翻转时同比失真;营业利润不是净利润
  • 常见错误:累计值直接同比;把营业利润增长代入 PEG