import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import os
from matplotlib.gridspec import GridSpec
from matplotlib.ticker import PercentFormatter
# -------------------------- 全局配置 --------------------------
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"] # 中文字体
plt.rcParams["axes.unicode_minus"] = False # 正确显示负号
plt. rcParams['font.sans–serif']=['Kaiti']
sns.set_theme(style="whitegrid", palette="deep") # 可视化主题
output_dir = "F:/大数据/第6章/杜邦分析图表" # 输出路径
os.makedirs(output_dir, exist_ok=True) # 自动创建目录
# -------------------------- 日期处理函数 --------------------------
def extract_year(date_value):
"""从'截止日期'列提取年份(支持多种格式)"""
date_str = str(date_value).strip()
if "-" in date_str: return int(date_str.split("-")[0]) # 处理2023-12-31
if "年" in date_str: return int(date_str.split("年")[0]) # 处理2023年12月
if len(date_str)>=4 and date_str.isdigit(): return int(date_str[:4]) # 处理20231231
return np.nan
# -------------------------- 数据读取与清洗 --------------------------
def load_financial_data(file_path):
"""加载财务报表并处理日期列(返回带年份列的清洗后DataFrame)"""
df = pd.read_excel(file_path, sheet_name=0) # 强制读取第一个工作表
if not isinstance(df, pd.DataFrame):
raise TypeError(f"文件{file_path}读取失败,返回类型为{type(df)}")
# 查找"截止日期"列(关键列)
date_cols = [col for col in df.columns if "截止日期" in col]
if not date_cols:
raise ValueError(f"文件{file_path}未找到'截止日期'列")
# 提取年份并清洗数据
df["年份"] = df[date_cols[0]].apply(extract_year)
df = df[df["年份"].notna()].astype({"年份": int}) # 过滤无效年份并转换为整数
return df.sort_values("年份").reset_index(drop=True) # 按年份排序
# -------------------------- 列名动态匹配 --------------------------
def find_column(df, keywords):
"""动态匹配DataFrame中的列名(支持模糊匹配)"""
for col in df.columns:
if any(keyword in col for keyword in keywords):
return col
raise ValueError(f"未找到包含以下关键词的列: {keywords}")
# -------------------------- 杜邦分析核心逻辑 --------------------------
def dupont_analysis(balance_sheet_path, income_statement_path):
# 加载并清洗数据
bs = load_financial_data(balance_sheet_path) # 资产负债表
is_ = load_financial_data(income_statement_path) # 利润表
# 匹配关键列(支持列名变体)
bs_assets = find_column(bs, ["资产总计", "资产合计"]) # 资产总计列
bs_equity = find_column(bs, ["所有者权益", "股东权益", "净资产"]) # 所有者权益列
is_revenue = find_column(is_, ["营业收入", "营业总收入"]) # 营业收入列
is_net_profit = find_column(is_, ["净利润", "归属于母公司", "归母净利润"]) # 净利润列
# 按年份合并数据(仅保留共同年份)
merged_df = bs[["年份", bs_assets, bs_equity]].merge(
is_[["年份", is_revenue, is_net_profit]],
on="年份",
how="inner"
)
merged_df.columns = ["年份", "资产总计", "所有者权益合计", "营业收入", "净利润"] # 统一列名
# 计算杜邦指标
merged_df["ROE"] = (merged_df["净利润"] / merged_df["所有者权益合计"]) * 100 # 净资产收益率
merged_df["净利润率"] = (merged_df["净利润"] / merged_df["营业收入"]) * 100 # 净利润率
merged_df["平均总资产"] = (merged_df["资产总计"] + merged_df["资产总计"].shift(1)) / 2 # 平均总资产
merged_df["总资产周转率"] = merged_df["营业收入"] / merged_df["平均总资产"].fillna(merged_df["资产总计"]) # 总资产周转率(处理首年)
merged_df["权益乘数"] = merged_df["资产总计"] / merged_df["所有者权益合计"] # 权益乘数
merged_df["ROA"] = merged_df["净利润率"] * merged_df["总资产周转率"] # 总资产收益率
return merged_df.dropna() # 过滤缺失值
# -------------------------- 可视化输出(完整标题版) --------------------------
def visualize_duPont(df):
# -------------------- 图表1:杜邦核心指标趋势图 --------------------
fig = plt.figure(figsize=(16, 10), constrained_layout=True)
fig.suptitle("格力电器杜邦分析核心指标趋势(2018-2023)", fontsize=18, fontweight="bold", y=1.02) # 主标题
gs = GridSpec(2, 2, figure=fig, height_ratios=[2, 1]) # 子图布局(2行2列)
# ROE趋势(主指标)
ax1 = fig.add_subplot(gs[0, :])
sns.lineplot(data=df, x="年份", y="ROE", marker="o", linewidth=3, color="#E64B35", ax=ax1)
ax1.set_title("核心指标:净资产收益率(ROE)", fontsize=16, pad=15) # 子标题
ax1.set_ylabel("ROE(%)", fontsize=14)
ax1.yaxis.set_major_formatter(PercentFormatter())
ax1.grid(linestyle="--", alpha=0.6)
# 添加数据标签
for x, y in df[["年份", "ROE"]].values:
ax1.text(x, y+0.5, f"{y:.1f}%", ha="center", fontsize=12, color="#E64B35")
# 净利润率趋势(柱状图)
ax2 = fig.add_subplot(gs[1, 0])
sns.barplot(data=df, x="年份", y="净利润率", color="#4DBBD5", ax=ax2)
ax2.set_title("盈利能力:净利润率", fontsize=15, pad=12) # 子标题
ax2.set_ylabel("净利润率(%)", fontsize=13)
ax2.yaxis.set_major_formatter(PercentFormatter())
# 添加柱状图标签
for p in ax2.patches:
height = p.get_height()
ax2.text(p.get_x()+p.get_width()/2., height+0.2, f"{height:.1f}%", ha="center", fontsize=11)
# 总资产周转率趋势(线图)
ax3 = fig.add_subplot(gs[1, 1])
sns.lineplot(data=df, x="年份", y="总资产周转率", marker="s", linewidth=2.5, color="#3C5488", ax=ax3)
ax3.set_title("运营效率:总资产周转率", fontsize=15, pad=12) # 子标题
ax3.set_ylabel("周转率(次)", fontsize=13)
ax3.grid(linestyle="--", alpha=0.6)
# 添加趋势阴影
ax3.fill_between(df["年份"], df["总资产周转率"], alpha=0.1, color="#3C5488")
plt.savefig(f"{output_dir}/杜邦核心指标趋势图.png", dpi=300, bbox_inches="tight") # 保存时自动调整边界
# -------------------- 图表2:ROE分解瀑布图 --------------------
fig, ax = plt.subplots(figsize=(14, 8))
base_year = df["年份"].min()
current_year = df["年份"].max()
ax.set_title(f"{base_year}年-{current_year}年ROE驱动因素分解", fontsize=18, fontweight="bold", pad=20) # 主标题
# 计算各因素贡献值
base = df[df["年份"] == base_year].iloc[0]
current = df[df["年份"] == current_year].iloc[0]
factors = [
("基年ROE", base["ROE"], "#3C5488"),
("净利润率贡献", base["ROE"]*(current["净利润率"]/base["净利润率"]-1), "#4DBBD5"),
("总资产周转率贡献", base["ROE"]*(current["净利润率"]/base["净利润率"])*(current["总资产周转率"]/base["总资产周转率"]-1), "#00A087"),
("权益乘数贡献", base["ROE"]*(current["净利润率"]/base["净利润率"])*(current["总资产周转率"]/base["总资产周转率"])*(current["权益乘数"]/base["权益乘数"]-1), "#F39B7F"),
("当前年ROE", current["ROE"], "#E64B35"),
]
# 绘制瀑布图
y_pos = np.arange(len(factors))
bars = ax.barh(y_pos, [f[1] for f in factors], color=[f[2] for f in factors], edgecolor="black", height=0.6)
ax.set_yticks(y_pos)
ax.set_yticklabels([f[0] for f in factors], fontsize=12)
ax.set_xlabel("ROE贡献值(%)", fontsize=14)
ax.xaxis.set_major_formatter(PercentFormatter())
# 添加数值标签
for bar, (name, value, _) in zip(bars, factors):
ax.text(bar.get_width()+0.5 if value>0 else bar.get_width()-1.5,
bar.get_y()+bar.get_height()/2,
f"{value:.1f}%", va="center", fontsize=11)
plt.savefig(f"{output_dir}/ROE驱动因素分解图.png", dpi=300, bbox_inches="tight")
# -------------------- 图表3:指标相关性热力图 --------------------
fig, ax = plt.subplots(figsize=(12, 9))
ax.set_title("杜邦分析核心指标相关性矩阵", fontsize=18, fontweight="bold", pad=25) # 主标题
corr = df[["ROE", "净利润率", "总资产周转率", "权益乘数", "ROA"]].corr().round(2)
# 绘制热力图
sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1,
linewidths=0.5, annot_kws={"fontsize": 14}, square=True,
cbar_kws={"shrink": 0.8, "label": "相关系数(r)"})
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right", fontsize=12)
ax.set_yticklabels(ax.get_yticklabels(), rotation=0, fontsize=12)
plt.savefig(f"{output_dir}/指标相关性热力图.png", dpi=300, bbox_inches="tight")
print(f"所有完整图表已保存至:{output_dir}")
# -------------------------- 主程序 --------------------------
if __name__ == "__main__":
try:
# 替换为实际文件路径(确保Excel文件包含"截止日期"列)
balance_sheet_path = "F:/大数据/第6章/格力资产负债表.xlsx"
income_statement_path = "F:/大数据/第6章/格力利润表.xlsx"
# 执行杜邦分析
analysis_df = dupont_analysis(balance_sheet_path, income_statement_path)
# 保存分析结果(保留2位小数)
analysis_df.round(2).to_excel(f"{output_dir}/格力杜邦分析数据.xlsx", index=False)
print(f"分析数据已保存至:{output_dir}/格力杜邦分析数据.xlsx")
# 生成完整图表
visualize_duPont(analysis_df)
except Exception as e:
print(f"程序运行错误: {str(e)}") 运行代码
最新发布