Search code examples
pythonmatplotlibseaborn

How to construct seaborn barplot when groups of bars do not have the same number of hues?


I have a seaborn barplot. There are two groups (two x-axis ticks). The first group has 2 hues/sub-groups/conditions. The second group has 3 hues.

When constructing the barplot, seaborn assumes that there are 3 hues for each group. Thus, it leaves an empty gap where the "third bar" of the first group should be, even though it does not exist.

How do I change the barplot so that:

  1. There is no "third bar" gap
  2. The width of all bars is the same

By the way, in the left subplot, group2 should have a third bar but is left as "n/a" for unrelated reasons.

Current barplot

chatGPT gave me various manual plotting solutions, but ultimately they didn't work. I'm sure if I tried hard enough the manual plotting could work.


Solution

  • Here's one way to do it that requires manipulating the Rectangle patch positions made by the barplot command:

    import seaborn as sns
    import pandas as pd
    
    from matplotlib.patches import Rectangle
    
    # some data
    data = {
        "group": [1, 1, 2, 2, 2],
        "measure": [0.7, 0.75, 0.78, 0.8, 1.3],
        "hue": [1, 2, 1, 2, 3]
    }
    
    df = pd.DataFrame(data)
    
    fig = sns.barplot(data=df, x="group", y="measure", hue="hue")
    
    # get xy positions of rectangles in bar
    rects = {}
    for p in fig.get_children():
        if isinstance(p, Rectangle):
            if p.get_height() in data["measure"]:
                # rectangle is one of the bars
                xy = p.get_xy()
                rects[xy[0]] = p
    
    # number of values in each group
    num_in_groups = df.groupby("group").count()["measure"].values
    
    x0 = min(rects.keys())  # x-position of first bar
    pad = 0.02  # some padding between groups
    
    # sort bars on x-position
    rects = sorted(rects.items())
    width = rects[0][1].get_width()  # bar widths
    
    tickpos = [
        x0 + (width * num_in_groups[0]) / 2,
        x0 + pad + (width * num_in_groups[0]) + (width * num_in_groups[1]) / 2
    ]
    
    i = 0
    for _, rect in rects:
        rect.set(x=x0)
        x0 += width
        i += 1
        if i == num_in_groups[0]:
            # add padding between groups
            x0 += pad
    
    # reset the group labels
    fig.set_xticks(tickpos, fig.get_xticklabels())
    
    # reset the x-axis limits
    edgepad = 0.05
    fig.set_xlim([rects[0][0] - edgepad, rects[0][0] + width * sum(num_in_groups) + pad + edgepad])
    

    enter image description here