文章

Seaborn 绘制带有坐标轴截断的条形图

Seaborn 绘制带有坐标轴截断的条形图

知乎有个教程通过使用 matplotlib+brokenaxes 能够实现对坐标轴截断图的绘制教程,效果不行…

为什么需要绘制带有坐标轴截断的条形图呢,是因为条形图里面有些数据异常的高(虽然数据本身是正确的),这样就会导致其它数值较小的数据不能够完全展示出来。那么我为什么使用 Seaborn 而不是直接用 matplotlib? 主要还是因为 matplotlibSeaborn 好看 🤔

效果

本文记录了如何在 seaborn 下实现坐标轴截断条形图的绘制,先看下效果:

alt text

效果看起来还可以吧,本来想实现 Tantivy Readme 里面的条形图风格,但是并没有想到什么好的方案(P 图什么的可不行,一点儿都不专业😼)

alt text

代码实现

数据预处理

这是需要进行可视化的 data.csv 文件,首先对它进行数据预处理,加载 DataFrame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

df = pd.read_csv('data.csv')
# CSV 文件中的 None 不应该被处理为空值
df['index_type'] = df['index_type'].fillna('None')

df_filter = df[(df['optimize'] == 1) & ((df['bench'] == 'hasToken') | (df['bench'] == 'TextSearch'))]
index_types = df_filter['index_type'].unique()
terms_counts = df_filter['terms_count'].unique()
# plot_data 会被用来可视化
plot_data = pd.DataFrame(columns=['terms_count', 'index_type', 'bench', 'qps'])
for terms_count in terms_counts:
    df_terms_count = df_filter[df_filter['terms_count'] == terms_count]
    for index_type in index_types:
        df_index_type = df_terms_count[df_terms_count['index_type'] == index_type]
        for bench_type in df_index_type['bench'].unique():
            df_bench_type = df_index_type[df_index_type['bench'] == bench_type]
            qps = df_bench_type['qps'].values[0] if len(df_bench_type) > 0 else 0
            plot_data = pd.concat(
                [plot_data, pd.DataFrame(
                    {
                        'terms_count': str(terms_count),
                        'index_type': index_type + " (hasToken)" if bench_type == 'hasToken' else index_type + ' (TextSearch)',
                        'bench': bench_type,
                        'qps': qps
                    }, index=[0])],
                ignore_index=True
            )

绘制两个子图模拟坐标轴截断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
sns.set_style(style="darkgrid")

fig = plt.figure(figsize=(14, 6))  # 调整尺寸以适应标题和图例
# 设置每个子图的宽度比例, 并且设置子图之间的间距为 0.01
gs = gridspec.GridSpec(1, 2, width_ratios=[17, 10], wspace=0.01)
# 生成两个子图
ax1 = fig.add_subplot(gs[0])
ax2 = fig.add_subplot(gs[1])

# 绘制第一个子图
sns.barplot(x='qps', y='terms_count', hue='index_type', data=plot_data, palette='viridis', gap=0.1, orient='h', ax=ax1)
ax1.set_xlim(0, 210)  # 设置x轴的显示范围 ⬇️ 截断位置
ax1.set_xlabel('QPS')
ax1.xaxis.set_label_coords(0.75, -0.08)  # 调整x轴标签的位置
ax1.set_ylabel('Term Frequency (Count)')
ax1.get_legend().remove()  # 移除子图的图例
# 给 ax1 所有的 bar 增加 label 标签
for container in ax1.containers:
    ax1.bar_label(container, label_type='edge')

# 绘制第二个子图
sns.barplot(x='qps', y='terms_count', hue='index_type', data=plot_data, palette='viridis', gap=0.1, orient='h', ax=ax2)
ax2.set_xlim(210, 1400)  # 设置x轴的显示范围 ⬇️ 截断位置
ax2.set_xlabel('')
ax2.set_ylabel('')
ax2.set_yticklabels('')
ax2.get_legend().remove()  # 移除子图的图例
# 给 ax2 所有的 bar 增加 label 标签
for container in ax2.containers:
    ax2.bar_label(container, label_type='edge')

# 投机倒把,在 bar 截断位置增加一个字符 'I',让截断位置看起来是连续的
for bar in ax1.patches:
    # 获取 bar 位置和尺寸
    x = bar.get_x()
    y = bar.get_y()
    width = bar.get_width()
    height = bar.get_height()
    if width >= 210:
        # 在条形右侧添加文本
        ax1.text(x + 210.2, y + height / 1.7, f'I', va='center')

# 设置标题
fig.suptitle('Performance Analysis by Term Frequency and Index Type', fontsize=16, y=0.99)

# 添加图例
handles, labels = ax1.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 0.95), ncol=5)
# 将图像保存为文件
plt.savefig('qps_all_index_types_seaborn.png')
plt.show()
plt.close()
本文由作者按照 CC BY 4.0 进行授权