It is possible to combine axes-level plot functions by simply calling them successively:
import seaborn as sns
import matplotlib.pyplot as plt
tips = sns.load_dataset("tips")
sns.set_theme(style="whitegrid")
ax = sns.boxplot(x="day", y="total_bill", data=tips)
ax = sns.stripplot(x="day", y="total_bill", data=tips,
color=".25", alpha=0.7, ax=ax)
plt.show()
How to achieve this for the figure-level function sns.catplot()? Successive calls to sns.catplot() creates a new figure each time, and passing a figure handle is not possible.
# This creates two separate figures:
sns.catplot(..., kind="box")
sns.catplot(..., kind="strip")
The following works for me with seaborn v0.11:
import seaborn as sns
import matplotlib.pyplot as plt
tips = sns.load_dataset("tips")
g = sns.catplot(x="sex", y="total_bill", hue="smoker", col="time",
data=tips, kind="box",
palette=["#FFA7A0", "#ABEAC9"],
height=4, aspect=.7);
g.map_dataframe(sns.stripplot, x="sex", y="total_bill",
hue="smoker", palette=["#404040"],
alpha=0.6, dodge=True)
# g.map(sns.stripplot, "sex", "total_bill", "smoker",
# palette=["#404040"], alpha=0.6, dodge=True)
plt.show()
Explanations: In a first pass, the box-plots are created using sns.catplot(). The function returns a sns.FacetGrid that accommodates the different axes for each value of the categorical parameter time. In a second pass, this FacetGrid is reused to overlay the scatter plot (sns.stripplot, or alternatively, sns.swarmplot). The above uses method map_dataframe() because data is a pandas DataFrame with named columns. (Alternatively, using map() is also possible.) Setting dodge=True makes sure that the scatter plots are shifted along the categorical axis for each hue category. Finally, note that by calling sns.catplot() with kind="box" and then overlaying the scatter in a second step, the problem of duplicated legend entries is implicitly circumvented.
Alternative (not recommended): It is also possible to create a FacetGrid object first and then call map_dataframe() twice. While this works for this example, in other situations one has to make sure that the mapping of properties is synchronized correctly across facets (see the warning in the docs). sns.catplot() takes care of this, as well as the legend.
g = sns.FacetGrid(tips, col="time", height=4, aspect=.7)
g.map_dataframe(sns.boxplot, x="sex", y="total_bill", hue="smoker",
palette=["#FFA7A0", "#ABEAC9"])
g.map_dataframe(sns.stripplot, x="sex", y="total_bill", hue="smoker",
palette=["#404040"], alpha=0.6, dodge=True)
# Note: the default legend is not resulting in the correct entries.
# Some fix-up step is required here...
# g.add_legend()
plt.show()
Related
I'm trying to plot a boxplot with seaborn with this line of code:
plot = sns.boxplot(x='Fecha', y='spread', data=merge, showfliers=False, showmeans=True, order=fechas)
but the showmeans property is ignored in the plot. So, I obtain this chart:
Why is this happening? and how can I fix that? Thanks
I'm using:
python 3.9.5
seaborn 0.11.1
I don't know why it doesn't work in your case, but if I had to do it manually, I would do this:
import seaborn as sns
tips = sns.load_dataset("tips")
ax = sns.boxplot(x="day", y="total_bill", data=tips, showfliers=False, color='C0')
means = tips.groupby('day')['total_bill'].mean()
ax.plot(range(len(means)), means, color='red', marker='s', linewidth=0)
Only makes sure that x-axis match in two plots.
I would like to draw a violin plot behind a jitter stripplot. The resulting plot has the mean/std bar behind the jitter points which makes it hard to see. I'm wondeing if there's a way to bring the bar in front of the points.
import seaborn as sns
import matplotlib.pyplot as plt
tips = sns.load_dataset("tips")
sns.violinplot(x="day", y="total_bill", data=tips, color="0.8")
sns.stripplot(x="day", y="total_bill", data=tips, jitter=True)
plt.show()
Seaborn doesn't care about exposing the objects it creates to the user. So one would need to collect them from the axes to manipulate them. The property you want to change here is the zorder. So the idea can be to
Plot the violins
Collect the lines and dots from the axes, and give the lines a high zorder, and give the dots an even higher zorder.
Last plot the strip- or swarmplot. This will have a lower zorder automatically.
Example:
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.collections import PathCollection
tips = sns.load_dataset("tips")
ax = sns.violinplot(x="day", y="total_bill", data=tips, color=".8")
for artist in ax.lines:
artist.set_zorder(10)
for artist in ax.findobj(PathCollection):
artist.set_zorder(11)
sns.stripplot(x="day", y="total_bill", data=tips, jitter=True, ax=ax)
plt.show()
I just came across the same problem and could fix it simply by adjusting the zorderparameter in sns.stripplot:
import seaborn as sns
import matplotlib.pyplot as plt
tips = sns.load_dataset("tips")
sns.violinplot(x="day", y="total_bill", data=tips, color="0.8")
sns.stripplot(x="day", y="total_bill", data=tips, jitter=True, zorder=1)
plt.show()
The result then similar to the answer of #ImportanceOfBeingErnest:
I need to add swarmplot to boxplot in matplotlib, but I don't know how to do it with factorplot. I think I can iterate with subplots, but I would like to learn how to do it with seaborn and factorplot.
A simple example (plotting by using the same axis ax):
import seaborn as sns
tips = sns.load_dataset("tips")
ax = sns.boxplot(x="tip", y="day", data=tips, whis=np.inf)
ax = sns.swarmplot(x="tip", y="day", data=tips, color=".2")
The result:
In my case, I need to overlay the swarm factorplot:
g = sns.factorplot(x="sex", y="total_bill",
hue="smoker", col="time",
data=tips, kind="swarm",
size=4, aspect=.7);
and boxplot
I can't figure out how to use axes (extract from g)?
Something like:
g = sns.factorplot(x="sex", y="total_bill",
hue="smoker", col="time",
data=tips, kind="box",
size=4, aspect=.7);
I want something like this, but with factorplot and boxplot instead of violinplot
Instead of trying to overlay the two subplots of a factorplot with individual boxplots (which is possible, but I don't like it), one can just create the two subplots individually.
You would then loop over the groups and axes an plot a pair of box- and swarmplot to each.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
tips = sns.load_dataset("tips")
fig, axes = plt.subplots(ncols=2, sharex=True, sharey=True)
for ax, (n,grp) in zip(axes, tips.groupby("time")):
sns.boxplot(x="sex", y="total_bill", data=grp, whis=np.inf, ax=ax)
sns.swarmplot(x="sex", y="total_bill", hue="smoker", data=grp,
palette=["crimson","indigo"], ax=ax)
ax.set_title(n)
axes[-1].get_legend().remove()
plt.show()
I have created a nested boxplot with an overlayed stripplot using the Seaborn package. I have seen answers on stackoverflow regarding how to edit box properties both for individual boxes and for all boxes using ax.artists generated by sns.boxplot.
Is there any way to edit whisker, cap, flier, etc. properties using a similar method? Currently I have to manually edit values in the restyle_boxplot method of the _BoxPlotter() class in the seaborn -> categorical.py file to get from the default plot to the desired plot:
Default Plot:
Desired Plot:
Here is my code for reference:
sns.set_style('whitegrid')
fig1, ax1 = plt.subplots()
ax1 = sns.boxplot(x="Facility", y="% Savings", hue="Analysis",
data=totalSavings)
plt.setp(ax1.artists,fill=False) # <--- Current Artist functionality
ax1 = sns.stripplot(x="Facility", y="% Savings", hue="Analysis",
data=totalSavings, jitter=.05,edgecolor = 'gray',
split=True,linewidth = 0, size = 6,alpha = .6)
ax1.tick_params(axis='both', labelsize=13)
ax1.set_xticklabels(['Test 1','Test 2','Test 3','Test 4','Test 5'], rotation=90)
ax1.set_xlabel('')
ax1.set_ylabel('Percent Savings (%)', fontsize = 14)
handles, labels = ax1.get_legend_handles_labels()
legend1 = plt.legend(handles[0:3], ['A','B','C'],bbox_to_anchor=(1.05, 1),
loc=2, borderaxespad=0.)
plt.setp(plt.gca().get_legend().get_texts(), fontsize='12')
fig1.set_size_inches(10,7)
EDIT: Note that this method appears to no longer work for matplotlib versions >=3.5. See the answer by #JohanC for an up to date answer
You need to edit the Line2D objects, which are stored in ax.lines.
Heres a script to create a boxplot (based on the example here), and then edit the lines and artists to the style in your question (i.e. no fill, all the lines and markers the same colours, etc.)
You can also fix the rectangle patches in the legend, but you need to use ax.get_legend().get_patches() for that.
I've also plotted the original boxplot on a second Axes, as a reference.
import matplotlib.pyplot as plt
import seaborn as sns
fig,(ax1,ax2) = plt.subplots(2)
sns.set_style("whitegrid")
tips = sns.load_dataset("tips")
sns.boxplot(x="day", y="total_bill", hue="smoker", data=tips, palette="Set1", ax=ax1)
sns.boxplot(x="day", y="total_bill", hue="smoker", data=tips, palette="Set1", ax=ax2)
for i,artist in enumerate(ax2.artists):
# Set the linecolor on the artist to the facecolor, and set the facecolor to None
col = artist.get_facecolor()
artist.set_edgecolor(col)
artist.set_facecolor('None')
# Each box has 6 associated Line2D objects (to make the whiskers, fliers, etc.)
# Loop over them here, and use the same colour as above
for j in range(i*6,i*6+6):
line = ax2.lines[j]
line.set_color(col)
line.set_mfc(col)
line.set_mec(col)
# Also fix the legend
for legpatch in ax2.get_legend().get_patches():
col = legpatch.get_facecolor()
legpatch.set_edgecolor(col)
legpatch.set_facecolor('None')
plt.show()
For matplotlib 3.5 the rectangles for the boxes aren't stored anymore in ax2.artists, but in ax2.patches. As the background of the subplot is also stored as a rectangular patch, the list of patches needs to be filtered.
The code below further makes a few adjustments:
the exact number of lines belonging to one boxplot is counted, as depending on the boxplot options there can be a different number of lines
saturation=1 is used; seaborn prefers to add some desaturation to larger areas, but lines will be better visible with full saturation
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12, 5))
sns.set_style("whitegrid")
tips = sns.load_dataset("tips")
sns.boxplot(x="day", y="total_bill", hue="smoker", data=tips, palette="Set1", ax=ax1)
sns.boxplot(x="day", y="total_bill", hue="smoker", data=tips, palette="Set1", saturation=1, ax=ax2)
box_patches = [patch for patch in ax2.patches if type(patch) == matplotlib.patches.PathPatch]
if len(box_patches) == 0: # in matplotlib older than 3.5, the boxes are stored in ax2.artists
box_patches = ax2.artists
num_patches = len(box_patches)
lines_per_boxplot = len(ax2.lines) // num_patches
for i, patch in enumerate(box_patches):
# Set the linecolor on the patch to the facecolor, and set the facecolor to None
col = patch.get_facecolor()
patch.set_edgecolor(col)
patch.set_facecolor('None')
# Each box has associated Line2D objects (to make the whiskers, fliers, etc.)
# Loop over them here, and use the same color as above
for line in ax2.lines[i * lines_per_boxplot: (i + 1) * lines_per_boxplot]:
line.set_color(col)
line.set_mfc(col) # facecolor of fliers
line.set_mec(col) # edgecolor of fliers
# Also fix the legend
for legpatch in ax2.legend_.get_patches():
col = legpatch.get_facecolor()
legpatch.set_edgecolor(col)
legpatch.set_facecolor('None')
sns.despine(left=True)
plt.show()
One of the coolest things you can easily make in seaborn is boxplot + stripplot combination:
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
tips = sns.load_dataset("tips")
sns.stripplot(x="day", y="total_bill", hue="smoker",
data=tips, jitter=True,
palette="Set2", split=True,linewidth=1,edgecolor='gray')
sns.boxplot(x="day", y="total_bill", hue="smoker",
data=tips,palette="Set2",fliersize=0)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.);
Unfortunately, as you can see above, it produced double legend, one for boxplot, one for stripplot. Obviously, it looks ridiculous and redundant. But I cannot seem to find a way to get rid of stripplot legend and only leave boxplot legend. Probably, I can somehow delete items from plt.legend, but I cannot find it in the documentation.
You can get what handles/labels should exist in the legend before you actually draw the legend itself. You then draw the legend only with the specific ones you want.
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
tips = sns.load_dataset("tips")
sns.stripplot(x="day", y="total_bill", hue="smoker",
data=tips, jitter=True,
palette="Set2", split=True,linewidth=1,edgecolor='gray')
# Get the ax object to use later.
ax = sns.boxplot(x="day", y="total_bill", hue="smoker",
data=tips,palette="Set2",fliersize=0)
# Get the handles and labels. For this example it'll be 2 tuples
# of length 4 each.
handles, labels = ax.get_legend_handles_labels()
# When creating the legend, only use the first two elements
# to effectively remove the last two.
l = plt.legend(handles[0:2], labels[0:2], bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
I want to add that if you use subplots, the legend handling might be a bit more problematic. The code above, which gives a very nice figure by the way (#Sergey Antopolskiy and #Ffisegydd), will not relocate the legend in a subplot, which keeps appearing very stubbornly. See code above adapted to subplots:
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
tips = sns.load_dataset("tips")
fig, axes = sns.plt.subplots(2,2)
sns.stripplot(x="day", y="total_bill", hue="smoker",
data=tips, jitter=True, palette="Set2",
split=True,linewidth=1,edgecolor='gray', ax = axes[0,0])
ax = sns.boxplot(x="day", y="total_bill", hue="smoker",
data=tips,palette="Set2",fliersize=0, ax = axes[0,0])
handles, labels = ax.get_legend_handles_labels()
l = plt.legend(handles[0:2], labels[0:2], bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
The original legend remains. In order to erase it, you can add this line:
axes[0,0].legend(handles[:0], labels[:0])
Edit: in recent versions of seaborn (>0.9.0), this used to leave a small white box in the corner as pointed in the comments. To solve it use the answer in this post:
axes[0,0].get_legend().remove()