pandasmatplotlibseaborncountplot

How to separate countplot visualization with three features


I am trying to generate a frequency plot at different time frames with each time frame consists of four conditions and two possible outcomes, High or low. I can use hue to separate the four conditions but I also want to separate each condition with outcomes. Here's my data-

import pandas as pd
import seaborn as sns
exp = {'Time':['2hrs', '2hrs', '2hrs','2hrs','2hrs', '2hrs', '2hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs' ],
       'Condition':['A+','A-','B+','B-','B+','B-','B+','A+','A-','B+','B-','A+','A-','A+','A-',],
       'Outcome': ['High', 'Low','High','High', 'Low','High', 'Low','High','High','High','Low', 'Low','Low','Low', 'High']}
df = pd.DataFrame(data=exp)
df.head()
hue_order = ['A+', 'A-', 'B+', 'B-']
ax = sns.countplot(data=df, x='Time' , hue='Condition', hue_order=hue_order, palette='Set1')
plt.legend(title='', loc='upper left', bbox_to_anchor=(1,1))

What I got is-

enter image description here

But what I want is to further split each column proportional to how many "High" or "Low" are there for that specific condition and time frame. For example, at 4hrs there are three A+ of which one is with "High" outcome and remaining two are with "Low" outcomes. So, I want to shade 1/3 with dark red and 2/3 with light red.

Not sure if I described the problem clearly.


Solution

  • If you need the High/Low to be shown as stacked along with the currently grouped bars, it is not available directly in seaborn. Once you plot the above bar, you will need to add the HIGHs (or LOWs) to the bar chart to show this. Steps will include:

    1. Build the chart as you did
    2. Get the values of HIGHs for each bar, adding zero's where no HIGHs are present
    3. For each bar in the plotted chart, add a rectangle patch on top with the same x,y, width, but heights for HIGH only and color with a darker shade. Note that I am using ax.containers to get the bars
    4. I have divided the RGB values by 1.5 to get a slightly darker shade... higher the denominator, darker the shade. Note that I have left A (opacity) as 1 by using color[0:3] only
    5. Add the patch.

    CODE:

    import pandas as pd
    import seaborn as sns
    from matplotlib.patches import Rectangle ## Added NEW
    exp = {'Time':['2hrs', '2hrs', '2hrs','2hrs','2hrs', '2hrs', '2hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs','4hrs' ],
           'Condition':['A+','A-','B+','B-','B+','B-','B+','A+','A-','B+','B-','A+','A-','A+','A-',],
           'Outcome': ['High', 'Low','High','High', 'Low','High', 'Low','High','High','High','Low', 'Low','Low','Low', 'High']}
    df = pd.DataFrame(data=exp)
    hue_order = ['A+', 'A-', 'B+', 'B-']
    ax = sns.countplot(data=df, x='Time' , hue='Condition', hue_order=hue_order, palette='Set1')
    
    ## Added code starts here
    ## Get the HIGH values for each bar, adding 0 if no HIGHS
    high_heights=df[df.Outcome=='High'].groupby(['Condition', 'Time']).count().unstack(fill_value=0).stack().reset_index().Outcome 
    i=0
    ## For each container, for each bar within this container
    for bars in ax.containers:
        for bar in bars:
            ## Use same bar x,y,width, but different heights and darker facecolor
            rect=Rectangle(bar.get_xy(), bar.get_width(), high_heights[i], color=tuple(t/1.5 for t in bar.get_facecolor()[0:3]))
            ax.add_patch(rect) ## Add to the plot
            i=i+1
    
    plt.legend(title='', loc='upper left', bbox_to_anchor=(1,1))
    

    This is the output...

    enter image description here

    I also wanted to show you another option here... rather than shades, you can use hatches, which will show something like this. Most steps are the same, expect that when you are drawing the new rectangle, plot the same x,y, color, but different height and add the hatch. More options for hatches are available here

    Just update the rectangle creation line as such, rest is as above...

    rect=Rectangle(bar.get_xy(), bar.get_width(), high_heights[i], hatch='oo', facecolor=bar.get_facecolor())
    

    enter image description here