如何创建和注释堆积比例条形图
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.plot
与 kind='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.10
、pandas 1.3.5
、matplotlib 3.5.1
、seaborn 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')
- 转置
df
与 df = 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()
我正在努力创建从数据框中的 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.plot
与kind='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.10
、pandas 1.3.5
、matplotlib 3.5.1
、seaborn 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')
- 转置
df
与df = 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()