如何创建和注释堆积比例条形图

How to create and annotate a stacked proportional bar chart

我正在努力创建从数据框中的 value_counts() 列派生的堆积条形图。

假设一个像下面这样的数据框,其中 responder 不重要,但想为所有 [=14] 堆叠 [1,2,3,4,5]count =]列。

responder, q1, q2, q3, q4, q5
------------------------------
r1, 5, 3, 2, 4, 1
r2, 3, 5, 1, 4, 2
r3, 2, 1, 3, 4, 5
r4, 1, 4, 5, 3, 2
r5, 1, 2, 5, 3, 4
r6, 2, 3, 4, 5, 1
r7, 4, 3, 2, 1, 5

看起来像,除了每个条形将被标记为 q# 并且它将包括 5 个部分,用于统计数据中 [1,2,3,4,5] 的数量:

理想情况下,所有条形都是“100%”宽,显示计数占条形的比例。但保证每个 responder 行都有一个条目,所以如果可能的话,百分比只是一个奖励。

任何帮助将不胜感激,略微偏爱 matplotlib 解决方案。

  • 来自 matplotlib 3.4.2,使用 matplotlib.pyplot.bar_label
  • pro = df.div(df.sum(axis=1), axis=0) 创建一个相对于每一行的比例数据框。请注意沿正确轴求和和除法的重要性。
  • 使用 pandas.DataFrame.plotkind='barh'stacked=True 绘制 pro 数据框,这将创建一个具有正确范围 (0 - 1) 的 x 轴。 matplotlib 是默认的绘图后端。
  • .bar_label 有一个 labels 参数,它接受自定义标签。
    • labels 是使用列表推导式创建的,其中 df 的值 (vals) 与每个条形图块的 per 的值相结合。
    • (w := v.get_width()) > 0 可用于有条件地显示注释,在这种情况下大于 0。 := 是一个赋值表达式,可从 python 3.8 获得。
      • labels = [f'{val}\n({w.get_width()*100:.1f}%)' for w, val in zip(c, vals)]如果不需要检查补丁大小可以使用
      • labels = [f'{val}\n({w.get_width()*100:.1f}%)' if w.get_width() > 0 else '' for w, val in zip(c, vals)] 可以在没有 := 的情况下使用,但需要使用 .get_width() 两次。
  • 测试于 python 3.10pandas 1.3.5matplotlib 3.5.1seaborn 0.11.2
import pandas as pd

# sample dataframe from OP
data = {'responder': ['r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7'], 'q1': [5, 3, 2, 1, 1, 2, 4], 'q2': [3, 5, 1, 4, 2, 3, 3], 'q3': [2, 1, 3, 5, 5, 4, 2], 'q4': [4, 4, 4, 3, 3, 5, 1], 'q5': [1, 2, 5, 2, 4, 1, 5]}

# The labels to be on the y-axis should be set as the index
# If the column names and index need to be swapped, use .T to transpose the dataframe
df = pd.DataFrame(data).set_index('responder')

# create dataframe with proportions
pro = df.div(df.sum(axis=1), axis=0)

# plot
ax = pro.plot(kind='barh', figsize=(12, 10), stacked=True)

# move legend
ax.legend(bbox_to_anchor=(1, 1.01), loc='upper left')

# column names from per used to get the column values from df
cols = pro.columns

# iterate through each group of containers and the corresponding column name
for c, col in zip(ax.containers, cols):
    
    # get the values for the column from df
    vals = df[col]

    # create a custom label for bar_label
    labels = [f'{val}\n({w*100:.1f}%)' if (w := v.get_width()) > 0 else '' for v, val in zip(c, vals)]
    
    # annotate each section with the custom labels
    ax.bar_label(c, labels=labels, label_type='center', fontweight='bold')

  • 转置 dfdf = pd.DataFrame(data).set_index('responder').T,交换索引和列,生成以下图。 figsize=(12, 10)可能需要调整。

数据帧

  • df
           q1  q2  q3  q4  q5
responder                    
r1          5   3   2   4   1
r2          3   5   1   4   2
r3          2   1   3   4   5
r4          1   4   5   3   2
r5          1   2   5   3   4
r6          2   3   4   5   1
r7          4   3   2   1   5
  • per
                 q1        q2        q3        q4        q5
responder                                                  
r1         0.333333  0.200000  0.133333  0.266667  0.066667
r2         0.200000  0.333333  0.066667  0.266667  0.133333
r3         0.133333  0.066667  0.200000  0.266667  0.333333
r4         0.066667  0.266667  0.333333  0.200000  0.133333
r5         0.066667  0.133333  0.333333  0.200000  0.266667
r6         0.133333  0.200000  0.266667  0.333333  0.066667
r7         0.266667  0.200000  0.133333  0.066667  0.333333

引用

  • How to put the legend out of the plot 显示格式化和移动图例的各种方法。
  • Adding value labels on a matplotlib bar chart.bar_label进行了详细的解释。
  • stack bar plot in matplotlib and add label to each section
  • How to annotate barplot with percent by hue/legend group

您可以使用百分比计算条形图的高度,并使用 ax = percents.T.plot(kind='barh', stacked=True) 获得堆叠条形图,其中 percents 是一个 DataFrame,其中 q1,...q5 作为列,1,...,5 作为指数。

>>> percents
         q1        q2        q3        q4        q5
1  0.196873  0.199316  0.206644  0.194919  0.202247
2  0.205357  0.188988  0.205357  0.205357  0.194940
3  0.202265  0.217705  0.184766  0.196089  0.199177
4  0.199494  0.199494  0.190886  0.198481  0.211646
5  0.196137  0.195146  0.211491  0.205052  0.192174

然后您可以使用 ax.patches 为每个柱添加标签。可以从原始计数 DataFrame 生成标签:counts = df.apply(lambda x: x.value_counts())

>>> counts
    q1   q2   q3   q4   q5
1  403  408  423  399  414
2  414  381  414  414  393
3  393  423  359  381  387
4  394  394  377  392  418
5  396  394  427  414  388

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

## create some data similar to yours
np.random.seed(42)
categories = ['q1','q2','q3','q4','q5']
df = pd.DataFrame(np.random.randint(1,6,size=(2000, 5)), columns=categories)

## counts will be used for the labels
counts = df.apply(lambda x: x.value_counts())

## percents will be used to determine the height of each bar
percents = counts.div(counts.sum(axis=1), axis=0)

counts_array = counts.values
nrows, ncols = counts_array.shape
indices = [(i,j) for i in range(0,nrows) for j in range(0,ncols)]
percents_array = percents.values

ax = percents.T.plot(kind='barh', stacked=True)
ax.legend(bbox_to_anchor=(1, 1.01), loc='upper right')
for i, p in enumerate(ax.patches):
    ax.annotate(f"({p.get_width():.2f}%)", (p.get_x() + p.get_width() - 0.15, p.get_y() - 0.10), xytext=(5, 10), textcoords='offset points')
    ax.annotate(str(counts_array[indices[i]]), (p.get_x() + p.get_width() - 0.15, p.get_y() + 0.10), xytext=(5, 10), textcoords='offset points')
plt.show()