import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.lines import Line2D
# 默认字体
plt.rcParams.update({"font.family": "Fira Sans Compressed"})
以下数据如果有需要的同学可关注公众号HsuHeinrich,回复【数据可视化】自动获取~
# 导入数据
animal_rescues = pd.read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-06-29/animal_rescues.csv")
# animal_group_parent大写处理
animal_rescues["animal_group_parent"] = animal_rescues["animal_group_parent"].str.capitalize()
animal_rescues.head()

image-20240129181733785
animal_group_parent:动物种群 cal_year:救助年份 borough_code:行政区码
gb_london_boroughs_grid = pd.read_csv("https://raw.githubusercontent.com/hafen/grid-designer/master/grids/gb_london_boroughs_grid.csv")
borough_names = gb_london_boroughs_grid.rename(columns={"code_ons": "borough_code"})
borough_names.head()

image-20240129181804267
borough_code:行政区代码 name:行政区名称
# 数据预处理
# 保留2021年之前的数据
rescues_borough = animal_rescues.query("cal_year < 2021").reset_index()
# 区分猫与非猫
rescues_borough["animal_group_parent"] = np.where(
rescues_borough["animal_group_parent"] == "Cat", "Cat", "Not_Cat"
)
# 汇总统计
rescues_borough = (
rescues_borough.groupby(["cal_year", "borough_code", "animal_group_parent"])
.size()
.to_frame("n")
.reset_index()
)
# 数据透视
rescues_borough = rescues_borough.pivot(
index=["cal_year", "borough_code"],
columns="animal_group_parent",
values="n"
).reset_index()
# 匹配borough_names
rescues_borough = pd.merge(rescues_borough, borough_names, how="left", on="borough_code")
rescues_borough = rescues_borough.dropna(subset=["name"])
# 将行政区的row/col各减1,因为python是按照0开始索引的
rescues_borough["row"] -= 1
rescues_borough["col"] -= 1
# 创建行政区的位置信息
df_idxs = rescues_borough[["row", "col", "name"]].drop_duplicates()
NAMES = df_idxs["name"].values
ROWS = df_idxs["row"].values.astype(int)
COLS = df_idxs["col"].values.astype(int)
# 自定义基础变量
BLUE = "#3D85F7"
BLUE_LIGHT = "#5490FF"
PINK = "#C32E5A"
PINK_LIGHT = "#D34068"
GREY40 = "#666666"
GREY25 = "#404040"
GREY20 = "#333333"
BACKGROUND = "#F5F4EF"
# 绘制一张子图看看效果
# 初始布局
fig, ax = plt.subplots(figsize=(8, 5))
# 选择一个行政区
df = rescues_borough[rescues_borough["name"] == "Enfield"]
# YEAR 为x轴
YEAR = df["cal_year"].values
# 猫的救援数量、非猫的救援数量作为y轴
CAT = df["Cat"].values
NOT_CAT = df["Not_Cat"].values
# 绘制折线图
ax.plot(YEAR, CAT, color=BLUE)
ax.plot(YEAR, NOT_CAT, color=PINK)
# 区域填充
# 当CAT > NOT_CAT时填充BLUE_LIGHT
ax.fill_between(
YEAR, CAT, NOT_CAT, where=(CAT > NOT_CAT),
interpolate=True, color=BLUE_LIGHT, alpha=0.3
)
# 当CAT <= NOT_CAT时填充PINK_LIGHT
ax.fill_between(
YEAR, CAT, NOT_CAT, where=(CAT <= NOT_CAT),
interpolate=True, color=PINK_LIGHT, alpha=0.3
);

# 将上述的子图过程封装成函数
def single_plot(x, y1, y2, name, ax):
'''
x: year数组
y1: 猫的救援数量数组
y2: 非猫的救援数量数组
name: 行政区名称
ax: 子图所在位置
'''
ax.plot(x, y1, color=BLUE)
ax.plot(x, y2, color=PINK)
ax.fill_between(
x, y1, y2, where=(y1 > y2),
interpolate=True, color=BLUE_LIGHT, alpha=0.3
)
ax.fill_between(
x, y1, y2, where=(y1 <= y2),
interpolate=True, color=PINK_LIGHT, alpha=0.3
);
ax.set_facecolor(BACKGROUND)
fig.set_facecolor(BACKGROUND)
xticks = [2010, 2015, 2020]
ax.set_xticks(xticks)
ax.set_xticks([2012.5, 2017.5], minor=True)
# added a 'size' argument
ax.set_xticklabels(xticks, color=GREY40, size=10)
yticks = [0, 10, 20]
ax.set_yticks(yticks)
ax.set_yticks([5, 15, 25], minor=True)
ax.set_yticklabels(yticks, color=GREY40, size=10)
ax.set_ylim((-1, 26))
ax.grid(which="minor", lw=0.4, alpha=0.4)
ax.grid(which="major", lw=0.8, alpha=0.4)
ax.yaxis.set_tick_params(which="both", length=0)
ax.xaxis.set_tick_params(which="both", length=0)
ax.spines["left"].set_color("none")
ax.spines["bottom"].set_color("none")
ax.spines["right"].set_color("none")
ax.spines["top"].set_color("none")
ax.set_title(name, weight="bold", size=9, color=GREY20)
# 自定义变量
NROW = len(rescues_borough["row"].unique())
NCOL = len(rescues_borough["col"].unique())
# 绘制多子图
fig, axes = plt.subplots(NROW, NCOL, figsize=(12, 10), sharex=True, sharey=True)
for i, name in enumerate(NAMES):
# 选择指定name
df = rescues_borough[rescues_borough["name"] == name]
# 获取坐标轴
ax = axes[ROWS[i], COLS[i]]
# 获取对应的x、y1、y2
YEAR = df["cal_year"].values
CAT = df["Cat"].values
NOT_CAT = df["Not_Cat"].values
# 调用函数绘制
single_plot(YEAR, CAT, NOT_CAT, name, ax)

# 移除空面板的子图
for i in range(7):
for j in range(8):
# 子图包含线条(即含有数据)
if axes[i, j].lines:
continue
# 否则删除该子图
else:
axes[i, j].remove()
fig

# 自定义刻度标签
# 每一行的第一个非空子图显示y轴标签
for i in range(7):
first_in_row = True
for j in range(8):
if first_in_row and axes[i, j].lines:
axes[i, j].yaxis.set_tick_params(labelleft=True)
first_in_row = False
# 每一列的最后一个非空子图显示x轴标签
for j in range(8):
first_in_col = True
for i in reversed(range(7)): # note the 'reversed()'
if first_in_col and axes[i, j].lines:
axes[i, j].xaxis.set_tick_params(labelbottom=True)
first_in_col = False
fig

# 创建图例样式
handles = [
Line2D([], [], c=color, lw=1.2, label=label)
for label, color in zip(["cats", "other"], [BLUE, PINK])
]
# 将图例添加至fig中
fig.legend(
handles=handles,
loc=(0.75, 0.94),
ncol=2, # 图例为两列,即1行*2列展示
columnspacing=1, # 图例的列间距
handlelength=1.2, # 险段长度
frameon=False # 无框
)
# 创建填充式图例样式
cats = patches.Patch(facecolor=BLUE_LIGHT, alpha=0.3, label="more cats")
other = patches.Patch(facecolor=PINK_LIGHT, alpha=0.3, label="more other")
fig.legend(
handles=[cats, other],
loc=(0.75, 0.9),
ncol=2,
columnspacing=1,
handlelength=2,
handleheight=2,
frameon=False,
)
# 标题
fig.text(
x=0.05, y=0.975, s="Rescues of\ncats vs other animals by\nthe London fire brigade\n2009-2020",
color=GREY25, fontsize=26, fontfamily="KyivType Sans", fontweight="bold",
ha="left",
va="top",
ma="left" # 多行左对齐
)
# 著作信息
fig.text(
x=0.95, y=0.025, s="Source: London.gov · Graphic: Georgios Karamanis",
fontsize=11,
ha="right",
va="baseline"
)
# 自定义子图空间边距
fig.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95, hspace=0.3, wspace=0.08)
fig

参考:Time series with filled area and custom facetting in Matplotlib[1]
共勉~
[1]
Time series with filled area and custom facetting in Matplotlib: https://python-graph-gallery.com/web-time-series-and-facetting-with-matplotlib/