'Labeling boxplot in seaborn with median value
How can I label each boxplot in a seaborn plot with the median value?
E.g.
import seaborn as sns
sns.set_style("whitegrid")
tips = sns.load_dataset("tips")
ax = sns.boxplot(x="day", y="total_bill", data=tips)
How do I label each boxplot with the median or average value?
Solution 1:[1]
I love when people include sample datasets!
import seaborn as sns
sns.set_style("whitegrid")
tips = sns.load_dataset("tips")
box_plot = sns.boxplot(x="day",y="total_bill",data=tips)
medians = tips.groupby(['day'])['total_bill'].median()
vertical_offset = tips['total_bill'].median() * 0.05 # offset from median for display
for xtick in box_plot.get_xticks():
box_plot.text(xtick,medians[xtick] + vertical_offset,medians[xtick],
horizontalalignment='center',size='x-small',color='w',weight='semibold')
Solution 2:[2]
This can also be achieved by deriving median from the plot itself without exclusively computing median from data
box_plot = sns.boxplot(x="day", y="total_bill", data=tips)
ax = box_plot.axes
lines = ax.get_lines()
categories = ax.get_xticks()
for cat in categories:
# every 4th line at the interval of 6 is median line
# 0 -> p25 1 -> p75 2 -> lower whisker 3 -> upper whisker 4 -> p50 5 -> upper extreme value
y = round(lines[4+cat*6].get_ydata()[0],1)
ax.text(
cat,
y,
f'{y}',
ha='center',
va='center',
fontweight='bold',
size=10,
color='white',
bbox=dict(facecolor='#445A64'))
box_plot.figure.tight_layout()
Solution 3:[3]
Based on ShikjarDua's approach, I created a version which works independent of tick positions. This comes in handy when dealing with grouped data in seaborn (i.e. hue=parameter). Additionally, I added a "flier-detection", which changes the lines per drawn box.
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
def add_median_labels(ax, fmt='.1f'):
lines = ax.get_lines()
boxes = [c for c in ax.get_children() if type(c).__name__ == 'PathPatch']
lines_per_box = int(len(lines) / len(boxes))
for median in lines[4:len(lines):lines_per_box]:
x, y = (data.mean() for data in median.get_data())
# choose value depending on horizontal or vertical plot orientation
value = x if (median.get_xdata()[1] - median.get_xdata()[0]) == 0 else y
text = ax.text(x, y, f'{value:{fmt}}', ha='center', va='center',
fontweight='bold', color='white')
# create median-colored border around white text for contrast
text.set_path_effects([
path_effects.Stroke(linewidth=3, foreground=median.get_color()),
path_effects.Normal(),
])
sns.set_style("darkgrid")
tips = sns.load_dataset("tips")
# simple example
ax = sns.boxplot(data=tips, x='day', y='total_bill', hue="sex")
add_median_labels(ax)
plt.show()
# all possible orientation and flier combinations
fig, axes = plt.subplots(2, 2, figsize=(10, 10))
for i_fly, show_fliers in enumerate([True, False]):
for i_data, data_kwargs in enumerate([{'x': 'day', 'y': 'total_bill'},
{'y': 'day', 'x': 'total_bill'}]):
ax = sns.boxplot(ax=axes[i_fly, i_data], **data_kwargs, data=tips,
showfliers=show_fliers, hue="sex")
add_median_labels(ax)
ax.set_title((
f"{['Fliers', 'No fliers'][i_fly]}, "
f"{['vertical', 'horizontal'][i_data]}"))
plt.show()
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 |