如何在分组柱状图中同时添加百分比和N值作为柱形标签
分组柱状图标签同时显示百分比和N值的问题排查与解决
问题背景
此前在Stack Overflow提问《Include both % and N as bar labels》并获得解答,尝试将该方法应用于分组柱状图时未得到正确结果,原代码如下:
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from matplotlib.ticker import PercentFormatter data = { 'id': [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], 'baseline': [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1], 'endline': [1, 0, np.nan, 1, 0, 0, 1, np.nan, 1, 0, 0, 1, 0, 0, 1, 0, np.nan, np.nan, 1, 0, 1, np.nan, 0, 1, 0, 1, 0, np.nan, 1, 0, np.nan, 0, 0, 0, np.nan, 1, np.nan, 1, np.nan, 0, np.nan, 1, 1, 0, 1, 1, 1, 0, 1, 1], 'gender': ['male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female'] } df = pd.DataFrame(data) sns.set_style('white') ax = sns.barplot(data = df.melt(id_vars = ['id', 'gender'], value_vars = ['baseline', 'endline']), x = 'variable', y = 'value', estimator=lambda x: np.sum(x) / np.size(x) * 100, ci=None, color='cornflowerblue', hue = 'gender') N = df.melt(id_vars = ['id', 'gender'], value_vars = ['baseline', 'endline']).groupby(['gender', 'variable'], sort=False)['value'].count().to_numpy() N_it = '$\it{N}$' labels=[f'{np.round(perc,1)}% ({N_it} = {n})' for perc, n in zip(ax.containers[0].datavalues, N)] ax.bar_label(ax.containers[0], labels = labels, fontsize = 10) ax.bar_label(ax.containers[1], labels = labels, fontsize = 10) sns.despine(ax = ax, left = True) ax.grid(True, axis = 'y') ax.yaxis.set_major_formatter(PercentFormatter(100)) ax.set_xlabel('') ax.set_ylabel('') plt.tight_layout() plt.show()
问题分析
- 标签复用错误:两组柱子(male和female)共用同一组
labels,导致标签内容完全重复,未对应各自分组的统计值。 - 百分比计算逻辑错误:原
estimator用np.size(x)作为分母,包含了NaN值,导致百分比偏离实际有效样本占比。 - 标签数据不匹配:仅用
ax.containers[0].datavalues生成标签,未对应第二组容器的实际百分比数据。
修正方案
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from matplotlib.ticker import PercentFormatter data = { 'id': [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], 'baseline': [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1], 'endline': [1, 0, np.nan, 1, 0, 0, 1, np.nan, 1, 0, 0, 1, 0, 0, 1, 0, np.nan, np.nan, 1, 0, 1, np.nan, 0, 1, 0, 1, 0, np.nan, 1, 0, np.nan, 0, 0, 0, np.nan, 1, np.nan, 1, np.nan, 0, np.nan, 1, 1, 0, 1, 1, 1, 0, 1, 1], 'gender': ['male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'male', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female', 'female'] } df = pd.DataFrame(data) # 预处理数据为长格式 melted_df = df.melt(id_vars=['id', 'gender'], value_vars=['baseline', 'endline']) sns.set_style('white') ax = sns.barplot( data=melted_df, x='variable', y='value', # 修正百分比计算:基于非NaN样本统计 estimator=lambda x: np.nansum(x) / np.count_nonzero(~np.isnan(x)) * 100, ci=None, hue='gender', palette='cornflowerblue' ) # 按分组统计有效样本数(排除NaN) grouped_counts = melted_df.groupby(['gender', 'variable'], sort=False)['value'].count() # 拆分N值为对应两组柱子的数组 N_male = grouped_counts.loc['male'].to_numpy() N_female = grouped_counts.loc['female'].to_numpy() N_it = '$\it{N}$' # 为每组柱子单独生成标签 labels_male = [f'{np.round(perc,1)}% ({N_it} = {n})' for perc, n in zip(ax.containers[0].datavalues, N_male)] labels_female = [f'{np.round(perc,1)}% ({N_it} = {n})' for perc, n in zip(ax.containers[1].datavalues, N_female)] # 为两组柱子分别添加对应标签 ax.bar_label(ax.containers[0], labels=labels_male, fontsize=10) ax.bar_label(ax.containers[1], labels=labels_female, fontsize=10) sns.despine(ax=ax, left=True) ax.grid(True, axis='y') ax.yaxis.set_major_formatter(PercentFormatter(100)) ax.set_xlabel('') ax.set_ylabel('') plt.tight_layout() plt.show()
关键修改点
- 修正百分比计算:用
np.nansum(x)求和,np.count_nonzero(~np.isnan(x))统计有效样本数,确保百分比基于实际非缺失值计算。 - 拆分N值数组:将分组统计的N值拆分为对应male和female的两个数组,保证标签与分组匹配。
- 分别生成标签:为每个容器单独生成对应标签,避免复用导致的内容错误。
内容的提问来源于stack exchange,提问作者Stephen Okiya
相关产品推荐
相关产品推荐

