Disable hue nesting in Seaborn

Viewed 806

When plotting a bar chart with Seaborn and using the hue parameter to color the bars according to their column value, bars with identical column values are nested, or aggregated, and only a single bar is shown. The image below illustrates the problem. Patient number 1 has two samples of sample_type 1, with values 10 and 20. The two values have been nested, and both values are represented as a single bar (as the average of the two). Example of what I'd like to avoid I'd like to avoid this nesting, and rather have something like in the image below. Example of what I'd like to achieve Is this possible to achieve? MVE below. Thanks!

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.DataFrame({
    "patient_number": [1, 1, 1, 2, 2, 2],
    "sample_type": [1, 1, 2, 1, 2, 3],
    "value": [10, 20, 15, 10, 11, 12]
})

sns.barplot(x="patient_number", y="value", hue="sample_type", data=df)
plt.show()
1 Answers

The following approach obtains the desired plot:

  • Seaborn's hue= parameter both defines the color and the position of the bars.
  • Per patient, an extra field ('idx') contains a unique number for each of the desired bars. This field 'idx' restarts from 0 for every next patient and is added to the dataframe.
  • 'idx' can then be used as hue='idx' to get the desired columns, although they will be colored just sequently.
  • In order to get one color per sample type, an extra column now contains a factorized version of the sample types (so, 0 for the first type, 1 for the next, etc.)
  • Seaborn generates the bars per hue, one for each patient. These bars can be accessed as a list via ax.patches If some patient doesn't have a value for a given 'idx', a dummy bar is will be added to the list.
  • By iterating through the patients and then through the 'idx', all bars can be visited and colored via 'sample_type'. As the ordering of the bars is a bit tricky, an adequate renumbering is needed.
  • The legend needs to be changed to reflect the sample types.

The given data is extended a bit to be able to test different numbers of samples per patient, and sample types that aren't simple subsequent numbers.

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.DataFrame({
    'patient_number': [1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3],
    'sample_type': ['st1', 'st1', 'st2', 'st1', 'st2', 'st3', 'st4', 'st4', 'st4', 'st4', 'st4'],
    'value': [10, 20, 15, 10, 11, 12, 1, 2, 3, 4, 5]
})
df['idx'] = df.groupby('patient_number').cumcount()
df['sample_factors'], sample_labels = pd.factorize(df['sample_type'])

ax = sns.barplot(x='patient_number', y='value', hue='idx', data=df)

colors = plt.cm.get_cmap('Set2').colors  # https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html
handles = [None for _ in sample_labels]
num_patients = len(ax.patches) // (df['idx'].max() + 1)
for i, (patient_id, group) in enumerate(df.groupby('patient_number')):
    for j, factor in enumerate(group['sample_factors']):
        patch = ax.patches[i + j * num_patients]
        patch.set_color(colors[factor])
        handles[factor] = patch

ax.legend(handles=handles, labels=list(sample_labels), title='Sample type')

plt.show()

resulting plot

Related