【问题标题】:swarmplot with hue affecting marker beyond colorswarmplot 的色调会影响颜色以外的标记
【发布时间】:2018-10-18 16:45:44
【问题描述】:

我试图让我的 swarmplot 在黑白和色盲人群中更容易阅读,方法是让色调不仅影响颜色,而且影响标记的另一个几何方面。

MWE

import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="whitegrid")
tips = sns.load_dataset("tips")

fig, ax = plt.subplots(1,1)
ax = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=ax)
plt.show()

结果

想要的结果(左边)

【问题讨论】:

  • 我担心这根本不可能。 :-(
  • @ImportanceOfBeingErnest 没有什么是不可能的,但它可能并不漂亮 ;)
  • @DizietAsahi “不可能”我的意思是没有简单的选择。由于 seaborn 通过一次性使用的类来保护其内部,因此甚至没有任何猴子修补的选项。
  • 老实说,在我看来,使用颜色和标记来表示您要显示的图表可能并不可取。
  • 简单性(视觉分析的一个主要方面)会因为同一事物添加不同颜色和不同标记而丢失,这会增加复杂性,人眼需要更多关注才能理解所代表的内容

标签: python plot seaborn swarmplot


【解决方案1】:

其实我之前也想过同样的问题。我没有想出最好的解决方案,但我有一个可以正常工作的 hack。不幸的是,如果你使用dodge=True,它会更容易实现。

这个想法是收集swarmplot创建的PathCollections对象。如果dodge=True,那么您将获得N_cat*N_hues+N_hues 集合(N_hues 额外用于创建图例)。您可以简单地遍历该列表。由于我们希望所有色调都相同,因此我们使用 N_hues 步长来获取与每种色调对应的所有集合。之后,您可以随意将该集合的paths 更新为您选择的任何Path 对象。参考the documentation for Path了解如何创建路径。

为了简化事情,我在手之前创建了一些虚拟散点图,以获得一些我可以使用的预制Paths。当然,任何Path 都应该可以工作。

import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="whitegrid")
tips = sns.load_dataset("tips")

fig, ax = plt.subplots(1,1)
# dummy plots, just to get the Path objects
a = ax.scatter([1,2],[3,4], marker='s')
b = ax.scatter([1,2],[3,4], marker='^')
square_mk, = a.get_paths()
triangle_up_mk, = b.get_paths()
a.remove()
b.remove()

ax = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=ax, dodge=True)
N_hues = len(pd.unique(tips.sex))

c = ax.collections
for a in c[::N_hues]:
    a.set_paths([triangle_up_mk])
for a in c[1::N_hues]:
    a.set_paths([square_mk])
#update legend
ax.legend(c[-2:],pd.unique(tips.sex))

plt.show()

更新dodge=False“工作”的解决方案。

如果您使用dodge=False,那么您将获得 N+2 个集合,每个类别一个,图例 +2。问题是这些集合中所有不同的标记颜色都混杂在一起。

一个可能但丑陋的解决方案是遍历集合的每个元素,并根据每个元素的颜色创建一个 Path 对象数组。

import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="whitegrid")
tips = sns.load_dataset("tips")

fig, ax = plt.subplots(1,1)
ax = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=ax, dodge=False)

collections = ax.collections
unique_colors = np.unique(collections[0].get_facecolors(), axis=0)
markers = [triangle_up_mk, square_mk]  # this array must be at least as large as the number of unique colors
for collection in collections:
    paths = []
    for current_color in collection.get_facecolors():
        for possible_marker,possible_color in zip(markers, unique_colors):
            if np.array_equal(current_color,possible_color):
                paths.append(possible_marker)
                break
    collection.set_paths(paths)
#update legend
ax.legend(collections[-2:],pd.unique(tips.sex))  

plt.show()

【讨论】:

  • 这里男女颠倒。如果一周中的某一天没有女性访客,我想你会遇到更大的麻烦。
  • 我想这会教我吹牛。我忘了检查图例是否与原始情节相匹配。无论如何,感谢您展示了一种获得正确结果的可靠方法。
【解决方案2】:

以下内容将提供一个 hack,可以轻松实现 swarmplot(或更一般地任何分类散点图)所需的不同标记。它可以按原样使用,只需将其复制到现有绘图脚本之上即可。

这个想法是将散点的颜色与标记联系起来。例如。任何散点都会自动从指定列表中获取标记。因此,这只适用于不同颜色的图。

import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

############## Begin hack ##############
class CM():
    def __init__(self, markers=["o"]):
        self.marker = np.array(markers)
        self.colors = []

    def get_markers_for_colors(self, c):
        for _co in c:
            if not any((_co == x).all() for x in self.colors):
                self.colors.append(_co)
        ind = np.array([np.where((self.colors == row).all(axis=1)) \
                        for row in c]).flatten()
        return self.marker[ind % len(self.marker)]

    def get_legend_handles(self, **kwargs):
        return [plt.Line2D([0],[0], ls="none", marker=m, color=c, mec="none", **kwargs) \
                for m,c in zip(self.marker, self.colors)]

from matplotlib.axes._axes import Axes
import matplotlib.markers as mmarkers
cm = CM(plt.Line2D.filled_markers)
old_scatter = Axes.scatter
def new_scatter(self, *args, **kwargs):
    sc = old_scatter(self, *args, **kwargs)
    c = kwargs.get("c", None)
    if isinstance(c, np.ndarray):
        m = cm.get_markers_for_colors(c)
        paths = []
        for _m in m:
            marker_obj = mmarkers.MarkerStyle(_m)
            paths.append(marker_obj.get_path().transformed(
                        marker_obj.get_transform()))
        sc.set_paths(paths)
    return sc

Axes.scatter = new_scatter
############## End hack. ##############
# Copy and past to your file ##########


## Code ###

sns.set(style="whitegrid")
tips = sns.load_dataset("tips")

fig, ax = plt.subplots(1,1)
## Optionally specify own markers:
#cm.marker = np.array(["^", "s"])
ax = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=ax)

## Optionally adjust legend:
_,l = ax.get_legend_handles_labels()
ax.legend(cm.get_legend_handles(markersize=8),l)

plt.show()

【讨论】:

  • @Miguel 我拒绝了尝试的编辑,因为据我所知,它会遇到与另一个答案相同的问题,即在并非所有色调都存在的情况下它不起作用每个类别。我建议您尝试使用一个数据集,其中周五没有男性,周六或类似的时间没有女性。
  • 我认为你说的不正确。我刚刚使用相同的数据集进行了测试,但根据您的情况进行了测试:tips2.loc[(tips['sex']=="Male") & (tips['day']=="Fri"),'sex']='Female' tips2.loc[(tips['sex']=="Female") & (tips['day']=="Sat"),'sex']='Male',它运行良好。我认为我的代码之所以健壮是因为我的颜色和制造商之间的关系来自调色板(因为我知道我使用的是 seaborn)
  • 好的,可能是。在这种情况下,请提供您自己的答案,如果需要,请解释与现有答案的不同之处。
【解决方案3】:

感谢@ImportanceOfBeingErnest 提供解决方案。我试图编辑他/她的解决方案以解决一些小问题,但最后他/她建议我发布自己的答案。

此解决方案与他/她的解决方案相同,但在未指定标记数组时不会改变正常散射的行为。它的应用也更简单,它修复了图例丢失标题的错误。

下图由以下代码生成:

import seaborn as sns
import matplotlib.pyplot as plt

############## Begin hack ##############
from matplotlib.axes._axes import Axes
from matplotlib.markers import MarkerStyle
from seaborn import color_palette
from numpy import ndarray

def GetColor2Marker(markers):
    palette = color_palette()
    mkcolors = [(palette[i]) for i in range(len(markers))]
    return dict(zip(mkcolors,markers))

def fixlegend(ax,markers,markersize=8,**kwargs):
    # Fix Legend
    legtitle =  ax.get_legend().get_title().get_text()
    _,l = ax.get_legend_handles_labels()
    palette = color_palette()
    mkcolors = [(palette[i]) for i in range(len(markers))]
    newHandles = [plt.Line2D([0],[0], ls="none", marker=m, color=c, mec="none", markersize=markersize,**kwargs) \
                for m,c in zip(markers, mkcolors)]
    ax.legend(newHandles,l)
    leg = ax.get_legend()
    leg.set_title(legtitle)

old_scatter = Axes.scatter
def new_scatter(self, *args, **kwargs):
    colors = kwargs.get("c", None)
    co2mk = kwargs.pop("co2mk",None)
    FinalCollection = old_scatter(self, *args, **kwargs)
    if co2mk is not None and isinstance(colors, ndarray):
        Color2Marker = GetColor2Marker(co2mk)
        paths=[]
        for col in colors:
            mk=Color2Marker[tuple(col)]
            marker_obj = MarkerStyle(mk)
            paths.append(marker_obj.get_path().transformed(marker_obj.get_transform()))
        FinalCollection.set_paths(paths)
    return FinalCollection
Axes.scatter = new_scatter
############## End hack. ##############


# Example Test 
sns.set(style="whitegrid")
tips = sns.load_dataset("tips")

# To test robustness
tips.loc[(tips['sex']=="Male") & (tips['day']=="Fri"),'sex']='Female'
tips.loc[(tips['sex']=="Female") & (tips['day']=="Sat"),'sex']='Male'

Markers = ["o","P"]

fig, axs = plt.subplots(1,2,figsize=(14,5))
axs[0] = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=axs[0])
axs[0].set_title("Original")
axs[1] = sns.swarmplot(x="day", y="total_bill", hue="sex",data=tips,size=8,ax=axs[1],co2mk=Markers)
axs[1].set_title("Hacked")
fixlegend(axs[1],Markers)

plt.show()

【讨论】:

    猜你喜欢
    • 2017-07-10
    • 2016-12-09
    • 2014-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多