# 导入包并设置绘图主题、色板、字体和输出格式。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
okabe_ito = ["#0072B2", "#E69F00", "#009E73", "#D55E00", "#CC79A7", "#56B4E9", "#999999"]
sns.set_theme(
style="ticks", # 使用带刻度、少装饰的主题
context="notebook", # 使用适合 notebook 阅读的字号
)
sns.set_palette(okabe_ito) # 使用色盲友好色板。当然也可以不设置,这样将会使用默认调色板。
plt.rcParams["font.sans-serif"] = ["Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei"] # 支持中文
plt.rcParams["axes.unicode_minus"] = False # 正常显示负号
%config InlineBackend.figure_formats = ["svg"] # 使用 SVG 格式12 Seaborn:常见统计图形
Python 常用的绘图工具有多个层次。matplotlib 是基础绘图库,能控制图形、坐标轴、标题、图例、颜色和保存格式;很多其他绘图库都建立在它之上。
Seaborn 是常用的统计绘图库。它基于 matplotlib,写法更适合 pandas 的 DataFrame,可以较方便地按变量作图、按组着色、计算分组统计量,并保留 matplotlib 的细节调整能力。
本章目标是基于数据绘制常见统计图形。我们使用同一份公司年度数据,依次绘制分布图、类别比较图、散点图、折线图、热力图、分面图和多子图组合。每类图先直接画出基本效果,再添加其他参数,最后微调绘图风格。
本章使用的数据
本章使用的数据放在工作目录下的 data 文件夹中。读取时用相对路径,例如 data/company_visualization_sample.xlsx。
data/company_visualization_sample.xlsx:从 CSMAR 切出的上市公司年度面板数据,保留了绘图常用变量,用于练习分布图、类别比较图、散点图、折线图、热力图、分面图和多子图组合。下载- 使用 seaborn 内置的
fmri数据,通过sns.load_dataset("fmri")读取,不需要单独下载。
12.1 本章内容
- 直方图、密度曲线和箱形图:观察单个变量的分布,比较少数组别或多个组别。
- 计数图和柱状图:比较类别数量和分组均值。
- 散点图:观察两个数值变量的关系,处理右偏金额变量和对数变量。
- 折线图:观察指标随时间变化的趋势。
- 热力图(可选):把二维汇总表转换成颜色深浅。
- 分面图和多子图:把多个小图放在同一张图中比较。
- 图例、标题、坐标轴、配色、注释、边框和留白:把图整理成更适合展示的版本。
12.2 准备工作
这里提供一个常见绘图之前的准备工作代码,可以认为是例行公事,直接粘贴到单元格中执行即可。
- 导入
pandas、numpy、matplotlib.pyplot和seaborn。 - 设置 seaborn 主题和色盲友好的 Okabe-Ito 色板。
- 重要: 设置中文字体,避免中文标题和坐标轴标签显示异常。
- 重要: 设置负号显示,避免负数坐标轴显示异常。
- 设置 notebook 图形输出为 SVG 格式。
常见图形格式:
- PNG、JPG:位图格式,图片由像素组成,适合照片、截图和网页图片;放大太多可能变模糊。
- SVG、PDF:矢量格式,图片由线条、形状和文字描述,适合统计图、流程图和论文插图。
- 本章使用 SVG 格式。SVG 是矢量格式,任意放大,或者导出后用于打印,一般都不会变模糊。
读取数据,并生成绘图常用变量
本章使用从 CSMAR 切出的上市公司年度面板数据。数据文件不变;为了画图更清楚,只额外生成几列展示用变量。
净利率:净利润_亿元 / 营业收入_亿元。负债水平:资产负债率大于等于 0.6 记为“较高”,否则记为“正常”。行业简称:保留原始行业名称,但绘图时使用更短的简称,避免长标签挤占图形空间。
# 读取公司年度数据,并生成后续绘图要用的变量。
annual = pd.read_excel("data/company_visualization_sample.xlsx")
annual["统计截止日期"] = pd.to_datetime(annual["统计截止日期"])
annual["年份"] = annual["统计截止日期"].dt.year
annual["净利率"] = (annual["净利润_亿元"] / annual["营业收入_亿元"]).round(4)
annual["负债水平"] = np.where(annual["资产负债率"] >= 0.6, "较高", "正常")
industry_short = {
"计算机、通信和其他电子设备制造业": "电子设备",
"电气机械及器材制造业": "电气设备",
"医药制造业": "医药",
"软件和信息技术服务业": "软件服务",
"汽车制造业": "汽车",
"房地产业": "房地产",
}
annual["行业简称"] = annual["行业名称"].map(industry_short)
annual[["证券代码", "股票简称", "统计截止日期", "行业名称", "行业简称", "营业收入_亿元", "净利润_亿元", "资产负债率"]].head()| 证券代码 | 股票简称 | 统计截止日期 | 行业名称 | 行业简称 | 营业收入_亿元 | 净利润_亿元 | 资产负债率 | |
|---|---|---|---|---|---|---|---|---|
| 0 | 403 | 振兴生化 | 2018-12-31 | 医药制造业 | 医药 | 8.60 | 0.75 | 0.5298 |
| 1 | 403 | 双林生物 | 2019-12-31 | 医药制造业 | 医药 | 9.16 | 1.58 | 0.3696 |
| 2 | 403 | 派林生物 | 2020-12-31 | 医药制造业 | 医药 | 10.50 | 1.83 | 0.4149 |
| 3 | 403 | 派林生物 | 2021-12-31 | 医药制造业 | 医药 | 19.72 | 3.92 | 0.1072 |
| 4 | 403 | 派林生物 | 2022-12-31 | 医药制造业 | 医药 | 24.05 | 5.88 | 0.1405 |
# 统计每个行业包含的公司数量。
annual.groupby("行业简称")["证券代码"].nunique().sort_values(
ascending=False, # 从高到低排序
).reset_index()| 行业简称 | 证券代码 | |
|---|---|---|
| 0 | 电子设备 | 32 |
| 1 | 电气设备 | 27 |
| 2 | 医药 | 24 |
| 3 | 软件服务 | 19 |
| 4 | 汽车 | 15 |
| 5 | 房地产 | 11 |
# 筛选 2020 年数据,并查看主要绘图变量。
annual_2020 = annual.loc[annual["年份"] == 2020].copy()
annual_2020[["股票简称", "行业简称", "营业收入_亿元", "净利润_亿元", "资产负债率", "营业毛利率", "负债水平"]].head()| 股票简称 | 行业简称 | 营业收入_亿元 | 净利润_亿元 | 资产负债率 | 营业毛利率 | 负债水平 | |
|---|---|---|---|---|---|---|---|
| 2 | 派林生物 | 医药 | 10.50 | 1.83 | 0.4149 | 0.4882 | 正常 |
| 8 | 仁和药业 | 医药 | 41.06 | 6.58 | 0.1102 | 0.4018 | 正常 |
| 14 | 广济药业 | 医药 | 6.88 | 0.70 | 0.3101 | 0.4063 | 正常 |
| 20 | 海翔药业 | 医药 | 24.71 | 3.20 | 0.1848 | 0.4542 | 正常 |
| 26 | 力生制药 | 医药 | 11.41 | 0.09 | 0.1434 | 0.6148 | 正常 |
12.3 Seaborn 通用写法
多数 seaborn 图形函数的写法相近:
sns.xxxplot(data=数据, x=横轴变量, y=纵轴变量, hue=分组变量, ...)常用参数:
data:数据表,通常是 pandas 的DataFrame。x:横轴变量。y:纵轴变量。x和y至少指定一个;有些图只需要一个变量,例如直方图可以只写x。- 横向和纵向由
x/y决定。类别变量放在x轴,通常得到竖向柱;类别变量放在y轴,通常得到横向柱。 hue:分组变量。不同组通常用不同颜色显示。order/hue_order:控制分类变量的显示顺序。color/palette:设置颜色或配色方案。ax:指定把图画到哪一个坐标轴上,常用于微调图形和组合多张图。
12.4 分布图
分布图用来观察一个数值变量集中在哪里、是否偏态、有没有明显极端值。这里以 营业毛利率 为例。直方图适合看分布形状;箱形图表达的信息接近,但更适合把多组放在一起比较。
histplot() 画直方图。
常用参数:
data:数据表。x/y:要绘制分布的变量;只给x是一维直方图,同时给x和y是二维直方图。hue:按某个分类变量分组着色。bins:直方图分组数量。kde:是否叠加平滑曲线。multiple:多组直方图如何摆放,如"layer"叠放。cbar:二维直方图是否显示颜色条。ax:把图画到指定坐标轴。
# 画 2020 年营业毛利率的基础直方图。
sns.histplot(data=annual_2020, x="营业毛利率")添加其他参数
加入 hue 可以比较两组分布;multiple="layer" 表示叠放;bins 控制区间数量;kde=True 增加平滑曲线。为了让演示更清楚,这里比较营业毛利率差异较明显的“医药”和“汽车”两个行业。
# 筛选医药和汽车行业,准备比较营业毛利率分布。
margin_compare = annual_2020.loc[annual_2020["行业简称"].isin(["医药", "汽车"])].copy()
margin_compare[["股票简称", "行业简称", "营业毛利率"]].head()| 股票简称 | 行业简称 | 营业毛利率 | |
|---|---|---|---|
| 2 | 派林生物 | 医药 | 0.4882 |
| 8 | 仁和药业 | 医药 | 0.4018 |
| 14 | 广济药业 | 医药 | 0.4063 |
| 20 | 海翔药业 | 医药 | 0.4542 |
| 26 | 力生制药 | 医药 | 0.6148 |
先看一眼 margin_compare,确认它确实只保留了“医药”和“汽车”两个行业。下面再把这张小表交给 histplot()。
# 按行业分组画营业毛利率直方图和密度曲线。
sns.histplot(
data=margin_compare,
x="营业毛利率",
hue="行业简称", # 按行业分组着色
multiple="layer", # 多组直方图叠放在同一坐标轴上
bins=15, # 把数值范围分成 15 个区间
kde=True, # 叠加平滑密度曲线
)微调绘图风格
展示用图形要处理颜色、标题、坐标轴和图例。分布图重叠时,用半透明减少遮挡;组别名称较短时,图例可以留在图内或紧贴图形。
plt.subplots() 用来创建图形对象和坐标轴对象。它返回两个对象:
fig:整张图,也就是 figure。保存图片、设置整张图标题、安排多个子图时,经常会用到它。ax:一张具体的坐标轴,也就是 axes。seaborn 的图通常画在ax上,标题、坐标轴名称、图例、网格线也常通过ax调整。
常用参数:
figsize:图形宽高,单位是英寸。nrows/ncols:创建多子图时的行数和列数。sharex/sharey:多个子图是否共用横轴或纵轴。
单张图也可以使用 fig, ax = plt.subplots()。这样写比直接画图多一步,但后续微调更清楚。
# 微调医药和汽车行业营业毛利率分布图。
fig, ax = plt.subplots(figsize=(6, 4)) # 设置图形宽高
sns.histplot(
data=margin_compare,
x="营业毛利率",
hue="行业简称", # 按行业分组着色
multiple="layer", # 多组直方图叠放在同一坐标轴上
bins=16, # 把数值范围分成 16 个区间
kde=True, # 叠加平滑密度曲线
alpha=0.45, # 设置透明度,减少重叠遮挡
palette={"医药": "#0072B2", "汽车": "#D55E00"}, # 指定两组颜色
ax=ax, # 画到前面创建的坐标轴
)
ax.set( # 设置标题和坐标轴名称
title="2020 年医药与汽车行业营业毛利率分布",
xlabel="营业毛利率",
ylabel="公司数量",
)
legend = ax.get_legend() # 取得当前图例对象
if legend is not None:
legend.set_title("行业") # 修改图例标题
legend.set_frame_on(False) # 去掉图例边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠箱形图
boxplot() 适合快速查看中位数、四分位范围和异常值。它和直方图表达的信息接近,但更紧凑,适合一次比较多组。习惯上,分类变量放在横轴,数值变量放在纵轴。
常用参数:
data:数据表。x/y:分类变量和数值变量。hue:在箱形图里再分一层组别。order:分类显示顺序。color/palette:颜色或配色方案。ax:把图画到指定坐标轴。
先把箱形图要用的数据单独取出来。我们需要看到公司名称、行业和营业毛利率,确认后面画图用的是这三列。
# 取出箱形图需要的行业和营业毛利率数据。
box_data = annual_2020[["股票简称", "行业简称", "营业毛利率"]].copy()
box_data.head()| 股票简称 | 行业简称 | 营业毛利率 | |
|---|---|---|---|
| 2 | 派林生物 | 医药 | 0.4882 |
| 8 | 仁和药业 | 医药 | 0.4018 |
| 14 | 广济药业 | 医药 | 0.4063 |
| 20 | 海翔药业 | 医药 | 0.4542 |
| 26 | 力生制药 | 医药 | 0.6148 |
箱形图会给每个行业画一个箱子。画图前先看每个行业有多少家公司,样本太少的组要谨慎解释。
# 统计每个行业用于箱形图的公司数量。
box_group_size = box_data["行业简称"].value_counts().rename_axis("行业简称").reset_index(name="公司数量")
box_group_size| 行业简称 | 公司数量 | |
|---|---|---|
| 0 | 电子设备 | 32 |
| 1 | 电气设备 | 27 |
| 2 | 医药 | 24 |
| 3 | 软件服务 | 19 |
| 4 | 汽车 | 15 |
| 5 | 房地产 | 11 |
箱形图本身不要求先计算中位数。这里先算各行业中位数,是为了确定横轴行业的显示顺序。
# 计算各行业营业毛利率中位数。
box_median = box_data.groupby("行业简称", as_index=False)["营业毛利率"].median()
box_median.round({"营业毛利率": 3})| 行业简称 | 营业毛利率 | |
|---|---|---|
| 0 | 医药 | 0.486 |
| 1 | 房地产 | 0.315 |
| 2 | 汽车 | 0.232 |
| 3 | 电子设备 | 0.265 |
| 4 | 电气设备 | 0.291 |
| 5 | 软件服务 | 0.235 |
再把中位数从高到低排序,并取出行业名称列表。这个列表会传给 order 参数,控制箱形图从左到右的顺序。
# 按中位数从高到低生成箱形图行业顺序。
box_median_sorted = box_median.sort_values("营业毛利率", ascending=False)
box_order = box_median_sorted["行业简称"].tolist()
box_median_sorted.round({"营业毛利率": 3})| 行业简称 | 营业毛利率 | |
|---|---|---|
| 0 | 医药 | 0.486 |
| 1 | 房地产 | 0.315 |
| 4 | 电气设备 | 0.291 |
| 3 | 电子设备 | 0.265 |
| 5 | 软件服务 | 0.235 |
| 2 | 汽车 | 0.232 |
现在已经准备好两样东西:box_data 是绘图数据,box_order 是行业顺序。下面再画箱形图。
# 用准备好的数据和顺序画营业毛利率箱形图。
fig, ax = plt.subplots(figsize=(6.2, 4.2)) # 设置图形宽高
sns.boxplot(
data=box_data,
x="行业简称",
y="营业毛利率",
order=box_order, # 按前面算出的中位数顺序显示行业
color="#56B4E9", # 使用统一颜色
ax=ax, # 画到前面创建的坐标轴
)
ax.set(title="2020 年各行业营业毛利率分布", xlabel="行业", ylabel="营业毛利率") # 设置标题和坐标轴名称
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠# 查看营业毛利率最高的几家公司。
annual_2020[["股票简称", "行业简称", "营业毛利率"]].sort_values(
"营业毛利率",
ascending=False, # 从高到低排序
).head()| 股票简称 | 行业简称 | 营业毛利率 | |
|---|---|---|---|
| 740 | 万兴科技 | 软件服务 | 0.9485 |
| 44 | 康弘药业 | 医药 | 0.8999 |
| 116 | 益佰制药 | 医药 | 0.7899 |
| 32 | 双成药业 | 医药 | 0.7766 |
| 74 | 九典制药 | 医药 | 0.7485 |
12.5 类别比较图
类别比较图用来比较不同组的数量或均值。这里先比较行业公司数量,再比较行业平均净利率。
countplot() 统计类别变量的频数。
常用参数:
data:数据表。x/y:要统计的类别变量。类别变量放在x轴时是竖向柱,放在y轴时是横向柱;类别标签较长时常放在y轴。hue:再按另一个分类变量分组。order:类别显示顺序。color/palette:颜色或配色方案。ax:把图画到指定坐标轴。
# 画各行业公司数量的基础计数图。
sns.countplot(data=annual_2020, y="行业简称") # 类别变量放在 y 轴,得到横向柱添加其他参数
order 控制类别显示顺序,hue 再加入一层分类。这里按行业公司数量排序,同时用颜色区分负债水平。
# 计算各行业公司数量,并保存计数图排序。
industry_count_order = annual_2020["行业简称"].value_counts().index
annual_2020["行业简称"].value_counts().reset_index()| 行业简称 | count | |
|---|---|---|
| 0 | 电子设备 | 32 |
| 1 | 电气设备 | 27 |
| 2 | 医药 | 24 |
| 3 | 软件服务 | 19 |
| 4 | 汽车 | 15 |
| 5 | 房地产 | 11 |
上面先看各行业公司数量,并把这个顺序保存到 industry_count_order。下面按这个顺序绘图,并用 ax.bar_label() 在柱体末端添加数字。
# 给横向计数图的柱体添加公司数量标注。
fig, ax = plt.subplots(figsize=(6, 4)) # 设置图形宽高
sns.countplot(
data=annual_2020,
y="行业简称",
order=industry_count_order, # 按公司数量排序显示行业
ax=ax, # 画到前面创建的坐标轴
)
ax.bar_label(ax.containers[0], padding=5, fontsize=10) # 在柱体末端添加数字
ax.margins(x=0.12) # 给数字留出横向空间
ax.set(xlabel="公司数量", ylabel="行业") # 设置坐标轴名称
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠如果计数图使用了 hue 分组,图中会有多组柱体。此时需要对 ax.containers 循环,分别给每一组柱体添加数字。
# 按负债水平分组画行业计数图,并给每组柱体添加数字。
ax = sns.countplot(
data=annual_2020,
y="行业简称",
hue="负债水平", # 按负债水平分组着色
order=industry_count_order, # 按公司数量排序显示行业
)
for container in ax.containers:
ax.bar_label(container, padding=5, fontsize = 10) # 分别给每组柱体添加数字
ax.margins(x=0.12) # 给数字留出横向空间微调绘图风格
处理类别图时,优先使用短标签;如果必须使用长标签,通常把类别放到 y 轴。图例放到图内空白处或图外右侧,但不要让图例比图形主体还宽。
# 微调按负债水平分组的行业计数图。
fig, ax = plt.subplots(figsize=(6, 4)) # 设置图形宽高
sns.countplot(
data=annual_2020,
y="行业简称",
hue="负债水平", # 按负债水平分组着色
order=industry_count_order, # 按公司数量排序显示行业
palette={"正常": "#0072B2", "较高": "#D55E00"}, # 指定两组颜色
ax=ax, # 画到前面创建的坐标轴
)
for container in ax.containers:
ax.bar_label(container, padding=5, fontsize = 10) # 分别给每组柱体添加数字
ax.margins(x=0.12) # 给数字留出横向空间
ax.set( # 设置标题和坐标轴名称
title="2020 年样本公司行业分布",
xlabel="公司数量",
ylabel="行业",
)
ax.legend(title="负债水平", loc="lower right", frameon=False) # 设置图例标题、位置,并去掉边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠柱状图比较均值
barplot() 画柱状图。一般要指定 x 和 y,其中一个是分类变量,另一个是数值变量。分类变量决定分组,数值变量决定柱子的长度。比如要比较每个行业的平均净利率,可以先算出一张“行业 + 平均净利率”的表,再把这张表交给 barplot()。
这种“先汇总,再画图”的做法更通用。柱子的长度可以是均值、数量、中位数、比例、增长率,或者任何已经算好的指标。
常用参数:
data:数据表。x/y:分类变量和数值变量。分类变量放在x轴、数值变量放在y轴时是竖向柱;分类变量放在y轴、数值变量放在x轴时是横向柱。hue:在柱状图里再分一层组别。order:分类显示顺序。errorbar:是否显示误差线,以及误差线类型。color/palette:颜色或配色方案。ax:把图画到指定坐标轴。
下面是具体例子。先构造汇总表,再指定 x 和 y 画图。这里把行业放在 y 轴、净利率放在 x 轴,所以画出来是横向柱。
先展示 industry_margin,确认每一行是一个行业及其平均净利率。
# 计算各行业平均净利率,作为柱状图数据。
# 这里只是简单的分组求均值再排序
industry_margin = (
annual_2020.groupby("行业简称", as_index=False)["净利率"] # 保留行业为普通列,便于绘图
.mean()
.sort_values(by="净利率", ascending=False) # 从高到低排序
)
industry_margin| 行业简称 | 净利率 | |
|---|---|---|
| 4 | 电气设备 | 0.104530 |
| 1 | 房地产 | 0.100700 |
| 3 | 电子设备 | 0.056406 |
| 0 | 医药 | 0.040125 |
| 5 | 软件服务 | 0.023889 |
| 2 | 汽车 | -0.062340 |
# 用汇总表画各行业平均净利率柱状图。
fig, ax = plt.subplots(figsize=(6, 4)) # 设置图形宽高
sns.barplot(
data=industry_margin,
y="行业简称",
x="净利率",
color="#009E73", # 使用统一颜色
ax=ax, # 画到前面创建的坐标轴
)
ax.bar_label(ax.containers[0], fmt="%.3f", padding=3, fontsize=9) # 在柱体末端添加数字
ax.margins(x=0.18) # 给数字留出横向空间
ax.set(title="2020 年各行业平均净利率", xlabel="平均净利率", ylabel="行业") # 设置标题和坐标轴名称
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠最后补充一种直接写法。如果不预先算好汇总表,也可以把原始明细数据交给 barplot(),同时指定分类变量和数值变量。这个时候,barplot() 会自动按分类变量分组,并默认计算数值变量的均值。
# 直接用明细数据画柱状图,barplot 默认按行业计算净利率均值。
ax = sns.barplot(
data=annual_2020,
y="行业简称",
x="净利率",
errorbar=None, # 先不显示误差线,只看均值柱体
)
ax.bar_label(ax.containers[0], fmt="%.3f", padding=3, fontsize=9) # 在柱体末端添加数字
ax.margins(x=0.18) # 给数字留出横向空间12.6 散点图和金额变量
散点图用来观察两个数值变量之间的关系。这里使用 总资产_亿元 和 营业收入_亿元:两个变量都是正数,适合展示金额变量常见的右偏和对数处理。
第一步:先画原始散点图
scatterplot() 画散点图。
常用参数:
data:数据表。x/y:横轴和纵轴变量。hue:用颜色表示分类变量。style:用点形状表示分类变量。size:用点大小表示数值变量。sizes:点大小范围。alpha:透明度。ax:把图画到指定坐标轴。
# 画总资产和营业收入的基础散点图。
sns.scatterplot(data=annual_2020, x="总资产_亿元", y="营业收入_亿元")这张图容易看到一个问题:大量公司挤在左下角,少数规模较大的公司把坐标轴拉得很长。这个现象在金额变量中很常见。
第二步:查看一个金额变量的分布
为了说明金额变量常见的右偏问题,用 总资产_亿元 一个变量做示范即可。这里要看的是:大多数公司是否集中在较小范围,少数大公司是否把横轴拉得很长。
# 查看总资产的原始分布。
sns.histplot(
data=annual_2020,
x="总资产_亿元",
bins=30, # 把数值范围分成 30 个区间
kde=True, # 叠加平滑密度曲线
)总资产_亿元 明显右偏,而且取值为正。对这类变量,取对数通常能压缩极端大值,让中小规模公司的差异更容易看见。后面的散点图需要同时比较总资产和营业收入,所以两个变量都生成对数列。
第三步:取对数后再看分布
np.log() 是自然对数,要求变量为正。取对数后先展示数据,确认新变量的含义。
# 对总资产和营业收入取对数,并查看结果。
annual_2020["log_总资产"] = np.log(annual_2020["总资产_亿元"])
annual_2020["log_营业收入"] = np.log(annual_2020["营业收入_亿元"])
annual_2020[["股票简称", "总资产_亿元", "log_总资产", "营业收入_亿元", "log_营业收入"]].head()| 股票简称 | 总资产_亿元 | log_总资产 | 营业收入_亿元 | log_营业收入 | |
|---|---|---|---|---|---|
| 2 | 派林生物 | 16.53 | 2.805177 | 10.50 | 2.351375 |
| 8 | 仁和药业 | 63.05 | 4.143928 | 41.06 | 3.715034 |
| 14 | 广济药业 | 19.79 | 2.985177 | 6.88 | 1.928619 |
| 20 | 海翔药业 | 71.79 | 4.273745 | 24.71 | 3.207208 |
| 26 | 力生制药 | 50.22 | 3.916413 | 11.41 | 2.434490 |
# 查看总资产取对数后的分布。
sns.histplot(
data=annual_2020,
x="log_总资产",
bins=30, # 把数值范围分成 30 个区间
kde=True, # 叠加平滑密度曲线
)第四步:绘制对数版散点图
这里不是只改变坐标轴显示,而是直接使用新生成的对数变量作图。
# 画对数总资产和对数营业收入的基础散点图。
sns.scatterplot(data=annual_2020, x="log_总资产", y="log_营业收入")第五步:添加常用参数
在对数版散点图上加入 hue、style 和 size。现在点不再挤在左下角,多层变量也更容易阅读。
# 在散点图中加入行业、负债水平和净利率映射。
sns.scatterplot(
data=annual_2020,
x="log_总资产",
y="log_营业收入",
hue="行业简称", # 用颜色表示行业
style="负债水平", # 用点形状表示负债水平
size="净利率", # 用点大小表示净利率
sizes=(30, 160), # 设置点大小范围
alpha=0.82, # 设置透明度
)第六步:微调绘图风格
展示用图形要控制图例、颜色、标题、坐标轴和留白。行业名称使用简称,图例放到右侧并去掉边框。
# 微调对数资产和对数收入散点图。
fig, ax = plt.subplots(figsize=(8.2, 4.8)) # 设置图形宽高
sns.scatterplot(
data=annual_2020,
x="log_总资产",
y="log_营业收入",
hue="行业简称", # 用颜色表示行业
style="负债水平", # 用点形状表示负债水平
size="净利率", # 用点大小表示净利率
sizes=(35, 160), # 设置点大小范围
alpha=0.82, # 设置透明度
ax=ax, # 画到前面创建的坐标轴
)
ax.set( # 设置标题和坐标轴名称
title="2020 年总资产与营业收入关系(对数变量)",
xlabel="log(总资产,亿元)",
ylabel="log(营业收入,亿元)",
)
ax.legend( # 设置图例
title="行业 / 负债 / 净利率",
bbox_to_anchor=(1.01, 1), # 把图例放到坐标轴右侧
loc="upper left", # 图例左上角对齐锚点
frameon=False, # 去掉图例边框
borderaxespad=0, # 减少图例和坐标轴之间的空白
)
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠12.7 折线图
折线图用来观察指标随时间变化的趋势。年度面板数据通常先按年份或“年份 + 组别”汇总,再绘图。
lineplot() 画折线图。
常用参数:
data:数据表。x/y:横轴和纵轴变量。hue:按分类变量画多条线。marker:是否在每个点上显示标记。errorbar:是否显示误差带,以及误差带类型。linewidth:线条粗细。linestyle:线型。ax:把图画到指定坐标轴。
常见点标记 marker:
"o":圆点。"s":方块。"^":上三角。"D":菱形。None:不显示点标记。
常见线型 linestyle:
"-":实线。"--":虚线。":":点线。"-.":点划线。
# 画年度净利率变化的基础折线图。
sns.lineplot(data=annual, x="年份", y="净利率")添加其他参数
加入 hue 可以分组画多条线;marker="o" 标出每个年份的位置。
# 按行业和年份计算平均净利率,并选出 3 个行业用于折线图。
industry_year = (
annual.groupby(["行业简称", "年份"], as_index=False) # 分组后保留为普通列,便于绘图
.agg(平均净利率=("净利率", "mean"))
)
line_industries = ["医药", "电气设备", "汽车"]
industry_year_3 = industry_year.loc[industry_year["行业简称"].isin(line_industries)].copy()
industry_year_3.head()| 行业简称 | 年份 | 平均净利率 | |
|---|---|---|---|
| 0 | 医药 | 2018 | -0.052887 |
| 1 | 医药 | 2019 | 0.092358 |
| 2 | 医药 | 2020 | 0.040125 |
| 3 | 医药 | 2021 | 0.072913 |
| 4 | 医药 | 2022 | 0.032800 |
先看 industry_year_3,确认每一行是一个“行业 + 年份”的平均净利率。折线太多会影响阅读,这里只选 3 个行业示范。
# 按 3 个行业画平均净利率折线图。
sns.lineplot(
data=industry_year_3,
x="年份",
y="平均净利率",
hue="行业简称", # 按行业画多条线
marker="o", # 在线上显示点标记
)黑白打印友好的线条
如果图形要打印成黑白,不能只依靠颜色区分线条。常见做法有两种:
- 同一种颜色,不同线型。
- 同一种颜色和线型,不同点标记。
下面先演示同色不同线型。
# 用相同颜色和不同线型画 3 个行业,便于黑白打印。
fig, ax = plt.subplots(figsize=(7.2, 4.4)) # 设置图形宽高
sns.lineplot(
data=industry_year_3,
x="年份",
y="平均净利率",
hue="行业简称", # 按行业分组
style="行业简称", # 用不同线型区分行业
dashes={"医药": "", "电气设备": (4, 2), "汽车": (1, 2)}, # 指定线型
markers=False, # 这里先只用线型区分
palette={"医药": "#333333", "电气设备": "#333333", "汽车": "#333333"}, # 使用同一种深色
linewidth=1, # 设置线条粗细
ax=ax, # 画到前面创建的坐标轴
)
ax.axhline(0, color="#666666", linewidth=1, linestyle="--", alpha=0.7) # 添加 y=0 参考线
ax.set( # 设置标题和坐标轴名称
title="3 个行业平均净利率变化",
xlabel="年份",
ylabel="平均净利率",
)
ax.grid(axis="y", linestyle="--", alpha=0.25) # 只添加横向辅助线
ax.legend(title="行业", frameon=False, loc="best") # 设置图例标题,并去掉边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠也可以保持同一种颜色和同一种线型,只用不同点标记区分线条。
# 用相同颜色和线型、不同点标记画 3 个行业。
fig, ax = plt.subplots(figsize=(7.2, 4.4)) # 设置图形宽高
sns.lineplot(
data=industry_year_3,
x="年份",
y="平均净利率",
hue="行业简称", # 按行业分组
style="行业简称", # 用不同点标记区分行业
markers={"医药": "o", "电气设备": "s", "汽车": "^"}, # 指定点标记
dashes=False, # 不使用不同线型
palette={"医药": "#333333", "电气设备": "#333333", "汽车": "#333333"}, # 使用同一种深色
linewidth=1, # 设置线条粗细
ax=ax, # 画到前面创建的坐标轴
)
ax.axhline(0, color="#666666", linewidth=1, linestyle="--", alpha=0.7) # 添加 y=0 参考线
ax.set(title="3 个行业平均净利率变化", xlabel="年份", ylabel="平均净利率") # 设置标题和坐标轴名称
ax.grid(axis="y", linestyle="--", alpha=0.25) # 只添加横向辅助线
ax.legend(title="行业", frameon=False, loc="best") # 设置图例标题,并去掉边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠12.8 热力图(可选)
热力图适合展示二维表格中的大小关系。比如“行业 × 年份”的平均净利率,用数字表能看,热力图也能直接看出高低格局。
heatmap() 画热力图。它需要二维矩阵,所以先用 pivot_table() 准备数据。
常用参数:
- 第一个参数:二维矩阵或二维表。
annot:是否在格子里显示数字。fmt:数字显示格式。cmap:颜色映射。linewidths:格子分隔线宽度。cbar_kws:颜色条设置。ax:把图画到指定坐标轴。
# 构造行业和年份交叉的平均净利率矩阵。
margin_matrix = annual.pivot_table(
index="行业简称", # 行索引
columns="年份", # 列索引
values="净利率", # 单元格数值
aggfunc="mean", # 同一行业年份内取均值
)
margin_matrix| 年份 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 |
|---|---|---|---|---|---|---|
| 行业简称 | ||||||
| 医药 | -0.052887 | 0.092358 | 0.040125 | 0.072913 | 0.032800 | 0.051508 |
| 房地产 | 0.113091 | 0.099636 | 0.100700 | 0.107945 | -0.052564 | -0.021400 |
| 汽车 | 0.091293 | 0.073660 | -0.062340 | 0.030087 | 0.021173 | 0.018460 |
| 电子设备 | -0.007700 | -0.059409 | 0.056406 | -0.024063 | -0.009953 | -0.051878 |
| 电气设备 | 0.021489 | -0.009141 | 0.104530 | 0.074619 | 0.056078 | 0.070400 |
| 软件服务 | -0.154474 | 0.023226 | 0.023889 | -0.060674 | -0.069732 | -0.039153 |
# 画平均净利率矩阵的基础热力图。
sns.heatmap(margin_matrix)添加其他参数
annot=True 显示数字,fmt 控制数字格式,cmap 控制配色,linewidths 控制格子分隔线。
# 在热力图中加入数值标注和配色设置。
sns.heatmap(
margin_matrix,
annot=True, # 在格子里显示数字
fmt=".2f", # 数字保留 2 位小数
cmap="viridis", # 使用连续型色图
linewidths=0.5, # 设置格子分隔线宽度
)微调绘图风格
连续数值适合使用感知均匀的色图,例如 viridis、cividis。如果数字太密,可以去掉 annot;这里格子不多,所以保留数字。
# 微调平均净利率热力图。
fig, ax = plt.subplots(figsize=(6, 4.2)) # 设置图形宽高
sns.heatmap(
margin_matrix,
annot=True, # 在格子里显示数字
fmt=".2f", # 数字保留 2 位小数
cmap="viridis", # 使用连续型色图
linewidths=0.5, # 设置格子分隔线宽度
cbar_kws={"label": "平均净利率", "shrink": 0.82}, # 设置颜色条标题和长度
ax=ax, # 画到前面创建的坐标轴
)
ax.set(title="各行业年度平均净利率", xlabel="年份", ylabel="行业") # 设置标题和坐标轴名称
plt.tight_layout() # 自动调整留白,减少文字重叠12.9 图例、分面和多图组合
前面已经能画出常见图形。实际写作业或报告时,经常还要处理图例位置、是否删除图例、分面展示和多张图组合。
图例控制
图例可以移动、改标题、去边框。如果图例和标题已经表达了同样信息,也可以删除。
ax.legend() 常用参数:
title:图例标题。loc:图例位置,如"best"、"upper left"。bbox_to_anchor:把图例放到图外时,用它指定锚点。frameon:是否显示图例边框。
ax.get_legend().remove() 用来删除已经生成的图例。
# 画资产负债率和营业毛利率散点图。
fig, ax = plt.subplots(figsize=(6, 4.5)) # 设置图形宽高
sns.scatterplot(
data=annual_2020,
x="资产负债率",
y="营业毛利率",
hue="负债水平", # 用颜色表示负债水平
palette={"正常": "#0072B2", "较高": "#D55E00"}, # 指定两组颜色
ax=ax, # 画到前面创建的坐标轴
)
ax.set(title="资产负债率与营业毛利率", xlabel="资产负债率", ylabel="营业毛利率") # 设置标题和坐标轴名称
ax.legend(title="负债水平", loc="best", frameon=False) # 自动选择图例位置,并去掉边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠# 调整散点图图例位置并比较显示效果。
fig, ax = plt.subplots(figsize=(6, 4.5)) # 设置图形宽高
sns.scatterplot(
data=annual_2020,
x="资产负债率",
y="营业毛利率",
hue="负债水平", # 用颜色表示负债水平
palette={"正常": "#0072B2", "较高": "#D55E00"}, # 指定两组颜色
ax=ax, # 画到前面创建的坐标轴
)
ax.get_legend().remove() # 删除图例
ax.set(title="资产负债率与营业毛利率(无图例版本)", xlabel="资产负债率", ylabel="营业毛利率") # 设置标题和坐标轴名称
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠分面图
如果要看 净利率 和 资产负债率 的关系,可以先画一张普通散点图。但如果还想按类别分开比较,例如每个行业单独画一张小图,就可以使用 relplot()。
relplot() 用来画关系图,可以画散点图,也可以画折线图。它的一个常见用途是分面:用 col 指定按哪个变量拆成多个列方向的小图,用 row 指定按哪个变量拆成多个行方向的小图。下面用 col="行业简称",表示每个行业单独一张散点图,再放在同一组图中比较。
分面变量要有足够样本,否则某些小图只有一两个点,看起来像空图。
常用参数:
data:数据表。x/y:每个小图里的横轴和纵轴变量。col/row:按哪些变量拆成列方向或行方向的小图。col_wrap:列方向分面太多时,每行放几个小图。col_order/row_order:分面的显示顺序。hue:用颜色区分类别。height:每个小图高度。aspect:每个小图的宽高比。palette:配色方案。
# 计算分面图中的行业显示顺序。
facet_order = annual_2020["行业简称"].value_counts().index
annual_2020["行业简称"].value_counts().reset_index()| 行业简称 | count | |
|---|---|---|
| 0 | 电子设备 | 32 |
| 1 | 电气设备 | 27 |
| 2 | 医药 | 24 |
| 3 | 软件服务 | 19 |
| 4 | 汽车 | 15 |
| 5 | 房地产 | 11 |
先看每个行业的样本数。这里按行业分面,而不是同时按“行业 × 负债水平”分面,避免某些小图只有 1 个样本。下面用颜色表示负债水平。
# 按行业分面画资产负债率和净利率散点图。
g = sns.relplot(
data=annual_2020,
x="资产负债率",
y="净利率",
col="行业简称", # 按行业拆成多个小图
col_wrap=3, # 每行放 3 个小图
col_order=facet_order, # 按样本数顺序排列小图
hue="负债水平", # 用颜色表示负债水平
height=2.6, # 每个小图的高度
aspect=1.2, # 每个小图的宽高比
palette={"正常": "#0072B2", "较高": "#D55E00"}, # 指定两组颜色
)
g.set_titles("{col_name}") # 简化每个小图标题,避免标题重叠
g.set_axis_labels("资产负债率", "净利率") # 设置所有小图的坐标轴名称
sns.despine() # 去掉上方和右侧边框多图组合
plt.subplots() 创建画布和子图网格,通常返回两个对象:
fig:整张图,也就是 figure。设置整张图标题、保存整张图、调整整体布局时会用到它。axes:子图组成的网格。axes[行, 列]指向其中一张子图。每个 seaborn 函数用ax=指定画到哪个位置。
常用参数:
nrows/ncols:子图网格的行数和列数。figsize:整张图的宽高。sharex/sharey:多个子图是否共用横轴或纵轴。
先看一个 2 行 2 列的示意图。axes[0, 0] 是第 0 行第 0 列,也就是左上角;axes[1, 1] 是第 1 行第 1 列,也就是右下角。
显示示意图代码
# 绘制 2×2 子图网格的位置示意图。
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(5, 3.6))
for row in range(2):
for col in range(2):
ax = axes[row, col]
ax.text(
0.5,
0.5,
f"axes[{row}, {col}]",
ha="center",
va="center",
fontsize=14,
weight="bold",
)
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor("#f2f2f2")
fig.suptitle("2×2 子图网格中的 axes 位置", fontsize=13)
plt.tight_layout()正式绘图时,每一个子图的绘制方法和前面几乎完全一样。区别在于:最后要用 ax 参数指定画到哪一个坐标轴上。例如 ax=axes[0, 0] 表示画到左上角,ax=axes[1, 1] 表示画到右下角。
# 组合多个子图,集中展示几类常见图形。
fig, axes = plt.subplots(
nrows=2, # 子图行数
ncols=2, # 子图列数
figsize=(11.5, 7.8), # 整张图的宽高
)
sns.histplot(
data=annual_2020,
x="营业毛利率",
kde=True, # 叠加平滑密度曲线
color="#0072B2", # 设置颜色
ax=axes[0, 0], # 画到左上角子图
)
axes[0, 0].set(title="A. 营业毛利率分布", xlabel="营业毛利率", ylabel="公司数量") # 设置子图标题和坐标轴名称
sns.boxplot(
data=annual_2020,
x="行业简称",
y="营业毛利率",
order=box_order, # 按前面算出的中位数顺序显示行业
color="#56B4E9", # 使用统一颜色
ax=axes[0, 1], # 画到右上角子图
)
axes[0, 1].set(title="B. 各行业毛利率", xlabel="行业", ylabel="营业毛利率") # 设置子图标题和坐标轴名称
subplot_industry_palette = dict(zip(industry_count_order, okabe_ito[:len(industry_count_order)])) # 为每个行业指定颜色
sns.scatterplot(
data=annual_2020,
x="log_总资产",
y="log_营业收入",
hue="行业简称", # 只用颜色表示行业,避免子图图例过于拥挤
palette=subplot_industry_palette, # 使用行业颜色字典
alpha=0.78, # 设置透明度
ax=axes[1, 0], # 画到左下角子图
)
axes[1, 0].set(title="C. 资产与收入(对数)", xlabel="log(总资产)", ylabel="log(营业收入)") # 设置子图标题和坐标轴名称
axes[1, 0].legend(title="行业", frameon=False, fontsize=8, title_fontsize=9) # 设置图例标题,并去掉边框
sns.barplot(data=industry_margin, y="行业简称", x="净利率", color="#009E73", ax=axes[1, 1]) # color 设置颜色,ax 指定子图位置
axes[1, 1].set(title="D. 行业平均净利率", xlabel="平均净利率", ylabel="") # 设置子图标题和坐标轴名称
for ax in axes.ravel():
sns.despine(ax=ax) # 对每个子图去掉上方和右侧边框
fig.suptitle("2020 年样本公司概览", fontsize=15) # 设置整张图标题
plt.tight_layout() # 自动调整留白,减少文字重叠12.10 补充:一张整理后的展示图
下面保留一个和本章主线不同的数据例子。它使用 seaborn 内置的 fmri 数据,展示一张经过较多整理的单图:颜色、线型、标题、图例、网格、边框和关键点标注都集中在一张图里。
这个例子不是作业要求,也不需要逐行记忆。它的作用是给你一个参照:前面学过的很多小设置,组合起来可以得到一张更像报告或论文中的图。
# 读取 fmri 内置数据,并整理成展示用折线图。
fmri = sns.load_dataset("fmri")
fmri_plot = fmri.copy()
fmri_plot["事件类型"] = fmri_plot["event"].map({"stim": "刺激", "cue": "提示"})
fig, ax = plt.subplots(figsize=(8, 4.8)) # 设置图形宽高
sns.lineplot(
data=fmri_plot,
x="timepoint",
y="signal",
hue="事件类型", # 用颜色区分事件类型
style="事件类型", # 用线型和点形状区分事件类型
markers=True, # 在线上显示点标记
dashes=False, # 不使用虚线,避免和点标记同时过于复杂
errorbar=("ci", 95), # 显示 95% 置信区间
linewidth=2.2, # 设置线条粗细
palette={"刺激": "#0072B2", "提示": "#D55E00"}, # 指定两条线的颜色
ax=ax, # 画到前面创建的坐标轴
)
stim_mean = (
fmri_plot.loc[fmri_plot["事件类型"] == "刺激"]
.groupby("timepoint")["signal"]
.mean()
)
peak_timepoint = stim_mean.idxmax()
peak_signal = stim_mean.max()
ax.annotate(
f"峰值:{peak_signal:.2f}",
xy=(peak_timepoint, peak_signal), # 箭头指向的位置
xytext=(30, -8), # 文字相对箭头位置的偏移
textcoords="offset points", # 表示 xytext 使用偏移量
ha="left", # 文字水平对齐方式
va="center", # 文字垂直对齐方式
arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 1.2}, # 设置箭头样式
)
ax.set( # 设置标题和坐标轴名称
title="刺激与提示事件下的信号变化",
xlabel="时间点",
ylabel="信号强度",
)
ax.set_xticks(range(0, 19, 3)) # 使用整数时间点作为横轴刻度
ax.grid(axis="y", linestyle="--", alpha=0.22) # 只添加横向辅助线
ax.legend(title="事件类型", frameon=False, loc="upper right", fontsize=10, title_fontsize=10) # 设置图例标题、位置,并去掉边框
sns.despine() # 去掉上方和右侧边框
plt.tight_layout() # 自动调整留白,减少文字重叠12.11 练习
每题都要求展示图形;如果题目要求生成中间表,也要先看一眼中间表。
12.11.1 分布图练习
练习 1.1:画出 2020 年公司 净利率 的直方图。
练习 1.2:画出 2020 年 医药 和 汽车 两个行业 营业毛利率 的重叠直方图,并加入 kde=True。
12.11.2 类别比较图练习
练习 2.1:画出 2020 年样本公司所属省份数量最多的前 10 个省份的计数图。
练习 2.2:画出 2020 年不同 行业简称 的平均 资产负债率 柱状图,行业按平均资产负债率从高到低排列。
12.11.3 散点图练习
练习 3.1:画出 2020 年 总资产_亿元 与 营业收入_亿元 的原始散点图。
练习 3.2:生成 log_总资产 和 log_营业收入,画出这两个对数变量的散点图。
12.11.4 折线图练习
练习 4.1:按年份计算样本公司的平均 营业毛利率,展示汇总表,并画折线图。
练习 4.2:按 负债水平 和年份计算平均 净利率,展示汇总表前几行,并画带颜色分组的折线图。
12.11.5 热力图练习
练习 5.1:制作“行业简称 × 年份”的平均 资产负债率 矩阵,并画热力图。
练习 5.2:制作“行业简称 × 年份”的平均 营业毛利率 矩阵,并画带数字标注的热力图。
12.11.6 图形整理练习
练习 6.1:整理一张 资产负债率 与 营业毛利率 的散点图,要求包含标题、坐标轴名称和合适的图例。
练习 6.2:使用 plt.subplots() 组合两张图:左边画 营业毛利率 直方图,右边画 资产负债率 箱形图。