Python Geopandas: Single Legend for multiple plots - python

How do I use a single legend for multiple geopandas plots?
Right now I have a Figure like this:
This post explains how to set legend values to the same for each plot. Though, i would like to have single legend for all plots. Optimally it should be possible to have multiple legends for different df's that I want to plot. E.g. the lines you see in the pictures also have a description.
Here is my current code:
years = [2005, 2009, 2013]
# initialize figure
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(10, 10), dpi=300, constrained_layout=True)
for i, year in enumerate(years):
# subset lines
lines_plot = lines[lines['year'] == year]
# subset controls plot
controls_plot = controls[controls['year'] == year]
# draw subfig
controls_plot.plot(column='pop_dens', ax=ax[i], legend=True, legend_kwds={'orientation': "horizontal"})
lines_plot.plot(ax=ax[i], color='red', lw=2, zorder=2)

Regarding the first of your questions 'How do I use a single legend for multiple geopandas plots?' you could make sure your plots all use the same colors (using the vmin and vmax args of the .plot() function) and then add a single colorbar to the figure like shown below. for the red lines you can just add another legend (the first thing is technically a colorbar not a legend).
import geopandas as gpd
from matplotlib import pyplot as plt
import matplotlib.cm as cm
import matplotlib.colors as mcolors
from matplotlib.lines import Line2D
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
f, ax = plt.subplots(nrows=1, ncols=3, figsize=(9, 4))
# define min and max values and colormap for the plots
value_min = 0
value_max = 1e7
cmap = 'viridis'
world.plot(ax=ax[0], column='pop_est', vmin=value_min, vmax=value_max, cmap=cmap)
world.plot(ax=ax[1], column='pop_est', vmin=value_min, vmax=value_max, cmap=cmap)
world.plot(ax=ax[2], column='pop_est', vmin=value_min, vmax=value_max, cmap=cmap)
# define a mappable based on which the colorbar will be drawn
mappable = cm.ScalarMappable(
norm=mcolors.Normalize(value_min, value_max),
cmap=cmap
)
# define position and extent of colorbar
cb_ax = f.add_axes([0.1, 0.1, 0.8, 0.05])
# draw colorbar
cbar = f.colorbar(mappable, cax=cb_ax, orientation='horizontal')
# add handles for the legend
custom_lines = [
Line2D([0], [0], color='r'),
Line2D([0], [0], color='b'),
]
# define labels for the legend
custom_labels = ['red line', 'blue line']
# plot legend, loc defines the location
plt.legend(
handles=custom_lines,
labels=custom_labels,
loc=(.4, 1.5),
title='2nd legend',
ncol=2
)
plt.tight_layout()
plt.show()

Related

How to customize seaborn boxplot with specific color sequence when boxplots have hue

I want to make boxplots with hues but I want to color code it so that each specific X string is a certain color with the hue just being a lighter color. I am able to do a boxplot without a hue. When I incorporate the hue, I get the second boxplot which loses the colors. Can someone help me customize the colors for the figure that contains the hue?
Essentially, its what the answer for this question is but with boxplots.
This is my code:
first boxplot
order=['Ash1','E1A','FUS','p53']
colors=['gold','teal','darkorange','royalblue']
color_dict=dict(zip(order,colors))
fig,ax=plt.subplots(figsize=(25,15))
bp=sns.boxplot(data=df_idrs, x=df_idrs["construct"], y=df_idrs['Norm_Ef_IDR/Ef_GS'],ax=ax,palette=color_dict)
sns.stripplot(ax=ax,y='Norm_Ef_IDR/Ef_GS', x='construct', data=df_idrs,palette=color_dict,
jitter=1, marker='o', alpha=0.4,edgecolor='black',linewidth=1, dodge=True)
ax.axhline(y=1,linestyle="--",color='black',linewidth=2)
plt.legend(loc='upper left', bbox_to_anchor=(1.03, 1))
second boxplot
order=['Ash1','E1A','FUS','p53']
colors=['gold','teal','darkorange','royalblue']
color_dict=dict(zip(order,colors))
fig,ax=plt.subplots(figsize=(25,15))
bp=sns.boxplot(data=df_idrs, x=df_idrs["construct"], y=df_idrs['Norm_Ef_IDR/Ef_GS'],ax=ax, hue=df_idrs["location"])
sns.stripplot(y='Norm_Ef_IDR/Ef_GS', x='construct', data=df_idrs, hue=df_idrs["location"],
jitter=1, marker='o', alpha=0.4,edgecolor='black',linewidth=1, dodge=True)
ax.axhline(y=1,linestyle="--",color='black',linewidth=2)
plt.legend(loc='upper left', bbox_to_anchor=(1.03, 1))
The only thing that changed was the palette to hue. I have seen many examples on here but I am unable to get them to work. Using the second code, I have tried the following:
Nothing happens for this one.
for ind, bp in enumerate(ax.findobj(PolyCollection)):
rgb = to_rgb(colors[ind // 2])
if ind % 2 != 0:
rgb = 0.5 + 0.5 * np.array(rgb) # make whiter
bp.set_facecolor(rgb)
I get index out of range for the following one.
for i in range(0,4):
mybox = bp.artists[i]
mybox.set_facecolor(color_dict[order[i]])
Matplotlib stores the boxes in ax.patches, but there are also 2 dummy patches (used to construct the legend) that need to be filtered away. The dots of the stripplot are stored in ax.collections. There are also 2 dummy collections for the legend, but as those come at the end, they don't form a problem.
Some remarks:
sns.boxplot returns the subplot on which it was drawn; as it is called with ax=ax it will return that same ax
Setting jitter=1in the stripplot will smear the dots over a width of 1. 1 is the distance between the x positions, and the boxes are only 0.4 wide. To avoid clutter, the code below uses jitter=0.4.
Here is some example code starting from dummy test data:
from matplotlib import pyplot as plt
from matplotlib.legend_handler import HandlerTuple
from matplotlib.patches import PathPatch
from matplotlib.colors import to_rgb
import seaborn as sns
import pandas as pd
import numpy as np
np.random.seed(20230215)
order = ['Ash1', 'E1A', 'FUS', 'p53']
colors = ['gold', 'teal', 'darkorange', 'royalblue']
hue_order = ['A', 'B']
df_idrs = pd.DataFrame({'construct': np.repeat(order, 200),
'Norm_Ef_IDR/Ef_GS': (np.random.normal(0.03, 1, 800).cumsum() + 10) / 15,
'location': np.tile(np.repeat(hue_order, 100), 4)})
fig, ax = plt.subplots(figsize=(12, 5))
sns.boxplot(data=df_idrs, x=df_idrs['construct'], y=df_idrs['Norm_Ef_IDR/Ef_GS'], hue='location',
order=order, hue_order=hue_order, ax=ax)
box_colors = [f + (1 - f) * np.array(to_rgb(c)) # whiten colors depending on hue
for c in colors for f in np.linspace(0, 0.5, len(hue_order))]
box_patches = [p for p in ax.patches if isinstance(p, PathPatch)]
for patch, color in zip(box_patches, box_colors):
patch.set_facecolor(color)
sns.stripplot(y='Norm_Ef_IDR/Ef_GS', x='construct', data=df_idrs, hue=df_idrs['location'],
jitter=0.4, marker='o', alpha=0.4, edgecolor='black', linewidth=1, dodge=True, ax=ax)
for collection, color in zip(ax.collections, box_colors):
collection.set_facecolor(color)
ax.axhline(y=1, linestyle='--', color='black', linewidth=2)
handles = [tuple(box_patches[i::len(hue_order)]) for i in range(len(hue_order))]
ax.legend(handles=handles, labels=hue_order, title='hue category',
handlelength=4, handler_map={tuple: HandlerTuple(ndivide=None, pad=0)},
loc='upper left', bbox_to_anchor=(1.01, 1))
plt.tight_layout()
plt.show()

Heatmap with multi-color y-axis and correspondend colorbar

I want to create a heatmap with seaborn, similar to this (with the following code):
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df)
plt.show()
I'd also like to add a new variable (lets say new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])), such as that the values (and possibly the spine and ticks as well) of the y-axis are colored according to the new variable and a second color bar plotted in the same plot to represent the colors of the y-axis values. How can I do that?
This uses the new values to color the y-ticks and the y-tick labels and adds the associated colorbar.
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df)
new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])
# Create the colorbar for y-ticks and labels
norm = plt.Normalize(new_var.min(), new_var.max())
cmap = matplotlib.cm.get_cmap('turbo')
yticks_locations = ax.get_yticks()
yticks_labels = df.index.values
#hide original ticks
ax.tick_params(axis='y', left=False)
ax.set_yticklabels([])
for var, ytick_loc, ytick_label in zip(new_var.values, yticks_locations, yticks_labels):
color = cmap(norm(float(var)))
ax.annotate(ytick_label, xy=(1, ytick_loc), xycoords='data', xytext=(-0.4, ytick_loc),
arrowprops=dict(arrowstyle="-", color=color, lw=1), zorder=0, rotation=90, color=color)
# Add colorbar for y-tick colors
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
cb = ax.figure.colorbar(sm)
# Match the seaborn style
cb.outline.set_visible(False)
I found your problem interesting, and inspired by the unanswered comment above:
How do you change the second colorbar position? For example, one on top the other on bottom sides. - Py-ser
I decided to spend a while doing some tests. After a little digging i find that cbar_kws={"orientation": "horizontal"} is the argument for sns.heatmap that makes the colorbars horizontal.
Borrowing the code from the solution and making some changes, you can format your plot the way you want as in:
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df, cbar_kws={"orientation": "horizontal"}, square = False, annot = True)
new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])
# Create the colorbar for y-ticks and labels
norm = plt.Normalize(new_var.min(), new_var.max())
cmap = matplotlib.cm.get_cmap('turbo')
yticks_locations = ax.get_yticks()
yticks_labels = df.index.values
#hide original ticks
ax.tick_params(axis='y', left=False)
ax.set_yticklabels([])
for var, ytick_loc, ytick_label in zip(new_var.values, yticks_locations, yticks_labels):
color = cmap(norm(float(var)))
ax.annotate(ytick_label, xy=(1, ytick_loc), xycoords='data', xytext=(-0.4, ytick_loc),
arrowprops=dict(arrowstyle="-", color=color, lw=1), zorder=0, rotation=90, color=color)
# Add colorbar for y-tick colors
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
cb = ax.figure.colorbar(sm)
# Match the seaborn style
cb.outline.set_visible(False)
Also, you will notice that I listed the values ​​related to each cell in the heatmap, but just out of curiosity to make it clearer to check that everything was working as expected.
I'm still not very happy with the shape/size of the horizontal colorbar, but I'll keep testing and update any progress by editing this answer!
==========================================
EDIT
just to keep track of the updates, first i tried to change just some parameters of seaborn's heatmap function but wouldn't consider this a major improvement on the task... by adding
ax = sns.heatmap(df, cbar_kws = dict(use_gridspec=True, location="top", shrink =0.6), square = True, annot = True)
I end up with:
I did get to separate the colormap using the matplotlib subplot routine and honestly i believe this is the right way given the parameter control that is possible to get here, by:
# Define two rows for subplots
fig, (cax, ax) = plt.subplots(nrows=2, figsize=(5,5.025), gridspec_kw={"height_ratios":[0.025, 1]})
# Default heatmap
ax = sns.heatmap(df, cbar=False, annot = True)
# colorbar
fig.colorbar(ax.get_children()[0], cax=cax, orientation="horizontal")
plt.show()
I obtained:
Which is still not the prettiest graph I've ever made, but now the position and size of the heatmap can be edited normally within the plt.subplots subroutines that give absolute control over these parameters.

How to plot a paired histogram using seaborn

I would like to make a paired histogram like the one shown here using the seaborn distplot.
This kind of plot can also be referred to as the back-to-back histogram shown here, or a bihistogram inverted/mirrored along the x-axis as discussed here.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
green = np.random.normal(20,10,1000)
blue = np.random.poisson(60,1000)
fig, ax = plt.subplots(figsize=(8,6))
sns.distplot(blue, hist=True, kde=True, hist_kws={'edgecolor':'black'}, kde_kws={'linewidth':2}, bins=10, color='blue')
sns.distplot(green, hist=True, kde=True, hist_kws={'edgecolor':'black'}, kde_kws={'linewidth':2}, bins=10, color='green')
ax.set_xticks(np.arange(-20,121,20))
ax.set_yticks(np.arange(0.0,0.07,0.01))
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.show()
Here is the output:
When I use the method discussed here (plt.barh), I get the bar plot shown just below, which is not what I am looking for.
Or maybe I haven't understood the workaround well enough...
A simple/short implementation of python-seaborn-distplot similar to these kinds of plots would be perfect. I edited the figure of my first plot above to show the kind of plot I hope to achieve (though y-axis not upside down):
Any leads would be greatly appreciated.
You could use two subplots and invert the y-axis of the lower one and plot with the same bins.
df = pd.DataFrame({'a': np.random.normal(0,5,1000), 'b': np.random.normal(20,5,1000)})
fig =plt.figure(figsize=(5,5))
ax = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
bins = np.arange(-20,40)
ax.hist(df['a'], bins=bins)
ax2.hist(df['b'],color='orange', bins=bins)
ax2.invert_yaxis()
edit:
improvements suggested by #mwaskom
fig, axes = plt.subplots(nrows=2, ncols=1, sharex=True, figsize=(5,5))
bins = np.arange(-20,40)
for ax, column, color, invert in zip(axes.ravel(), df.columns, ['teal', 'orange'], [False,True]):
ax.hist(df[column], bins=bins, color=color)
if invert:
ax.invert_yaxis()
plt.subplots_adjust(hspace=0)
Here is a possible approach using seaborn's displots.
Seaborn doesn't return the created graphical elements, but the ax can be interrogated. To make sure the ax only contains the elements you want upside down, those elements can be drawn first. Then, all the patches (the rectangular bars) and the lines (the curve for the kde) can be given their height in negative. Optionally the x-axis can be set at y == 0 using ax.spines['bottom'].set_position('zero').
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
green = np.random.normal(20, 10, 1000)
blue = np.random.poisson(60, 1000)
fig, ax = plt.subplots(figsize=(8, 6))
sns.distplot(green, hist=True, kde=True, hist_kws={'edgecolor': 'black'}, kde_kws={'linewidth': 2}, bins=10,
color='green')
for p in ax.patches: # turn the histogram upside down
p.set_height(-p.get_height())
for l in ax.lines: # turn the kde curve upside down
l.set_ydata(-l.get_ydata())
sns.distplot(blue, hist=True, kde=True, hist_kws={'edgecolor': 'black'}, kde_kws={'linewidth': 2}, bins=10,
color='blue')
ax.set_xticks(np.arange(-20, 121, 20))
ax.set_yticks(np.arange(0.0, 0.07, 0.01))
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
pos_ticks = np.array([t for t in ax.get_yticks() if t > 0])
ticks = np.concatenate([-pos_ticks[::-1], [0], pos_ticks])
ax.set_yticks(ticks)
ax.set_yticklabels([f'{abs(t):.2f}' for t in ticks])
ax.spines['bottom'].set_position('zero')
plt.show()

How to edit properties of whiskers, fliers, caps, etc. in Seaborn boxplot

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()

Matplotlib: how to adjust zorder of second legend?

Here is an example that reproduces my problem:
import matplotlib.pyplot as plt
import numpy as np
data1,data2,data3,data4 = np.random.random(100),np.random.random(100),np.random.random(100),np.random.random(100)
fig,ax = plt.subplots()
ax.plot(data1)
ax.plot(data2)
ax.plot(data3)
ax2 = ax.twinx()
ax2.plot(data4)
plt.grid('on')
ax.legend(['1','2','3'], loc='center')
ax2.legend(['4'], loc=1)
How can I get the legend in the center to plot on top of the lines?
To get exactly what you have asked for, try the following. Note I have modified your code to define the labels when you generate the plot and also the colors so you don't get a repeated blue line.
import matplotlib.pyplot as plt
import numpy as np
data1,data2,data3,data4 = (np.random.random(100),
np.random.random(100),
np.random.random(100),
np.random.random(100))
fig,ax = plt.subplots()
ax.plot(data1, label="1", color="k")
ax.plot(data2, label="2", color="r")
ax.plot(data3, label="3", color="g")
ax2 = ax.twinx()
ax2.plot(data4, label="4", color="b")
# First get the handles and labels from the axes
handles1, labels1 = ax.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
# Add the first legend to the second axis so it displaysys 'on top'
first_legend = plt.legend(handles1, labels1, loc='center')
ax2.add_artist(first_legend)
# Add the second legend as usual
ax2.legend(handles2, labels2)
plt.show()
Now I will add that it would be clearer if you just use a single legend adding all the lines to that. This is described in this SO post and in the code above can easily be achieved with
ax2.legend(handles1+handles2, labels1+labels2)
But obviously you may have your own reasons for wanting two legends.

Categories