Seaborn箱线图分组层级X轴标签实现问题求助
为Seaborn箱线图实现分组层级X轴标签
需求说明
- 给Seaborn箱线图添加多层级分组X轴标签,效果类似带分组表头的柱状图
- 实际数据分组大小不一致,不能用均匀分组逻辑
- 所有箱线图保持同一颜色,不使用Seaborn的
hue参数 - 此前尝试适配热图/柱状图的层级标签方案,但因箱线图无需聚合数据,无法正确匹配分组位置
原有问题代码
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from itertools import groupby def test_table(): data_table = pd.DataFrame({'Room':['Room A']*24 + ['Room B']*24, 'Shelf':(['Shelf 1']*12 + ['Shelf 2']*12)*2, 'Staple':['Milk','Water','Sugar','Honey','Wheat','Corn']*8, 'Quantity':np.random.randint(1, 20, 48), }) return data_table def add_line(ax, xpos, ypos): line = plt.Line2D([xpos, xpos], [ypos + .1, ypos], transform=ax.transAxes, color='black') line.set_clip_on(False) ax.add_line(line) def label_len(my_index,level): labels = my_index.get_level_values(level) return [(k, sum(1 for i in g)) for k,g in groupby(labels)] def label_group_bar_table(ax, df): ypos = -.1 scale = 1./df.index.size for level in range(df.index.nlevels)[::-1]: pos = 0 for label, rpos in label_len(df.index,level): lxpos = (pos + .5 * rpos)*scale ax.text(lxpos, ypos, label, ha='center', transform=ax.transAxes) add_line(ax, pos*scale, ypos) pos += rpos add_line(ax, pos*scale , ypos) ypos -= .1 df = test_table().groupby(['Room','Shelf','Staple']).sum() fig = plt.figure() fig = plt.figure(figsize = (15, 10)) ax = fig.add_subplot(111) sns.boxplot(x=df.Quantity, y=df.Quantity,data=df) # 移除默认标签 labels = ['' for item in ax.get_xticklabels()] ax.set_xticklabels(labels) ax.set_xlabel('') label_group_bar_table(ax, df) fig.subplots_adjust(bottom=.1*df.index.nlevels) plt.show()
问题分析
- 错误对数据执行
groupby.sum(),箱线图需要原始数据计算四分位数等统计量,聚合后的数据无法生成正确分布 - 箱线图x轴参数设置错误,导致箱体位置与分组标签不匹配
- 层级标签函数基于聚合后的索引计算位置,与箱线图实际x轴位置脱节
解决方案
核心思路
- 保留原始数据直接绘制箱线图
- 按最细粒度分类(如
Staple)绘制箱体,再根据上层分组(Room、Shelf)的范围,在x轴下方添加层级标签和分隔线 - 自定义标签绘制逻辑,适配箱线图x轴的实际位置(每个箱体对应x轴0,1,2...n-1坐标)
修正后完整代码
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from itertools import groupby def test_table(): data_table = pd.DataFrame({'Room':['Room A']*24 + ['Room B']*24, 'Shelf':(['Shelf 1']*12 + ['Shelf 2']*12)*2, 'Staple':['Milk','Water','Sugar','Honey','Wheat','Corn']*8, 'Quantity':np.random.randint(1, 20, 48), }) return data_table def add_group_labels(ax, df, group_cols): """ 为箱线图添加多层级分组X轴标签 :param ax: 绘图轴对象 :param df: 原始数据DataFrame :param group_cols: 从上层到下层的分组列名列表,如['Room', 'Shelf'] """ x_cats = df['Staple'].unique() n_cats = len(x_cats) y_pos = -0.15 # 最上层标签的相对y轴位置 line_height = 0.1 # 每层标签的间距 # 从上层到下层遍历分组 for col in group_cols: sorted_df = df.sort_values([col, 'Staple']) group_counts = [(k, len(list(g))) for k, g in groupby(sorted_df[col])] current_pos = 0 for group_name, count in group_counts: # 计算分组中心的轴坐标 group_center = current_pos + count/2 - 0.5 ax_coord_x = (group_center + 0.5) / n_cats # 添加分组标签 ax.text(ax_coord_x, y_pos, group_name, ha='center', va='top', transform=ax.transAxes) # 添加左侧分隔线 line_x = current_pos / n_cats ax.plot([line_x, line_x], [y_pos, y_pos + line_height], transform=ax.transAxes, color='black') current_pos += count # 添加最右侧分隔线 ax.plot([1, 1], [y_pos, y_pos + line_height], transform=ax.transAxes, color='black') y_pos -= line_height # 调整底部边距容纳标签 plt.subplots_adjust(bottom=0.15 + len(group_cols)*line_height) # 获取原始数据 df = test_table() # 创建绘图对象 fig, ax = plt.subplots(figsize=(15, 10)) # 绘制同色箱线图 sns.boxplot(x='Staple', y='Quantity', data=df, hue=None, ax=ax, color='#3377bb') # 隐藏x轴主标题,可选择保留/隐藏细粒度标签 ax.set_xlabel('') # ax.set_xticklabels([]) # 添加层级分组标签 add_group_labels(ax, df, ['Room', 'Shelf']) plt.show()
关键说明
- 数据处理:直接使用原始DataFrame,确保箱线图能正确计算数据分布
- 箱线图绘制:指定
x='Staple'为最细粒度分类,hue=None强制所有箱体同色 - 层级标签函数:
- 根据原始数据的分组顺序计算箱体范围
- 将箱体位置转换为轴坐标,保证标签和分隔线与分组对齐
- 动态调整底部边距,避免标签被截断
内容的提问来源于stack exchange,提问作者flashliquid
相关产品推荐
相关产品推荐

