在 Seaborn / Matplotlib 的小提琴图上指定高于和低于中位数的颜色

Posted

技术标签:

【中文标题】在 Seaborn / Matplotlib 的小提琴图上指定高于和低于中位数的颜色【英文标题】:Specify colors above and below median on violin plot in Seaborn / Matplotlib 【发布时间】:2019-01-09 09:22:41 【问题描述】:

我正在生成小提琴图,并希望在分布的中位数处显示一条线,中位数上方和下方的区域使用不同的颜色。这是一个 MVCE:

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

np.random.seed(1)
d1 = np.random.normal(size=5000)
d2 = np.random.normal(scale=0.5, size=5000)

x = d1 + d2

plt.figure(figsize=(5, 5))
seaborn.violinplot(y=x)

这是结果图:

以及我想要创建的输出:

我已经搜索了一段时间,似乎找不到任何文档或示例来执行此操作。可以在 matplotlib 或 seaborn(或 Python 中的任何其他绘图库)中完成吗?

【问题讨论】:

【参考方案1】:

我对结果并不完全满意,但这是我的尝试。

我使用 violinplot()matplotlib 版本而不是 seaborn 的版本,因为前者返回一个包含所制作的各种艺术家的字典,尽管同样可以对 seaborn 进行更多的努力来完成找到正确的Collection 对象。

小提琴图实际上是使用PolyCollection 绘制的,从中可以提取顶点的坐标。有了这些,只需选择高于或低于中位数的坐标,然后创建一个新的PolyCollection 以添加到轴上。最后,我删除了原来的艺术家。

我对结果并不完全满意,因为这样创建的两位艺术家没有接触。这是因为我们缺少最初将底部连接到顶部的顶点。如果这对您来说是个问题,可以通过在任一集合顶点坐标的开头和结尾添加与另一个集合中的顶点坐标相匹配的新坐标来解决此问题,从而填补空白。

fig, ax = plt.subplots()


np.random.seed(1)
d1 = np.random.normal(size=5000)
d2 = np.random.normal(scale=0.5, size=5000)

x = d1 + d2
mdn = np.median(x)

# draw the violinplot using matplotlib, storing the resulting dictionnary of artists
result_dict = ax.violinplot(x, showextrema=False, showmedians=True)

orig_violin = result_dict['bodies'][0]  # in this case, there is only one violin plot, hence [0]
orig_vertices = orig_violin.get_paths()[0].vertices # extract the vertices coordinates from the Path object contained in the PolyCollection

top = orig_vertices[orig_vertices[:,1]>=mdn]   # the vertices above the median
bottom = orig_vertices[orig_vertices[:,1]<mdn] # and below 

# create new PolyCollections, adjusting their appearance as desired
topP = matplotlib.collections.PolyCollection([top])
topP.set_facecolor('C1')
bottomP = matplotlib.collections.PolyCollection([bottom])
bottomP.set_facecolor('C2')

ax.add_collection(topP)
ax.add_collection(bottomP)

# remove the original(s) artists created by matplotlib's violinplot()
[temp.remove() for temp in result_dict['bodies']]

【讨论】:

是的,我想出了大致相同的解决方案,只是为时已晚。由于仍然存在一些细微的差异,我认为无论如何发布都是有用的。 @Fiver 如果有疑问,这可能是接受的解决方案,因为我的解决方案来得较晚,只会带来边际改善。 @ImportanceOfBeingErnest 谢谢你们。正如我所怀疑的那样,在 seaborn 中似乎没有直接的方法可以做到这一点。我想我会把这个问题留得更久一些,也许会悬赏一下,看看我是否还有其他想法。再次感谢。 “在 seaborn 中执行此操作”的原因是什么? Seaborn 只是 matplotlib 的包装器。您还在寻找其他什么想法? @ImportanceOfBeingErnest 抱歉,我并不是要暗示它必须在 seaborn 中完成。我改成了seaborn,因为它看起来更好,也更方便;另外,我想保留箱线图。你的两篇文章都非常有助于展示如何修改情节的内部运作。【参考方案2】:

我已经准备好解决方案,但现在看到@DizietAsahi 发布了类似的解决方案。我仍然会在这里发布它,并且只指出不同之处。

通常你会想要几把小提琴。所以最好把所有东西都放在一个循环中。该循环可以存在于函数中。并且该功能可以直接用于小提琴的样式。现在,我将与现有解决方案形成对比,创建两个小提琴图,并从每个图上剪下上半部分或下半部分。这可能看起来像

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(1)
d1 = np.random.normal(size=5000)
d2 = np.random.normal(scale=0.2, size=5000)

x = [d1+1, d1 + d2, d2-0.5]

fig, ax = plt.subplots()
violin1 = ax.violinplot(x, showmedians=True, showextrema=False, points=300)
violin2 = ax.violinplot(x, showmedians=True, showextrema=False, points=300)

def cut_violin_at_median(violin, cut_above=True, **kwargs):
    for i in range(len(violin["bodies"])):
        median = violin["cmedians"].get_paths()[i].vertices[0,1]
        pthcol = violin["bodies"][i]
        v = pthcol.get_paths()[0].vertices
        if cut_above:
            ind = v[:,1] <= median
        else:
            ind = v[:,1] > median
        pthcol.set_verts([v[ind]])
        pthcol.set(**kwargs)

cut_violin_at_median(violin1, cut_above=True, color="crimson")
cut_violin_at_median(violin2, cut_above=False, color="limegreen")

plt.show()

请注意,为了在小提琴的两个部分之间不存在巨大差距,您可以增加执行核密度估计的点数。在这里,我使用 300,但也许更大的数字也有用。

【讨论】:

以上是关于在 Seaborn / Matplotlib 的小提琴图上指定高于和低于中位数的颜色的主要内容,如果未能解决你的问题,请参考以下文章

如何在不更改 matplotlib 默认值的情况下使用 seaborn?

在 matplotlib/seaborn 中使用 groupby 绘制线图?

如何在 seaborn / matplotlib 中绘制和注释分组条形

使用 seaborn 向 matplotlib 图添加次要网格线

Python可视化必备,在Matplotlib/Seaborn中轻松玩转图形拼接!

使用 seaborn 或 matplotlib 分组箱线图的数据格式