seaborn.objects 接口#
seaborn.objects
命名空间在版本 0.12 中被引入,作为一个全新的界面用于制作 seaborn 图表。它提供了一个更加一致和灵活的 API,包含了一系列可组合的类用于数据转换和绘图。与现有的 seaborn
函数相比,新界面旨在支持端到端的图表指定和定制,而无需下降到 matplotlib(尽管在必要时仍然可以这样做)。
备注
对象接口目前处于实验阶段且不完整。它已经足够稳定,可以用于严肃的使用,但肯定还有一些粗糙的地方和缺失的功能。
指定图表并映射数据#
对象接口应按以下约定导入:
import seaborn.objects as so
seaborn.objects
命名空间将提供对所有相关类的访问。最重要的是 Plot
。您通过实例化一个 Plot
对象并调用其方法来指定图表。让我们看一个简单的例子:
(
so.Plot(penguins, x="bill_length_mm", y="bill_depth_mm")
.add(so.Dot())
)
这段代码生成一个散点图,看起来应该相当熟悉。就像使用 seaborn.scatterplot()
时一样,我们传递了一个整洁的数据框(penguins
),并将它的两列分配给图形的 x
和 y
坐标。但与先选择图表类型然后添加数据分配不同,这里我们先进行数据分配,然后添加图形元素。
设置属性#
The Dot
类是 Mark
的一个例子:一个图形化表示数据值的对象。每个标记都有一些属性可以设置,以改变其外观:
(
so.Plot(penguins, x="bill_length_mm", y="bill_depth_mm")
.add(so.Dot(color="g", pointsize=4))
)
映射属性#
与seaborn的函数一样,也可以将数据值 映射 到各种图形属性上:
(
so.Plot(
penguins, x="bill_length_mm", y="bill_depth_mm",
color="species", pointsize="body_mass_g",
)
.add(so.Dot())
)
虽然这种基本功能并不新颖,但与函数API的一个重要区别是,属性是使用与直接设置它们相同的参数名称映射的(而不是有 hue
与 color
等区别)。重要的是 属性定义的位置:在初始化 Dot
时传递一个值将直接设置它,而在设置 Plot
时分配一个变量将 映射 相应的数据。
除了这个区别之外,对象接口还允许映射更广泛的标记属性:
(
so.Plot(
penguins, x="bill_length_mm", y="bill_depth_mm",
edgecolor="sex", edgewidth="body_mass_g",
)
.add(so.Dot(color=".8"))
)
定义组#
The Dot
标记代表每个数据点独立存在,因此将变量分配给属性只会改变每个点的外观。对于分组或连接观察结果的标记,例如 Line
,它还决定了不同图形元素的数量:
(
so.Plot(healthexp, x="Year", y="Life_Expectancy", color="Country")
.add(so.Line())
)
通过使用 group
,也可以在不改变任何视觉属性的情况下定义一个分组:
(
so.Plot(healthexp, x="Year", y="Life_Expectancy", group="Country")
.add(so.Line())
)
绘图前的数据转换#
统计变换#
与许多 seaborn 函数一样,对象接口支持统计变换。这些变换由 Stat
对象执行,例如 Agg
。
(
so.Plot(penguins, x="species", y="body_mass_g")
.add(so.Bar(), so.Agg())
)
在函数接口中,一些视觉表示(例如 seaborn.barplot()
)可以进行统计变换,而其他一些(例如 seaborn.scatterplot()
)则不能。对象接口更清晰地将表示和变换分开,允许你组合 Mark
和 Stat
对象:
(
so.Plot(penguins, x="species", y="body_mass_g")
.add(so.Dot(pointsize=10), so.Agg())
)
在通过映射属性形成组时,Stat
转换会分别应用于每个组:
(
so.Plot(penguins, x="species", y="body_mass_g", color="sex")
.add(so.Dot(pointsize=10), so.Agg())
)
解决重叠绘图#
一些 seaborn 函数也有机制可以自动解决重叠绘图问题,例如当分配了 hue
时,seaborn.barplot()
会 “错开” 条形图。对象接口的默认行为较为简单。代表多个组的条形图默认会重叠:
(
so.Plot(penguins, x="species", y="body_mass_g", color="sex")
.add(so.Bar(), so.Agg())
)
尽管如此,可以通过 Dodge
实现的第二次变换,将 Bar
标记与 Agg
统计组合在一起。
(
so.Plot(penguins, x="species", y="body_mass_g", color="sex")
.add(so.Bar(), so.Agg(), so.Dodge())
)
The Dodge
类是 Move
变换的一个例子,它类似于 Stat
但只调整 x
和 y
坐标。Move
类可以应用于任何标记,并且不需要先使用 Stat
:
(
so.Plot(penguins, x="species", y="body_mass_g", color="sex")
.add(so.Dot(), so.Dodge())
)
也可以按顺序应用多个 移动
操作:
(
so.Plot(penguins, x="species", y="body_mass_g", color="sex")
.add(so.Dot(), so.Dodge(), so.Jitter(.3))
)
通过转换创建变量#
The Agg
统计需要 x
和 y
都已经被定义,但变量也可以通过统计转换来 创建 。例如,Hist
统计只需要定义 x
或 y
中的一个,它将通过计数观察来创建另一个:
(
so.Plot(penguins, x="species")
.add(so.Bar(), so.Hist())
)
当给定数值数据时,Hist
统计量也会创建新的 x
值(通过分箱):
(
so.Plot(penguins, x="flipper_length_mm")
.add(so.Bars(), so.Hist())
)
注意我们是如何使用 Bars
而不是 Bar
来绘制带有连续 x
轴的图表的。这两个标记是相关的,但 Bars
有不同的默认设置,更适合用于连续直方图。它还会生成一个不同的、更高效的 matplotlib 艺术家。你会在其他地方发现单数/复数标记的模式。复数版本通常针对具有更多标记的情况进行了优化。
一些变换接受 x
和 y
,但为每个坐标添加 间隔 数据。这在聚合后绘制误差条时尤其相关:
(
so.Plot(penguins, x="body_mass_g", y="species", color="sex")
.add(so.Range(), so.Est(errorbar="sd"), so.Dodge())
.add(so.Dot(), so.Agg(), so.Dodge())
)
定向标记和变换#
在聚合、躲避和绘制条形图时,x
和 y
变量被区别对待。每个操作都有一个 方向 的概念。Plot
尝试根据变量的数据类型自动确定方向。例如,如果我们交换 species
和 body_mass_g
的赋值,我们将得到相同的图表,但方向是水平的:
(
so.Plot(penguins, x="body_mass_g", y="species", color="sex")
.add(so.Bar(), so.Agg(), so.Dodge())
)
有时,正确的方向是不明确的,例如当 x
和 y
变量都是数值时。在这些情况下,你可以通过传递 orient
参数给 Plot.add()
来明确指定。
(
so.Plot(tips, x="total_bill", y="size", color="time")
.add(so.Bar(), so.Agg(), so.Dodge(), orient="y")
)
构建和显示图表#
到目前为止,大多数示例都生成只有一个标记的单个子图。但是 Plot
并不限制你只能这样做。
添加多层#
通过反复调用 Plot.add()
可以创建更复杂的单子图图形。每次调用时,它都会定义图中的一个 层 。例如,我们可能想要添加一个散点图(现在使用 Dots
),然后是一个回归拟合:
(
so.Plot(tips, x="total_bill", y="tip")
.add(so.Dots())
.add(so.Line(), so.PolyFit())
)
在 Plot
构造函数中定义的变量映射将用于所有图层:
(
so.Plot(tips, x="total_bill", y="tip", color="time")
.add(so.Dots())
.add(so.Line(), so.PolyFit())
)
特定层的映射#
你也可以定义一个映射,使其仅在特定层中使用。这是通过在相关层的 Plot.add
调用中定义映射来实现的:
(
so.Plot(tips, x="total_bill", y="tip")
.add(so.Dots(), color="time")
.add(so.Line(color=".2"), so.PolyFit())
)
或者,为整个图定义图层,但通过将变量设置为 None
来*移除*特定图层:
(
so.Plot(tips, x="total_bill", y="tip", color="time")
.add(so.Dots())
.add(so.Line(color=".2"), so.PolyFit(), color=None)
)
回顾一下,指定标记属性的值有三种方法:(1) 通过映射所有图层中的变量,(2) 通过映射特定图层中的变量,以及 (3) 通过直接设置属性:
分面和配对子图#
与 seaborn 的图形级函数(如 seaborn.displot()
、seaborn.catplot()
等)类似,Plot
接口也可以生成包含多个“面”或包含数据子集的子图的图形。这是通过 Plot.facet()
方法实现的:
(
so.Plot(penguins, x="flipper_length_mm")
.facet("species")
.add(so.Bars(), so.Hist())
)
调用 Plot.facet()
方法,并传入用于定义图表列和/或行的变量:
(
so.Plot(penguins, x="flipper_length_mm")
.facet(col="species", row="sex")
.add(so.Bars(), so.Hist())
)
你可以通过“跨维度包装”来使用具有更多级别的变量进行分面:
(
so.Plot(healthexp, x="Year", y="Life_Expectancy")
.facet(col="Country", wrap=3)
.add(so.Line())
)
除非你明确排除某些图层,否则所有图层都将被分面,这可以为每个子图提供额外的上下文信息:
(
so.Plot(healthexp, x="Year", y="Life_Expectancy")
.facet("Country", wrap=3)
.add(so.Line(alpha=.3), group="Country", col=None)
.add(so.Line(linewidth=3))
)
另一种生成子图的方法是 Plot.pair()
。类似于 seaborn.PairGrid
,这会在每个子图上绘制所有数据,使用不同的变量作为 x 和/或 y 坐标:
(
so.Plot(penguins, y="body_mass_g", color="species")
.pair(x=["bill_length_mm", "bill_depth_mm"])
.add(so.Dots())
)
只要操作在相反的维度上添加子图,您就可以结合分面和配对:
(
so.Plot(penguins, y="body_mass_g", color="species")
.pair(x=["bill_length_mm", "bill_depth_mm"])
.facet(row="sex")
.add(so.Dots())
)
与 matplotlib 集成#
在某些情况下,您可能希望在一个图形中出现多个子图,其结构比 Plot.facet()
或 Plot.pair()
所能提供的更复杂。当前的解决方案是将图形设置委托给 matplotlib,并通过 Plot.on()
方法提供 Plot
应使用的 matplotlib 对象。该对象可以是 matplotlib.axes.Axes
、matplotlib.figure.Figure
或 matplotlib.figure.SubFigure
;后者对于构建定制的子图布局最为有用:
f = mpl.figure.Figure(figsize=(8, 4))
sf1, sf2 = f.subfigures(1, 2)
(
so.Plot(penguins, x="body_mass_g", y="flipper_length_mm")
.add(so.Dots())
.on(sf1)
.plot()
)
(
so.Plot(penguins, x="body_mass_g")
.facet(row="sex")
.add(so.Bars(), so.Hist())
.on(sf2)
.plot()
)
构建和显示图表#
需要知道的一个重要事情是,Plot
方法会克隆它们所调用的对象,并返回该克隆对象,而不是就地更新对象。这意味着你可以定义一个通用的绘图规范,然后在此基础上生成几种变体。
因此,考虑这个基本规范:
p = so.Plot(healthexp, "Year", "Spending_USD", color="Country")
我们可以使用它来绘制折线图:
p.add(so.Line())
或者是一个堆积面积图:
p.add(so.Area(), so.Stack())
这些 Plot
方法是完全声明式的。调用它们会更新绘图规范,但实际上并不进行任何绘图。这一特性的一个结果是,方法可以按任意顺序调用,并且其中许多方法可以被多次调用。
图表实际上是在什么时候渲染的?Plot
针对在笔记本环境中使用进行了优化。当 Plot
在 Jupyter REPL 中显示时,渲染会自动触发。这就是为什么在上面的例子中我们没有看到任何东西,我们在那里定义了一个 Plot
但将其赋值给 p
而不是让它返回给 REPL。
要在笔记本中查看图表,可以从单元格的最后一行返回它,或者在对象上调用 Jupyter 的内置 display
函数。笔记本集成完全绕过了 matplotlib.pyplot
,但您可以通过调用 Plot.show()
在其他上下文中使用其图形显示机制。
你也可以通过调用 Plot.save()
将图表保存到文件(或缓冲区)中。
自定义外观#
新的界面旨在通过 Plot
支持深度的自定义,减少切换工具并直接使用 matplotlib 功能的需要。(但请耐心等待;尚未实现实现此目标所需的所有功能!)
参数化尺度#
所有数据依赖的属性都由 Scale
的概念和 Plot.scale()
方法控制。此方法接受几种不同类型的参数。一种可能性,最接近 matplotlib 中使用尺度的用法,是传递一个转换坐标的函数名称:
(
so.Plot(diamonds, x="carat", y="price")
.add(so.Dots())
.scale(y="log")
)
Plot.scale()
也可以控制语义属性(如 color
)的映射。你可以直接传递任何你想要传递给 seaborn 函数接口中 palette
参数的参数:
(
so.Plot(diamonds, x="carat", y="price", color="clarity")
.add(so.Dots())
.scale(color="flare")
)
另一个选项是提供一个 (min, max)
值的元组,控制刻度应映射的范围。这适用于数值属性和颜色:
(
so.Plot(diamonds, x="carat", y="price", color="clarity", pointsize="carat")
.add(so.Dots())
.scale(color=("#88c", "#555"), pointsize=(2, 10))
)
为了进一步控制,你可以传递一个 Scale
对象。有几种不同类型的 Scale
,每种都有适当的参数。例如,Continuous
允许你定义输入域(norm
)、输出范围(values
)以及它们之间的映射函数(trans
),而 Nominal
允许你指定一个顺序:
(
so.Plot(diamonds, x="carat", y="price", color="carat", marker="cut")
.add(so.Dots())
.scale(
color=so.Continuous("crest", norm=(0, 3), trans="sqrt"),
marker=so.Nominal(["o", "+", "x"], order=["Ideal", "Premium", "Good"]),
)
)
自定义图例和刻度#
这些 Scale
对象还用于指定哪些值应作为刻度标签/在图例中显示,以及它们的显示方式。例如,Continuous.tick()
方法允许您控制刻度的密度或位置,而 Continuous.label()
方法允许您修改格式:
(
so.Plot(diamonds, x="carat", y="price", color="carat")
.add(so.Dots())
.scale(
x=so.Continuous().tick(every=0.5),
y=so.Continuous().label(like="${x:.0f}"),
color=so.Continuous().tick(at=[1, 2, 3, 4]),
)
)
自定义限制、标签和标题#
Plot
有许多用于简单自定义的方法,包括 Plot.label()
、Plot.limit()
和 Plot.share()
。
(
so.Plot(penguins, x="body_mass_g", y="species", color="island")
.facet(col="sex")
.add(so.Dot(), so.Jitter(.5))
.share(x=False)
.limit(y=(2.5, -.5))
.label(
x="Body mass (g)", y="",
color=str.capitalize,
title="{} penguins".format,
)
)
主题定制#
最后,Plot
通过 Plot.theme
方法支持与数据无关的主题设置。目前,此方法接受一个 matplotlib rc 参数的字典。您可以直接设置这些参数,和/或从 seaborn 的主题函数传递参数包:
from seaborn import axes_style
theme_dict = {**axes_style("whitegrid"), "grid.linestyle": ":"}
so.Plot().theme(theme_dict)
要更改所有 Plot
实例的主题,请更新 Plot.config
中的设置:
so.Plot.config.theme.update(theme_dict)