8

Using a Seaborn scatterplot, how can I set the markers to be hollow circles instead of filled circles?

Here is a simple example:

import pandas as pd
import seaborn as sns

df = pd.DataFrame(
    {'x': [3,2,5,1,1,0],
     'y': [1,1,2,3,0,2],
     'cat': ['a','a','a','b','b','b']}
)

sns.scatterplot(data=df, x='x', y='y', hue='cat')

enter image description here

I have tried the following without success; most of these do not throw an error but instead produce the same plot as above. I think these don't work because the colors are set with the hue parameter, but I am not sure what the fix is.

sns.scatterplot(data=df, x='x', y='y', hue='cat', facecolors = 'none')
sns.scatterplot(data=df, x='x', y='y', hue='cat', facecolors = None)
sns.scatterplot(data=df, x='x', y='y', hue='cat', markerfacecolor = 'none')
sns.scatterplot(data=df, x='x', y='y', hue='cat', markerfacecolor = None)

with sns.plotting_context(rc={"markerfacecolor": None}):
    sns.scatterplot(data=df, x='x', y='y', hue='cat')
a11
  • 3,122
  • 4
  • 27
  • 66
  • Take a look at this link: https://stackoverflow.com/questions/62778123/customize-legend-marker-facecolor-in-scatterplot-with-patches - the standard way of achieving this in matplotlib is to set facecolor to none – Michel Gokan Khan Feb 28 '21 at 01:04
  • 1
    @MichelGokan I tried that, but it did not work with Seaborn: `'PathCollection' object has no property 'markerfacecolor'` and just using `facecolor='none'` or `facecolor=None` produces the same plot shown in the OP – a11 Feb 28 '21 at 01:38
  • 2
    I think it would be reasonable for seaborn to map the hue variable to the edge color when `facecolor` is "none". It would take some deliberation and might be more complicated that it seems at first, but if you file an issue on the github I would support it. – mwaskom Feb 28 '21 at 13:33

4 Answers4

11

In principle you should be able to create a circular marker with fillstyle="none", but there are some deep complications there and it doesn't currently work as you'd hope.

The simplest pure seaborn solution is to take advantage of the fact that you can use arbitrary latex symbols as the markers:

sns.scatterplot(data=df, x='x', y='y', hue="cat", marker="$\circ$", ec="face", s=100)

enter image description here

That is somewhat limited because you lose control over the thickness of the circle.

A hybrid seaborn-matplotlib approach is more flexible, but also more cumbersome (you need to create the legend yourself):

palette = {"a": "C0", "b": "C1"}
kws = {"s": 70, "facecolor": "none", "linewidth": 1.5}

ax = sns.scatterplot(
    data=df, x='x', y='y',
    edgecolor=df["cat"].map(palette),
    **kws,
)
handles, labels = zip(*[
    (plt.scatter([], [], ec=color, **kws), key) for key, color in palette.items()
])
ax.legend(handles, labels, title="cat")

enter image description here

A third option is to use FacetGrid. This is less flexible because the plot will have to be in its own figure. But it's reasonably simple; the other answer uses FacetGrid but it's a bit over-engineered because it forgets the hue_kws parameter:

palette = ["C0", "C1"]
g = sns.FacetGrid(
    data=df, hue="cat",
    height=4, aspect=1.25,
    hue_kws={"edgecolor": palette},
)
g.map(plt.scatter, "x", "y", facecolor="none", lw=1.5)
g.add_legend()

enter image description here

mwaskom
  • 46,693
  • 16
  • 125
  • 127
3

Sometimes*, retreating to matplotlib functionality is easier:

import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({'x': [3,2,5,2,1,0],
                   'y': [1,1,2,3,0,2],
                   'cat': ['a','a','a','b','b','b']})

fig, ax = plt.subplots()
m_colors = ["blue", "tab:orange", "red", "green"]

for (cat, group), col in zip(df.groupby("cat"), m_colors):
    ax.scatter(group.x, group.y, edgecolors=col, facecolors="none", alpha=0.7, label=cat)

ax.legend(title="empty circles")
plt.show()

Sample output: enter image description here

*seaborn and pandas are great for what they provide. But I see so many examples, where people think "This was easy, so it should be easy to simply add the XYZ feature" and then end up with convoluted code that would be much simpler had they written their code in base matplotlib in the first place.

Mr. T
  • 11,960
  • 10
  • 32
  • 54
0

I modified your code using seaborn.FacetGrid as follows (based on the code provided by @cphlewis here):

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

x1 = [3,2,5]
y1 = [1,1,2]
x2 = [1,1,0]
y2 = [3,0,2]

df1 = pd.DataFrame({'x1':x1, 'y1':y1})
df2 = pd.DataFrame({'x2':x2, 'y2':y2})

df = pd.concat([df1.rename(columns={'x1':'x','y1':'y'})
                .join(pd.Series(['a']*len(df1), name='df')),
                df2.rename(columns={'x2':'x','y2':'y'})
                .join(pd.Series(['b']*len(df2), name='df'))],
               ignore_index=True)

pal = dict(a="blue", b="red")

g = sns.FacetGrid(df, hue='df', palette=pal, size=5)

g.map(plt.scatter, "x", "y", s=50, alpha=.7,
      linewidth=.5,
      facecolors = 'none',
      edgecolor=['red', 'blue'])

markers = [plt.Line2D([0,0],[0,0], markeredgecolor=pal[key],
                      marker='o', markerfacecolor='none',
                      mew=0.3,
                      linestyle='')
            for key in pal]

plt.legend(markers, pal.keys(), numpoints=1, title="cat")
plt.show()

Here are the results:

enter image description here

Michel Gokan Khan
  • 2,525
  • 3
  • 30
  • 54
0

Based on a similar question (How to get markers with no fill, from seaborn 0.11+), here is a nice new way to handle this:

First, define your colormap. Second, set your appropriate hue and specify the colormap in the palette and edgecolor parameters.

penguins = sns.load_dataset("penguins")

colormap = {"Adelie": "purple", "Chinstrap": "orange", "Gentoo": "green"}

sns.jointplot(
    data=penguins,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="species",
    palette=colormap,
    ec=penguins["species"].map(colormap),
    fc="none",
)

enter image description here

a11
  • 3,122
  • 4
  • 27
  • 66