Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geographic Filled KDE Plot #3694

Open
bschweigert opened this issue May 10, 2024 · 5 comments
Open

Geographic Filled KDE Plot #3694

bschweigert opened this issue May 10, 2024 · 5 comments

Comments

@bschweigert
Copy link

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
Copy link
Owner

mwaskom commented May 11, 2024

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
Copy link
Author

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
Copy link
Owner

mwaskom commented May 12, 2024

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
Copy link
Owner

mwaskom commented May 12, 2024

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

@nicholas-ys-tan
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants