mwaskom / seaborn

Statistical data visualization in Python
https://seaborn.pydata.org
BSD 3-Clause "New" or "Revised" License
12.18k stars 1.89k forks source link

Geographic Filled KDE Plot #3694

Open bschweigert opened 1 month ago

bschweigert commented 1 month ago

I am plotting lat/lon points on a geographic map, and I would like to underlay a KDE plot beneath scatter points. Because they are lat/lon points, the plotting will use a cartopy transform (in my case PlateCarree). When calling the kdeplot function with fill=False, everything works fine. However, with fill = True it returns the error:

AttributeError: 'PlateCarree' object has no attribute 'contains_branch_seperately'

Here is reproducible code:

import numpy as np import seaborn as sns import cartopy.crs as ccrs import matplotlib.pyplot as plt import cartopy.feature as cfeature

Create example lat/lon points

lons = np.random.uniform(low = -100, high = -90, size = 20) lats = np.random.uniform(low = 30, high = 40, size = 20)

Create the figure

fig = plt.figure(figsize = (6, 4), dpi = 300) ax = fig.add_subplot(1, 1, 1, projection = ccrs.LambertConformal())

Add features

ax.add_feature(cfeature.STATES.with_scale('50m'), zorder = 1) ax.set_extent((-103, -88, 22.5, 42))

Scatter plot the lat/lon points

ax.scatter(lons, lats, c = 'black', marker = 'x', transform = ccrs.PlateCarree(), zorder = 2)

Add a KDE underlay

kde = sns.kdeplot(x = lons, y = lats, fill = True, transform = ccrs.PlateCarree(), zorder = 1)

mwaskom commented 1 month ago

hm this smells like something between matplotlib and cartopy, that's not a function seaborn is calling directly. can you share the full traceback?

bschweigert commented 1 month ago

Here is the full traceback:

File ~\Miniconda3\envs\figs\Lib\site-packages\spyder_kernels\py3compat.py:356 in compat_exec exec(code, globals, locals)

File c:\users\bschweigert2\desktop\lm clim\error_ex.py:28 kde = sns.kdeplot(x = lons, y = lats, fill = True, transform = ccrs.PlateCarree(), zorder = 1)

File ~\Miniconda3\envs\figs\Lib\site-packages\seaborn\distributions.py:1682 in kdeplot color = _default_color(method, hue, color, kwargs)

File ~\Miniconda3\envs\figs\Lib\site-packages\seaborn\utils.py:136 in _default_color scout = method([], [], **kws)

File ~\Miniconda3\envs\figs\Lib\site-packages\matplotlib__init__.py:1478 in inner return func(ax, *map(sanitize_sequence, args), **kwargs)

File ~\Miniconda3\envs\figs\Lib\site-packages\matplotlib\axes_axes.py:5509 in fill_between return self._fill_between_x_or_y(

File ~\Miniconda3\envs\figs\Lib\site-packages\matplotlib\axes_axes.py:5500 in _fill_between_x_or_y up_x, up_y = kwargs["transform"].contains_branch_seperately(self.transData)

AttributeError: 'PlateCarree' object has no attribute 'contains_branch_seperately'

That would be intriguing, because I have done plenty of plotting recently with matplotlib and cartopy with no issues thus far. Thanks for the help!

mwaskom commented 1 month ago

Based on that it seems like you could reproduce this with something like

f, ax = plt.subplots()
ax.fill_between([], [], transform=ccrs.PlateCarree())

(untested as I don't have the relevant geographic dependencies)

mwaskom commented 1 month ago

You may need to set up the subplot with the same projection too, I am not sure.

nicholas-ys-tan commented 1 month ago

@bschweigert , I've been looking into this one too.

I compared to code from 0.9 branch (I saw it was working from someone on stack exchange in 2020 so picked the branch of that year).

Back then the kwargs being fed into seaborn.distributions.kdeplot went straight to _bivariate_kdeplot_ or _univariate_kdeplot, which later feeds the kwargs into ax.contourf or ax.contour which I believe is managed by matplotlib.

However, somewhere along the way (about 3 years ago), this was added before the kwargs can get to plot_univariate_density or plot_bivariate_density

color = _default_color(method, hue, color, kwargs)

It consequently tries to pass in the transform kwarg into the fill_between method in matplotlib - the tricky bit is that fill_between does accept the transform kwarg, except it must be a matplotlib.transform type!

I am not sure if you might still encounter the error with a univariate density plot with fill=True because it does look like it can calll fill_between. I haven't tested/looked further into this.

But it seems to work fine on a bivariate density maybe because contourf supports the cartopy projection? transform is an argument for matplotlib.contour.ContourSet and seems to accept a wide range of transformation types based on the get_transform function. This function converts the transform variable to a matplotlib.Transform type.

A workaround for you to continue forward is to do this:

 kde = sns.kdeplot(x = lons, y = lats, fill = True, transform = ccrs.PlateCarree()._as_mpl_transform(ax), zorder = 1)

as cartopy projections have a (private) method to convert to matplotlib transform types.

Whether this is a bug by seaborn, or by matplotlib - I don't know. Maybe matplotlib should convert all transforms to a matplotlib type first in fill_between, or maybe seaborn should be doing the conversion - or maybe transform type should be first checked to prevent passing into _default_color() as I don't think it's doing much with that information anyway.

@mwaskom , let me know if you would like to me to raise a PR to address this if one of the suggestions is something you would want to implement in seaborn.