7天学会Python最佳可视化工具Seaborn(一):可视化变量间的关系

众所周知,Seaborn“可能”是Python下最友好、易用的可视化工具了,可视化效果也非常好。但是截止目前,并没有一份中文教程供广大国内Python使用者查阅学习。怎么能因为语言的问题,让大家错过这么好用的一个可视化工具呢?

思考再三,我决定花一些时间将官方的英文文档整理出来,为大家提供一份最权威的中文教程。考虑到我的时间比较碎片化,这项工作可能会在未来的几周内完成,感兴趣的朋友可以先关注和收藏下,后续的更新会在头条和我的个人博客中(www.data-insights.cn/www.data-insight.cn)发布。

今天,我整理了第一部分:如何用Seaborn可视化变量之间的关系,这一部分非常实用,基本上学习后马上就能给我们带来很多帮助。那么接下来,就开始我们的学习吧!

文章较长,建议收藏后在PC站或者我的个人博客阅读。


统计分析是这样的一个过程:尝试去理解一个数据集中变量之间的关系,以及这些关系如何受到其他变量的影响。可视化是这个过程的核心元素,因为当数据以非常恰当的方式展示出来时,我们可以非常直观地观察到某些趋势或者模式,而这些,就揭示了变量之间的关系。

在这篇教程中,我们会讨论三个seaborn函数。我们用的最多的是relplot()。这是一个图形级别的函数,它用散点图和线图两种常用的手段来表现统计关系。relplot()使用两个坐标轴级别的函数来结合了FacetGrid:

  • scatterplot():(使用kind="scatter",这是默认参数)
  • lineplot():(使用`kind="line")

正如我们所见,这些函数非常直白,因为它们用了简单易懂的展示方式,却可以呈现复杂的数据集结构。这是因为通过hue、size、style参数,我们可以在二维图形中表现出更多的变量(除了x轴和y轴的两个,还可以通过不同的方式额外展示3个变量)。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")

一、用散点图展示相关变量

散点图是统计图形中的中流砥柱。它用一系列的散点将两个变量的联合分布描绘出来,其中每个点就是一个观测样本。这种描述方式可以让我们从视觉上推断出大量信息,来判断两个变量之间是否存在某种有意义的关系。

在seaborn中,我们有数种方法可以实现散点图的绘制。最基本的一种适用于两个变量都是数值型变量的情况,它就是scatterplot()。在分类可视化教程中,我们会看到如何绘制分类数据的散点图。relplot()的默认类型(kind)就是scatterplot()(当然,我们也可以强制指定参数kind="scatter",这和不指定这一参数时效果是一样的)。

tips = sns.load_dataset("tips")
sns.relplot(x="total_bill", y="tip", data=tips);

当我们已经将散点绘制在二维的平面上时,我们还可以根据第三个变量来对这些点施以不同的颜色,从而引入一个新的维度。在seaborn中,我们用hue参数实现了这种想法,因为点的颜色是有意义的。

sns.relplot(x="total_bill", y="tip", hue="smoker", data=tips);

如果我们想要强调不同分类之间的差异,同时增加易读性,我们可以对不同的分类使用不同的标记样式:

sns.relplot(x="total_bill", y="tip", hue="smoker", style="smoker", data=tips);

我们也可以同时展示四个变量,只需要将hue和style参数单独调整到不同的分类变量即可。但是我们要谨慎使用这种方法,因为我们的眼睛对于形状的敏感性远远不如对颜色的敏感性。

sns.relplot(x="total_bill", y="tip", hue="smoker", style="time", data=tips);

在上边这个例子中,hue参数对应的变量是分类型数据,因此seaborn自动为它应用了默认的定性(分类)调色板。如果hue参数对应的变量是数值型的(可转化为浮点数的),那么默认的颜色也会随之变为连续的定量调色板。

sns.relplot(x="total_bill", y="tip", hue="size", data=tips);

上述两种情况下(分类或连续数据),我们都可以自定义我们的调色板。有很多选项可以实现这一目的。下面,我们使用cubehelix_palette()的字符串接口来定制我们的连续调色板:

sns.relplot(x="total_bill", y="tip", hue="size", 
 palette="ch:r=-.5,l=.75", data=tips);

我们还可以使用点的大小来引入第三个额外的变量:

sns.relplot(x="total_bill", y="tip", size="size", data=tips);

与matplotlib.pyplot.scatter()不同的是,这里并不是使用原始数据中的数值来为每个点选择面积大小,seaborn将原始数据归一化(正则化)到了某个范围,这个范围可以由我们来指定:

sns.relplot(x="total_bill", y="tip", size="size", sizes=(15, 200), data=tips);

更多关于如何定制不同参数来展示统计关系的示例可以在scatterplot()的API中找到。

二、使用线图表现连续性

散点图很高效,但是没有哪种可视化类型可以完美应对所有情况。事实上,我们的可视化呈现方式要适应数据集的种类以及我们想要通过图形回答的问题。

在某些数据集中,我们可能想要理解某个变量随着时间的变化规律,或者想要理解某个连续型的变量。这种情况下,线图会是一个不错的选择。在seaborn中,我们可以通过lineplot()函数或者使用带有kind="line"参数的relplot()来实现线图的绘制。

df = pd.DataFrame(dict(time=np.arange(500), value=np.random.randn(500).cumsum()))
g = sns.relplot(x="time", y="value", kind="line", data=df)
g.fig.autofmt_xdate()

由于lineplot()假设用户在大多数情况下是在尝试描绘y相对于x的函数(变化规律),因此它在绘制之前会默认先对x做一个排序。不过我们可以禁止它。

df = pd.DataFrame(np.random.randn(500, 2).cumsum(axis=0), columns=["x", "y"])
sns.relplot(x="x", y="y", sort=False, kind="line", data=df);

聚合并展示不确定性

在更多复杂的数据集中,会出现一个x轴变量对应了多个观测值(y)的情况。seaborn会默认将多个观测值聚合起来,并且将它们的均值以及95%的置信区间展示出来:

fmri = sns.load_dataset("fmri")
sns.relplot(x="timepoint", y="signal", kind="line", data=fmri);

置信区间是通过自助采样法(bootstrapping)计算的,这在遇到大型数据集时可以帮助我们节省时间。当然,我们也可以禁止它。

sns.relplot(x="timepoint", y="signal", ci=None, kind="line", data=fmri);

另一个不错的选择是,我们可以用标准差替代置信区间来展示每个时间点下观测值的分布,当数据集比较大时这一选择尤其明智。

sns.relplot(x="timepoint", y="signal", kind="line", ci="sd", data=fmri);

如果想要关闭所有的聚合操作,我们可以设置estimator=None。不过当同一时间点存在多个观测值时,我们的图会看起来有些奇怪。

sns.relplot(x="timepoint", y="signal", estimator=None, kind="line", data=fmri);

通过参数映射可视化数据子集

lineplot()与scatterplot()一样具有很强的灵活性:它也可以通过hue、size和style参数来展示额外的三个变量。它和scatterplot()使用了相同的API,因此我们不需要停下来绞尽脑汁地思考哪些参数是用来控制线条、哪些参数是用来控制散点。

使用不同的参数会决定我们的数据如何聚合。比如,增加一个具有两个水平的分类变量作为hue参数,会将我们的图形分为两条线以及两个误差带,并分别施以不同的颜色来区分数据的分类归属。

sns.relplot(x="timepoint", y="signal", hue="event", 
 kind="line", data=fmri);

我们可以增加一个style参数,以不同的线条样式来展示不同的分类:

sns.relplot(x="timepoint", y="signal", hue="region", 
 style="event", kind="line", data=fmri);

我们还可以设置不同分类的标记样式,标记样式既可以和线条样式同时设置,也可以各自单独设置。

sns.relplot(x="timepoint", y="signal", hue="region", style="event", 
 dashes=False, markers=True, kind="line", data=fmri);

跟散点图一样,我们要慎重使用这些参数来展示太多变量。有些时候它们会展示丰富的信息,但是有些时候它们会使图形太过复杂导致我们难以解析和解释它。然而当你仅打算考虑额外的一个变量时,同时修改它们的颜色和样式会很有帮助。当需要考虑到色盲人群时,将图形颜色设置为黑白色调则是一个不错的选择。

sns.relplot(x="timepoint", y="signal", hue="event", style="event",
 kind="line", data=fmri);

当我们需要应对重复测量的数据时,我们可以将不同的抽样单元(单次实验观测到的数据系列)分离开来展示,这并不需要我们使用一个语义参数(hue/style/size)。后者会导致图例看起来像一个灾难(想象一下几十个分类的情况):

sns.relplot(x="timepoint", y="signal", hue="region", 
 units="subject", estimator=None, kind="line", 
 data=fmri.query("event == 'stim'"));

与scatterplot()类似,lineplot()默认的调色板以及图例处理方式也取决于hue对应的数据是分类型还是连续数值型。

dots = sns.load_dataset("dots").query("align == 'dots'")
sns.relplot(x="time", y="firing_rate",
 hue="coherence", style="choice",
 kind="line", data=dots);

当hue参数对应的变量的数据是均匀分布在对数刻度上的(即数据分布范围非常大,比如从1到1亿),即使是连续的调色板也无法很好地应对这种情况。但是我们可以使用列表或者字典对每条线指定一个颜色。

palette = sns.cubehelix_palette(light=.8, n_colors=6)
sns.relplot(x="time", y="firing_rate",
 hue="coherence", style="choice",
 palette=palette,
 kind="line", data=dots);

或者我们可以直接修改调色板数值的正则方式:

from matplotlib.colors import LogNorm
palette = sns.cubehelix_palette(light=.7, n_colors=6)
sns.relplot(x="time", y="firing_rate", hue="coherence", style="choice",
 hue_norm=LogNorm(), kind="line", data=dots);

不要忘了我们还有个size参数,它用来修改线条的宽度:

sns.relplot(x="time", y="firing_rate", size="coherence", 
 style="choice", kind="line", data=dots);

size一般接受连续数值型变量,但是我们也可以传入分类型变量。但是要慎重考虑这种做法,因为这样比“粗线 vs. 细线”的区分难多了。然而,当数据具有非常高频的变异性时,我们使用style表现的不同线条样式会很难区分,这时使用不同的线条宽度就是一个更高效的选择了:

sns.relplot(x="time", y="firing_rate",
 hue="coherence", size="choice",
 palette=palette,
 kind="line", data=dots);

绘制时间序列数据

线图常用来描绘日期、时间相关的诗句。这些方法以原始格式传入更底层的matplotlib函数中,这样它们就可以利用matplotlib的能力来格式化日期数据。但是所有的时间格式化过程都是在matplotlib层实现的,想要知道更多实现的细节就需要去看一下matplotlib中关于这部分的文档:

df = pd.DataFrame(dict(time=pd.date_range("2017-1-1", periods=500),
 value=np.random.randn(500).cumsum()))
g = sns.relplot(x="time", y="value", kind="line", data=df)
g.fig.autofmt_xdate()

三、用更多子图展示多重关系

前边我们已经强调过,虽然我们可以在一张图中展示数个不同的语义变量(通过hue/style/size参数),但是这么做并不是总会高效。那么当我们真的很想理解在某些额外变量的影响下两个变量之间的关系有什么不同时怎么办呢?

最好的办法就是画更多的图。由于relplot()是基于FacetGrid的,因此这很容易做到。当我们想要表现出一个额外变量的影响时,我们可以不用将它赋给前边提到的语义参数(hue/style/size),而是用它来将图形“面板”化。这意味着我们会创建多个坐标轴,分别用来绘制不同的子数据集:

sns.relplot(x="total_bill", y="tip", hue="smoker", col="time", data=tips);

我们还可以同时使用col(列)和row(行)参数来展示两个变量的影响。当我们在图中增加了更多的变量时(会有更多的子图),我们可能会想要调整图形的大小。要记住在FacetGrid中,我们用height(子图高度)和aspect(高宽比)来定制每个子图的大小:

sns.relplot(x="timepoint", y="signal", hue="subject", col="region", 
 row="event", height=3, kind="line", estimator=None, data=fmri);

当我们想要检验某个具有大量水平的变量的影响时,我们可以将这个变量赋给col参数,同时我们通过col_wrap参数设置每行达到多少列就换行:

sns.relplot(x="timepoint", y="signal", hue="event", style="event",
 col="subject", col_wrap=5,
 height=3, aspect=.75, linewidth=2.5,
 kind="line", data=fmri.query("region == 'frontal'"));

这种常被叫做“格子图”或“small-multiples”的可视化方式,非常高效,因为它们呈现数据的方式使得我们很容易同时发现整体的模式以及不同模式之间的偏差。当你需要利用scatterplot()和relplot()的灵活性来表现更多信息时,一定要记住,多幅简单的图通常比一幅复杂的图更加高效。

举报
评论 0