行业景气度分析的理论基础与Python实现(附代码)
在正式进入代码实现之前,我们需要先理解一些基本概念和原理。这一章会详细讲解行业景气度的定义、量化方法、以及为什么需要使用TTM指标。理解这些基础概念,对于后续正确实现分析系统至关重要。同时,我们会将理论与代码实现紧密结合,帮助读者更好地掌握每个知识点。
1.1 行业景气度的基本概念
1.1.1 什么是行业景气度
行业景气度是用来衡量某个行业当前经营状况和发展趋势的综合性指标。简单来说,它回答的问题是:这个行业现在"火不火"?
从投资实践的角度来看,行业景气度直接影响行业内公司的盈利能力和成长空间。当行业处于高景气周期时,即使公司基本面一般,往往也能获得不错的收益;反之,当行业进入下行周期时,公司的经营难度会显著增加。
1.1.2 景气度与投资的关系
投资领域有一句经典名言:"不要逆流而上,要顺势而为。"这里的"势"在很大程度上指的就是行业景气度。
具体来说,行业景气度对投资的影响体现在以下几个方面:
第一,盈利能力。高景气行业的公司通常能够获得更高的毛利率和净利率,这是因为供不应求的市场环境允许企业提价或者获得更强的议价能力。
第二,成长性。高景气行业的营业收入增速通常较快,这不仅带来了业绩增长,也给投资者带来了更高的预期回报。
第三,持续性。景气度的变化往往具有惯性,一旦进入上升通道,通常会持续一段时间;同样,下行周期也不会很快结束。
1.2 量化景气度的方法
1.2.1 为什么要量化
如果我们只是定性地判断"这个行业很火",那么很难进行横向比较,也很难追踪其历史变化。因此,我们需要一套量化的方法来精确描述景气度。
1.2.2 核心财务指标
经过实践验证,以下四个财务指标能够较好地反映行业景气度:
第一个指标是ROE,即净资产收益率。它的计算公式是净利润除以净资产。ROE反映了股东投入的资本能够获得多少回报,是衡量企业盈利能力的核心指标。
第二个指标是毛利率。计算方式是毛利润除以营业收入。毛利率反映了企业的产品定价能力和成本控制能力。毛利率高的企业通常具有较强的竞争优势,比如品牌溢价或者技术壁垒。
第三个指标是净利率。计算方式是净利润除以营业收入。与毛利率不同,净利率考虑了所有费用,能够更全面地反映企业的最终盈利能力。
第四个指标是营收增长率。计算方式是当期营业收入减去上期营业收入,再除以上期营业收入。这个指标反映了企业的成长速度和市场规模扩张能力。
1.2.3 指标选择的技术细节
在实际操作中,我们选择这四个指标是有充分理由的。
首先,这四个指标涵盖了盈利能力、运营效率和成长性三个维度,能够全面反映企业的经营状况。
其次,这些指标都是相对指标而非绝对数值。这很重要,因为不同规模的企业不能直接比较绝对数值,但相对指标可以在同一尺度上进行比较。
最后,这些指标都是可以直接从财务报表中获取的数据,不需要进行复杂的估算或者预测。
1.2.4 数据加载
import pandas as pd
import numpy as np
# 读取财务数据
income_df = pd.read_excel('利润表_汇总.xlsx')
balance_df = pd.read_excel('资产负债表_汇总.xlsx')
print(f"利润表记录数: {len(income_df):,}")
print(f"资产负债表记录数: {len(balance_df):,}")
执行结果:
利润表记录数: 304,644
资产负债表记录数: 294,540
我们需要读取两类财务数据:利润表和资产负债表。利润表包含企业的收入、成本和利润信息,资产负债表包含企业的资产和负债信息。数据来源于A股上市公司的定期报告,经过汇总整理后存储在Excel文件中。
1.3 TTM指标详解
1.3.1 什么是TTM
TTM是Trailing Twelve Months的缩写,翻译过来是"滚动十二个月"或者"过去十二个月"。它是一种计算财务指标的方法,通过将最近四个季度的数据相加,得到过去十二个月的累计值。
1.3.2 为什么要用TTM
要理解TTM的必要性,我们首先要了解A股财报披露的特殊规则。
A股的财务报告是这样披露的:一季报在每年4月披露,内容是当年第一季度的单季数据;半年报在8月披露,内容是当年1-6月的累计数据;三季报在10月披露,内容是当年1-9月的累计数据;年报在次年4月披露,内容是当年全年的累计数据。
问题来了。如果我们直接使用这些原始数据,会遇到什么麻烦?
以半年报为例。假设某公司2025年半年报显示净利润为3亿元,你能直接说公司二季度赚了3亿元吗?不能,因为半年报的数据是1-6月累计值,而不是二季度单季值。如果一季度赚了1亿元,那么二季度实际只赚了2亿元。
这种披露规则导致不同季度的数据不可直接比较。一季报、半年报、三季报是累计值,而年报又是另一个累计值。如果不加处理地直接使用这些数据,分析结果会出现严重偏差。
TTM方法通过滚动计算解决了这个问题。无论当前是什么季度,TTM始终代表过去十二个月的数据,这样就可以进行跨时间的横向比较了。
1.3.3 TTM的计算方法
TTM的计算公式如下:
其中代表当前季度的单季数据,、、分别代表前三个季度的单季数据。
这里需要特别注意一个问题。A股的年报数据是年度累计值,不能直接用于TTM计算。我们需要先将年报数据转换为第四季度的单季值。
转换公式是:
或者等价地:
这个转换步骤绝对不能省略,否则计算出的TTM数据将会严重错误。
1.3.4 季度类型识别
首先,我们需要编写一个函数来识别每个报告期属于哪个季度:
def get_quarter_type(date):
"""
根据报告日期判断季度类型
参数: date - 报告日期
返回: 'Q1', 'Q2', 'Q3', 'Q4' 或 'OTHER'
"""
month = pd.to_datetime(date).month
if month == 3:
return 'Q1'
elif month == 6:
return 'Q2'
elif month == 9:
return 'Q3'
elif month == 12:
return 'Q4'
return 'OTHER'
这个函数的逻辑很简单:根据报告日期的月份来判断属于哪个季度。3月对应Q1,6月对应Q2,9月对应Q3,12月对应Q4。
1.3.5 年度累计转单季值的核心逻辑
这是TTM计算中最关键的一步。对于年报数据(Q4),我们需要将其从"年度累计值"转换为"单季值"。
def convert_to_quarterly(df):
"""
将年度累计值转换为单季值
核心逻辑:Q4单季 = Q4累计 - Q3累计
参数:
df: 包含财务数据的DataFrame
返回:
转换后的DataFrame,新增季度类型列和单季值列
"""
df = df.copy()
df = df.sort_values(['SECURITY_CODE', 'REPORT_DATE'])
# 添加季度类型标识
df['quarter_type'] = df['REPORT_DATE'].apply(get_quarter_type)
# 需要转换的财务指标列
convert_cols = ['PARENT_NETPROFIT', 'TOTAL_OPERATE_INCOME', 'TOTAL_OPERATE_COST']
for col in convert_cols:
if col not in df.columns:
continue
# 获取上一期的值(同一股票的前一个报告期)
df[f'{col}_prev'] = df.groupby('SECURITY_CODE')[col].shift(1)
# 标记Q4行
mask_q4 = df['quarter_type'] == 'Q4'
# 核心转换逻辑:
# 如果是Q4:用累计值减去上期累计值
# 如果是Q1-Q3:保持原值不变
df[f'{col}_quarterly'] = np.where(
mask_q4,
df[col] - df[f'{col}_prev'].fillna(0),
df[col]
)
return df
代码解读:np.where函数有三个参数。第一个参数是条件(mask_q4),如果条件为True,则取第二个参数的值(即Q4单季计算结果);如果条件为False,则取第三个参数的值(即原始单季数据)。
核心逻辑可以用一句话概括:对于Q4(年报),用"年度累计值减去上期累计值"得到单季值;对于Q1-Q3,直接使用原始单季值。
1.3.6 计算TTM值
在完成季度转换后,我们就可以正确计算TTM了:
def calculate_ttm(df):
"""
计算TTM(滚动12个月)指标
参数:
df: 已完成季度转换的DataFrame
返回:
添加了TTM列的DataFrame
"""
# 需要计算TTM的指标
ttm_cols = ['PARENT_NETPROFIT', 'TOTAL_OPERATE_INCOME', 'TOTAL_OPERATE_COST']
for col in ttm_cols:
col_q = f'{col}_quarterly'
if col_q not in df.columns:
continue
# 使用groupby确保只对同一股票的数据进行滚动计算
df[f'{col}_TTM'] = df.groupby('SECURITY_CODE')[col_q].transform(
lambda x: x.rolling(4, min_periods=4).sum()
)
# 计算TTM财务指标
# ROE_TTM = TTM净利润 / 净资产 * 100
df['ROE_TTM'] = df['PARENT_NETPROFIT_TTM'] / df['TOTAL_EQUITY'] * 100
# 净利率_TTM = TTM净利润 / TTM营业收入 * 100
df['净利率_TTM'] = df['PARENT_NETPROFIT_TTM'] / df['TOTAL_OPERATE_INCOME_TTM'] * 100
# 毛利率_TTM = (TTM营业收入 - TTM营业成本) / TTM营业收入 * 100
df['毛利率_TTM'] = (
df['TOTAL_OPERATE_INCOME_TTM'] - df['TOTAL_OPERATE_COST_TTM']
) / df['TOTAL_OPERATE_INCOME_TTM'] * 100
# 营收增速 = (本期TTM - 上年同期TTM) / 上年同期TTM * 100
df['营收增速'] = df.groupby('SECURITY_CODE')['TOTAL_OPERATE_INCOME_TTM'].pct_change(4) * 100
return df
这里使用groupby + transform的方式,确保每个股票独立计算自己的TTM,不会与其他股票的数据混淆。
1.3.7 验证TTM计算结果
为了确保计算正确,我们可以用已知公司进行验证:
# 合并数据
merged = income_converted.merge(
balance_df[['SECURITY_CODE', 'REPORT_DATE', 'TOTAL_EQUITY', 'TOTAL_ASSETS']],
on=['SECURITY_CODE', 'REPORT_DATE'],
how='left'
)
merged = calculate_ttm(merged)
# 以巨化股份(600160)为例验证
test_code = 600160
test_data = merged[merged['SECURITY_CODE'] == test_code]
test_latest = test_data[test_data['REPORT_DATE'] == pd.to_datetime('2025-09-30')]
print(f"巨化股份(600160)2025年Q3数据验证:")
print(f"TTM净利润: {test_latest['PARENT_NETPROFIT_TTM'].iloc[0] / 1e8:.2f} 亿元")
print(f"TTM ROE: {test_latest['ROE_TTM'].iloc[0]:.2f}%")
print(f"TTM毛利率: {test_latest['毛利率_TTM'].iloc[0]:.2f}%")
正确的输出结果应该是:
巨化股份(600160)2025年Q3数据验证:
TTM净利润: 68.19 亿元
TTM ROE: 29.74%
TTM毛利率: 18.30%
如果你的计算结果与上述数值不符,请检查是否遗漏了"年度累计转单季"这一关键步骤。
1.4 景气度指数的构建方法
1.4.1 归一化处理
我们已经选择了四个核心指标,现在需要将它们合成为一个综合的景气度指数。
首先遇到的问题是量纲不统一。ROE、毛利率、净利率通常是0%到100%之间的百分比,而营收增长率可能是负数也可能是正数,范围在-50%到100%之间甚至更宽。如果直接把这些指标相加,数值大的指标会主导结果,数值小的指标则会被忽略。
解决方法是进行归一化处理,将所有指标转换到0到1的区间内。
常用的归一化方法是Min-Max标准化,公式如下:
其中是原始值,是归一化后的值,和分别是该指标在所有样本中的最小值和最大值。
1.4.2 归一化代码实现
def normalize_minmax(series):
"""
Min-Max归一化
参数:
series: 需要归一化的Series
返回:
归一化后的Series
"""
min_val = series.min()
max_val = series.max()
# 避免除零错误
if max_val - min_val < 0.001:
return pd.Series([0.5] * len(series), index=series.index)
return (series - min_val) / (max_val - min_val)
1.4.3 权重分配
归一化之后,我们需要为每个指标分配权重,然后将它们加权求和得到最终的景气度指数。
常用的权重分配方法有两种。
第一种是等权重法,即每个指标的权重相同。公式是:
这种方法简单公平,每个指标的重要性相当。
第二种是主成分分析法,即通过统计方法自动计算各指标的权重。这种方法更加科学,但解释起来比较困难。
在实际应用中,我们采用等权重法,并建议将权重设置为ROE占30%、净利率占25%、毛利率占25%、营收增速占20%。这个权重设置反映了我们对盈利能力的重视。
1.4.4 按行业汇总指标
由于我们最终需要的是行业层面的景气度而非单个公司,因此需要对数据进行行业汇总:
# 按行业和报告期聚合
industry_metrics = merged.groupby(['INDUSTRY_NAME', 'REPORT_DATE']).agg({
'ROE_TTM': 'median', # 使用中位数而非平均数,更稳健
'净利率_TTM': 'median',
'毛利率_TTM': 'median',
'营收增速': 'median'
}).reset_index()
print(f"行业指标数据: {len(industry_metrics):,} 条")
print(f"行业数: {industry_metrics['INDUSTRY_NAME'].nunique()}")
使用中位数而非平均数的原因在于:平均数容易受到极端值的影响,而中位数则更加稳健,能够更好地代表行业的整体水平。
1.4.5 构建景气度指数
prosperity_results = []
# 遍历每个行业
for industry in industry_metrics['INDUSTRY_NAME'].unique():
industry_data = industry_metrics[
industry_metrics['INDUSTRY_NAME'] == industry
].copy()
# 数据太少则跳过
if len(industry_data) < 4:
continue
# 对每个指标进行归一化
for col in ['ROE_TTM', '净利率_TTM', '毛利率_TTM', '营收增速']:
short_col = col.replace('_TTM', '')
industry_data[f'{short_col}_norm'] = normalize_minmax(industry_data[col])
# 加权计算景气度指数
# 权重设置:ROE 30%, 净利率 25%, 毛利率 25%, 营收增速 20%
industry_data['prosperity'] = (
industry_data['ROE_norm'].fillna(0.5) * 0.30 +
industry_data['净利率_norm'].fillna(0.5) * 0.25 +
industry_data['毛利率_norm'].fillna(0.5) * 0.25 +
industry_data['营收增速_norm'].fillna(0.5) * 0.20
)
prosperity_results.append(industry_data)
# 合并结果
prosperity_df = pd.concat(prosperity_results, ignore_index=True)
print(f"景气度指数计算完成,共 {len(prosperity_df):,} 条记录")
1.5 动量指标与趋势判断
1.5.1 动量的概念
动量是一个物理学概念,在投资中用来描述趋势的惯性。如果一个指标在过去一段时间内持续上升,那么它就具有正向动量;反之则具有负向动量。
在行业景气度分析中引入动量指标,可以帮助我们判断行业是处于上升趋势还是下降趋势。
1.5.2 动量指标的计算
动量指标的计算公式是:
如果动量大于零,说明行业景气度在上升;如果动量小于零,说明行业景气度在下降。
这个指标的作用类似于移动平均线的交叉信号。当动量由负转正时,是买入信号;当动量由正转负时,是卖出信号。
1.5.3 动量计算代码
# 合并所有行业数据
all_prosperity = pd.concat(prosperity_results, ignore_index=True)
# 计算动量:当前景气度 - 过去4季度平均景气度
all_prosperity = all_prosperity.sort_values(['INDUSTRY_NAME', 'REPORT_DATE'])
all_prosperity['momentum'] = all_prosperity.groupby('INDUSTRY_NAME')['prosperity'].transform(
lambda x: x - x.rolling(4, min_periods=1).mean()
)
# 动量信号
# momentum > 0: 上升趋势
# momentum < 0: 下降趋势
all_prosperity['signal'] = np.where(all_prosperity['momentum'] > 0, '上升', '下降')
1.6 行业轮动的理论基础
1.6.1 周期的普遍性
经济学和商业的历史告诉我们,几乎所有行业都会经历周期波动。没有哪个行业能够永远保持高速增长,也没有哪个行业会永远低迷不振。
这是由多种因素共同作用的结果,包括宏观经济波动、技术变革、政策调整、市场竞争格局变化等等。
1.6.2 强周期与弱周期行业
根据行业受经济周期影响的程度,我们可以将行业分为强周期行业和弱周期行业。
强周期行业的业绩与经济周期高度相关。经济繁荣时,这些行业的需求旺盛、利润丰厚;经济衰退时,这些行业则会受到严重冲击。典型的强周期行业包括钢铁、煤炭、有色金属、房地产、金融等。
弱周期行业的业绩相对稳定,受经济周期影响较小。典型的弱周期行业包括食品饮料、医药、消费等日常生活必需的行业。
1.6.3 轮动策略
基于行业景气度的轮动策略,其核心逻辑是在行业景气度上升时买入,在景气度下降时卖出。
具体的操作步骤是:首先构建所有行业的景气度指数;然后按照景气度高低进行排序;接着选择景气度最高且动量正向的行业进行配置;当某个行业的景气度开始下降或者动量转负时,切换到其他景气度正在上升的行业。
1.7 数据可视化
1.7.1 行业景气度趋势对比图
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
# 选择需要对比的行业
target_industries = ['化学制品', '化学原料', '氟化工', '半导体', '房地产']
# 筛选数据
plot_data = all_prosperity[
(all_prosperity['INDUSTRY_NAME'].isin(target_industries)) &
(all_prosperity['REPORT_DATE'] >= '2023-01-01')
].sort_values('REPORT_DATE')
# 绑图
fig, ax = plt.subplots(figsize=(14, 7))
for industry in target_industries:
industry_plot = plot_data[plot_data['INDUSTRY_NAME'] == industry]
ax.plot(
industry_plot['REPORT_DATE'],
industry_plot['prosperity'],
marker='o',
label=industry,
linewidth=2
)
ax.set_xlabel('时间', fontsize=12)
ax.set_ylabel('景气度指数', fontsize=12)
ax.set_title('行业景气度趋势对比(2023-2025)', fontsize=14)
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('industry_prosperity_trend.jpg', dpi=150, bbox_inches='tight')
plt.show()
1.7.2 行业景气度热力图
# 获取最新一期的数据
latest_date = all_prosperity['REPORT_DATE'].max()
latest_data = all_prosperity[all_prosperity['REPORT_DATE'] == latest_date]
# 取景气度最高的前20个行业
top_20 = latest_data.nlargest(20, 'prosperity')['INDUSTRY_NAME'].tolist()
latest_top = latest_data[latest_data['INDUSTRY_NAME'].isin(top_20)]
# 转换为热力图格式
heatmap_data = latest_top.pivot_table(
values='prosperity',
index='INDUSTRY_NAME'
).sort_values('prosperity', ascending=False)
# 绑制热力图
plt.figure(figsize=(10, 8))
sns.heatmap(
heatmap_data,
annot=True,
fmt='.3f',
cmap='RdYlGn',
center=0.5,
linewidths=0.5
)
plt.title(f'行业景气度热力图({latest_date.strftime("%Y-%m-%d")})', fontsize=14)
plt.tight_layout()
plt.savefig('prosperity_heatmap.jpg', dpi=150, bbox_inches='tight')
plt.show()
1.8 完整代码整合
以下是整合后的完整代码,可扫码获取
本章小结
本章我们详细讲解了行业景气度分析的理论基础和Python实现。主要内容包括:
第一,我们定义了行业景气度的概念,并解释了它与投资的关系。
第二,我们选择了ROE、毛利率、净利率和营收增长率四个核心指标来量化景气度。
第三,我们深入讲解了TTM指标的必要性和计算方法,特别强调了年报数据需要特殊处理这一关键点。
第四,我们介绍了归一化处理和权重分配的方法。
第五,我们引入了动量指标来判断趋势方向。
第六,我们讨论了行业轮动的理论基础,并提供了完整的数据可视化代码。
下一章,我们将应用这套系统对氟化工行业进行具体分析,展示如何在实际投资中使用这些工具。


