Swarmplot с оттенком, влияющим на маркер за пределами цвета - PullRequest
0 голосов
/ 18 октября 2018

Я пытаюсь сделать мой 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()

Результат

enter image description here

Желаемый результат (левый)

enter image description here

Ответы [ 3 ]

0 голосов
/ 22 октября 2018

Следующее обеспечит хак, который позволяет легко получить желаемые различные маркеры для Swarmplots (или, в более общем случае, для любых категорийных диаграмм разброса).Его можно использовать как есть, просто скопируйте его поверх существующих сценариев построения.

Идея состоит в том, чтобы связать цвет точки разброса с маркером.Например, любая точка рассеяния автоматически получит маркер из указанного списка.Как следствие, это работает только для участков с разными цветами.

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

enter image description here

0 голосов
/ 25 октября 2018

Спасибо @ImportanceOfBeingErnest за решение.Я попытался отредактировать его / ее решение, чтобы исправить некоторые незначительные проблемы, но в итоге он / она предложил мне опубликовать свой собственный ответ.

Это решение такое же, как его / ее, но оно не меняетповедение нормального разброса, когда массив маркеров не указан.Это также проще в применении, и это исправляет ошибку, когда легенда теряет заголовок.

Следующая цифра получается с помощью кода ниже:

enter image description here

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()
0 голосов
/ 20 октября 2018

Я действительно думал о той же проблеме некоторое время назад.Я не придумал лучших решений, но у меня есть взлом, который работает нормально.К сожалению, это гораздо проще реализовать, если вы используете dodge=True.

Идея состоит в том, чтобы собрать PathCollections объекты, созданные swarmplot.Если dodge=True, то вы получите N_cat*N_hues+N_hues коллекций (дополнения N_hues используются для создания легенды).Вы можете просто перебрать этот список.Поскольку мы хотим, чтобы все оттенки были одинаковыми, мы используем шаг N_hues, чтобы получить все коллекции, соответствующие каждому из оттенков.После этого вы можете обновить paths этой коллекции до любого Path объекта, который вы выберете.Обратитесь к документации для 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()

enter image description here

ОБНОВЛЕНИЕ Решение, которое «работает»с 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()

enter image description here

...