Seaborn中文教程(五):通过“多图网格”结构化展示多维数据

当探索具有中等数量(不多不少的意思……)维度的数据集时,一个很好的方式是基于不同的子数据集构建不同的实例,并将它们以网格的方式组织在一张图之中。这种技术有时被称为“lattice”或“trellis”(大概是格子图、网格图),这跟“small multiples”的概念类似(多张更小的子图)。它能帮助我们快速从复杂的数据中提取大量信息。matplotlib对于创建带有多个坐标轴(每个坐标轴体系意味着一张子图)的图形有着良好的支持,seaborn基于这些来直接地将图形的排布结构与数据集的结构联结起来。

要利用这些特性,我们的数据集应该保存在一个pandas DataFrame中,并且应该是Hadley Whickam口中的“tidy data”格式。简短来说,就是我们的dataframe对象中,每一行是一个观测样本,每一列是一个变量。

对于一些更高级的应用来说,我们可以直接使用这篇教程中讨论的一些对象来获得最大的灵活性。一些seaborn函数(如lmplot()/catplot()/pairplot())也隐式地使用了这些对象。很多seaborn函数是坐标轴级别的,它们仅仅针对某个特定的matplotlib Axes来绘图,而不会修改图形的属性;而在这篇教程中即将讨论到的这些方法是更高级别的函数,在被调用时,它们会创建一个新的图形,而且一般情况下对于创建过程更加严格。在某些案例中,这些函数和他们依赖的类所需要输入的参数依赖于不同的接口属性,比如在lmplot()中我们通过设置高度和宽高比(heightaspect)来控制每个子图(facet)的大小,而非直接指定整个图形的大小。这些函数在调用后都会返回这个图形对象,而且大多数对象都提供了非常方便的方法来改变绘图的方式,这些方法往往更加抽象和简单。

import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="ticks")

一、条件式多图

当我们想要基于不同的数据子集来展示某个变量的分布或者多个变量之间的关系时,FacetGrid类会提供很大的帮助。一个FacetGrid图可以从三个维度来构建:rowcolhue。前两个与它返回的坐标轴数组有着之间的关联;我们可以把hue变量理解为第三个维度,就像长、宽和高一样,只不过在这里我们是用不同的颜色来体现它的。

在使用FacetGrid时,我们会通过一个pandas DataFrame以及控制图形网格的行、列和颜色的变量名称来初始化一个对象。这些维度变量(控制行、列和颜色的变量)应该是分类变量或者离散变量,然后这些变量的不同水平组合起来就构成了整个图形的每一个子图(facet,在这里可以理解为我们维度拆解的最小粒度)。比如说我们想要检验一下tips数据集中午餐和晚餐的差异。

另外,relplot()/catplot()/lmplot()内置了FacetGrid对象,绘图完成后他们都会返回这个对象,这样我们就可以进行更多的调整。

tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, col="time")
image

传入数据和维度变量名称会初始化一个网格,其中生成了基于matplotlib的图形和坐标轴,但是不会在这些坐标轴中画任何东西(因为我们还没告诉它们画什么)。

在这些网格中画图的主要方式是使用FacetGrid.map()方法。我们需要告诉它使用哪个绘图函数以及使用哪些(哪个)变量。我们用直方图来看下两个数据子集中小费的分布情况:

g = sns.FacetGrid(tips, col="time")
g.map(plt.hist, "tip");
image

这一函数的目标是一步到位地提供一幅完整的成品图,它在完成绘图后还会对每个坐标轴添加注释。想要生成一个战士变量关系的图,只需要传递更多的变量名称进去。我们还可以提供关键字参数,它会将他们传递给绘图函数:

g = sns.FacetGrid(tips, col="sex", hue="smoker")
g.map(plt.scatter, "total_bill", "tip", alpha=.7)
g.add_legend();
image

有很多选项可以传递给FacetGrid的构造函数,用以控制网格的样式:

g = sns.FacetGrid(tips, row="smoker", col="time", margin_titles=True)
g.map(sns.regplot, "size", "total_bill", color=".3", fit_reg=False, x_jitter=.1);
image

需要注意的是,margin_title参数并没有被matplotlib API提供正式支持,在某些案例中或许并不可用。比如说当图外边有图例时,它是不可用的。

图的大小是通过每张子图的高度和宽高比来控制的:

g = sns.FacetGrid(tips, col="day", height=4, aspect=.5)
g.map(sns.barplot, "sex", "total_bill");
/anaconda3/lib/python3.7/site-packages/seaborn/axisgrid.py:715: UserWarning: Using the barplot function without specifying `order` is likely to produce an incorrect plot.
  warnings.warn(warning)
image

它默认会从DataFrame中推导分类的顺序。如果用来控制某个方面(facet:维度/轴/角度,看哪个概念能帮助你理解它,你就用哪个名字)的变量是分类变量,那么分类变量的顺序就会被使用。否则,seaborn会使用这些分类在数据集中出现的先后顺序。当然,我们完全可以通过*_order参数直接指定某个维度变量的顺序:

ordered_days = tips.day.value_counts().index
g = sns.FacetGrid(tips, row="day", row_order=ordered_days, height=1.7, aspect=4)
g.map(sns.distplot, "total_bill", hist=False, rug=True);
image

我们可以指定某个seaborn调色板,也可以通过字典将hue变量中的每个分类与其对应的matplotlib颜色传递给函数(这样就可以随心所以使用大量的matplotlib支持的色彩):

pal = dict(Lunch="seagreen", Dinner="gray")
g = sns.FacetGrid(tips, hue="time", palette=pal, height=5)
g.map(plt.scatter, "total_bill", "tip", s=50, alpha=.7, linewidth=.5, edgecolor="white")
g.add_legend();
image

我们还可以控制hue变量的不同水平展示出来的其他样式(方面),这些在黑白色调下(比如黑白印刷图)对于提高图形的可读性尤其有用。我们需要将一个字典传递给hue_kws参数,在这个字典中,key(字典的键)是绘图函数的关键字参数名称;而value(字典的值)则是一个列表,用于存储关键字参数的取值,其中每个取值对应了hue变量的一个水平。

g = sns.FacetGrid(tips, hue="sex", palette="Set1", height=5, hue_kws={"marker": ["^", "v"]})
g.map(plt.scatter, "total_bill", "tip", s=100, linewidth=.5, edgecolor="white")
g.add_legend();
image

如果某个维度变量(用于col/row/hue参数的变量,之后不再说明)具有非常多的水平(level:取值),我们可以把它们分布到不同的列,然后把它们“折叠”到不同的行中。当我们使用这种操作时,我们不能设置row变量。

attend = sns.load_dataset("attention").query("subject <= 12")
g = sns.FacetGrid(attend, col="subject", col_wrap=4, height=2, ylim=(0, 10))
g.map(sns.pointplot, "solutions", "score", color=".3", ci=None);
/anaconda3/lib/python3.7/site-packages/seaborn/axisgrid.py:715: UserWarning: Using the pointplot function without specifying `order` is likely to produce an incorrect plot.
  warnings.warn(warning)
image

当我们已经使用FacetGrid.map()完成了绘图,我们可能还想对图形的某些方面做些调整。FacetGrid支持很多在更高层级调整图形的方法,最常用的是FacetGrid.set(),还有一些更加具体的方法比如FacetGrid.set_axis_labels(),如图:

with sns.axes_style("white"):
    g = sns.FacetGrid(tips, row="sex", col="smoker", margin_titles=True, height=2.5)
g.map(plt.scatter, "total_bill", "tip", color="#334488", edgecolor="white", lw=.5);
g.set_axis_labels("Total bill (US Dollars)", "Tip");
g.set(xticks=[10, 30, 50], yticks=[2, 6, 10]);
g.fig.subplots_adjust(wspace=.02, hspace=.02);
image

想要做更多定制的话,我们可以直接操作更底层的FigureAxes对象,它们存储在FacetGrid.figFacetGrid.axes中,其中,FacetGrid.axes是一个二维数组。假如我们没有指定行和列,那么我们也可以直接使用FacetGrid.ax来操作那个唯一的坐标轴:

g = sns.FacetGrid(tips, col="smoker", margin_titles=True, height=4)
g.map(plt.scatter, "total_bill", "tip", color="#338844", edgecolor="white", s=50, lw=1)
for ax in g.axes.flat:
    ax.plot((0, 50), (0, .2 * 50), c=".2", ls="--")
g.set(xlim=(0, 60), ylim=(0, 14));
image

二、使用自定义绘图函数

使用FacetGrid时,我们并非只能使用现成的matplotlibseaborn绘图函数。不过,如果想要正常工作,我们的自定义函数需要满足一下规则:

  1. 它必须是一个坐标轴级别的函数(仅对当前活跃的坐标轴进行绘图)。在matplotlib.pyplot的命名空间中存在的函数都是符合要求的,我们也可以调用plt.gca()(get current axes)来获取符合要求的坐标轴。
  2. 它必须支持以位置参数的方式接受数据传入。FacetGrid内部会把我们传递给FacetGrid.map()的多组序列数据分别传递给这些位置参数。
  3. 它必须支持接受关键字参数colorlabel,另外,理想情况下,它还会利用这两个参数做一些有用的事情。在大多数情况下,接受一个关键字参数(**kwargs)的字典并传递给底层的绘图函数是很容易的。

我们来看一个符合最低条件的例子,这个绘图函数仅接受一组来自某个分类(子数据集)的向量型数据:

from scipy import stats

def quantile_plot(x, **kwargs):
    qntls, xr = stats.probplot(x, fit=False)
    plt.scatter(xr, qntls, **kwargs)
    
g = sns.FacetGrid(tips, col="sex", height=4)
g.map(quantile_plot, "total_bill");
image

如果我们想要绘制一个二元图,我们需要让函数接受两组数据,且x轴对应的数据在前边,y轴对应的数据在后边:

def qqplot(x, y, **kwargs):
    _, xr = stats.probplot(x, fit=False)
    _, yr = stats.probplot(y, fit=False)
    plt.scatter(xr, yr, **kwargs)

g = sns.FacetGrid(tips, col="smoker", height=4)
g.map(qqplot, "total_bill", "tip");
image

由于plt.scatter可以接受关键字参数colorlabel并且能正确处理它们,所以我们可以毫不费力地增加一个hue参数(因为它的原理就是给不同的分类数据传入不同的颜色参数):

g = sns.FacetGrid(tips, hue="time", col="sex", height=4)
g.map(qqplot, "total_bill", "tip")
g.add_legend();
image

我们还可以通过关键字参数控制额外的美学设计来区分hue变量的不同水平:

g = sns.FacetGrid(tips, hue="time", col="sex", height=4,
                  hue_kws={"marker": ["s", "D"]})
g.map(qqplot, "total_bill", "tip", s=40, edgecolor="w")
g.add_legend();
image

当然,有时我们完全不想处理colorlabel。这种情况下我们需要显式地接受他们,然后用自己的逻辑去处理他们。比如下边这个使用plt.hexbin的例子,在与FacetGridAPI搭配的过程中。如果我们没有手动调整颜色处理方式的话,它们的表现会不太好:

def hexbin(x, y, color, **kwargs):
    cmap = sns.light_palette(color, as_cmap=True)
    plt.hexbin(x, y, gridsize=15, cmap=cmap, **kwargs)

with sns.axes_style("white"):
    g = sns.FacetGrid(tips, hue="time", col="time", height=4)
g.map(hexbin, "total_bill", "tip", extent=[0, 50, 0, 10]);
image

三、展示变量间的两两(成对)关系

PairGrid也支持我们以同样的方式快速绘制多个子图。在PairGrid中,每行每列都被分配给一个不同的变量,所以最后生成的图片可以展示数据集中所有的成对关系。这种风格的图形有时被称作“散点图矩阵”,因为散点图是表现两两关系最常用的方法。不过PairGrid并不会仅仅局限于散点图。

了解FacetGridPairGrid之间的区别非常重要。在FacetGrid中,每张图表现的都是同样的变量关系,只是每张图对应着不同的数据子集,数据子集的划分是由我们指定的维度变量决定的(colrowhue),这些变量相互交叉后产生一系列最小粒度的数据子集,每个子集就对应了一张子图,也就是说,不管是多少行、多少列还是多少颜色,它们都对应着这些维度变量的取值(水平)。而在PairGrid中,每张子图都代表了不同的两个变量间的关系(当然,上三角和下三角会有镜像的关系,因为它们相当于互换了x轴和y轴)。PairGrid可以对于我们数据集中的变量关系提供一个非常快速、整体(不深入)的总结。

它的基本使用方法与FacetGrid非常类似。首先我们初始化一个网格,然后把绘图函数传递给map方法,它会将我们的绘图函数在所有的子图中调用。与PairGrid对应的函数是pairplot(),它失去了一些灵活性,但是让我们的绘图更加快捷。

iris = sns.load_dataset("iris")
g = sns.PairGrid(iris)
g.map(plt.scatter);
image

我们可以在对角线上用不同的函数来展示单变量的分布情况。不过需要注意的是,轴上的刻度与分桶计数或者密度没有关系(因为我们已经用轴刻度去展示数据的取值了)。

g = sns.PairGrid(iris)
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter);
image

在这种图中,我们常常会对属于不同分类的样本标记上不同的颜色。比如,iris数据集中有三种不同的鸢尾花,其中每个样本都有4个特征,因此我们可以看下它们的区别在哪里。

g = sns.PairGrid(iris, hue="species")
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter)
g.add_legend();
image

默认情况下,数据集中所有的数值型变量都会被使用,不过我们也可以仅选用特定的列:

g = sns.PairGrid(iris, vars=["sepal_length", "sepal_width"], hue="species")
g.map(plt.scatter);
image

我们也可以分别在上三角和下三角中使用不同的函数,用以强调关系的不同角度。

g = sns.PairGrid(iris)
g.map_upper(plt.scatter)
g.map_lower(sns.kdeplot)
g.map_diag(sns.kdeplot, lw=3, legend=False);
image

事实上,这种对称的方形网格矩阵只是一个特例,我们可以在行和列上分别使用不同的变量。

g = sns.PairGrid(tips, y_vars=["tip"], x_vars=["total_bill", "size"], height=4)
g.map(sns.regplot, color=".3")
g.set(ylim=(-1, 11), yticks=[0, 5, 10]);
image

当然,设计(美学)属性都是可以配置的。比如,我们可以使用不同的调色板、可以给绘图函数传递关键字参数。

g = sns.PairGrid(tips, hue="size", palette="GnBu_d")
g.map(plt.scatter, s=50, edgecolor="white")
g.add_legend();
image

PairGrid很灵活,但是想要快速观察数据特点的话,使用pairplot()更容易。这个函数默认使用散点图和直方图,但是也支持一些其他类型(现在我们还可以在非对角位置上画回归图、在对角位置上画KDE图),未来还会支持更多。

sns.pairplot(iris, hue="species", height=2.5);
image

pairplot()中我们也可以通过关键字参数调整设计风格(美学),并且它会返回一个PairGrid对象用于更多的调整。

g = sns.pairplot(iris, hue="species", palette="Set2", diag_kind="kde", height=2.5)
image

推荐阅读更多精彩内容