许多数据集含有多个定量变量(数值型变量),而我们分析的目的往往是将他们关联起来。我们曾讨论过通过两个变量的联合分布来实现这一点。然而,使用统计模型来为两组带有噪声数据的观测值评估出一个简单的关系可以是非常有用的。这一章节我们讨论的函数将会在线性回归的框架下实现这种预测。
seaborn
中的回归图主要是为了在EDA(探索数据分析)阶段为发掘数据中存在的规律提供一些视觉指引,也就是说,seaborn
本身并非是一个用于统计分析的库。想要得到关于回归模型拟合效果的一些量化指标,你需要使用statsmodels
库。seaborn
的终极目标就是让我们通过可视化快速、轻易地探索数据,毕竟对于探索数据来说,可视化的重要性不比得到一个统计表格低。
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(color_codes=True)
tips = sns.load_dataset("tips")
一、绘制线性回归模型
seaborn
主要通过两个函数来展示通过回归得到的线性关系,regplot()
和lmplot()
。它们紧密相关,而且共享大多数的核心功能。但是弄清楚他们的区别非常重要,这样我们就可以在针对特定工作时快速判断哪个工具更适合。
在最基本的调用过程中,他们都会画出关于x、y两个变量的散点图,同时用数据拟合一个y ~ x
的模型出来,并将对应的直线和95%的置信区间绘制出来:
sns.regplot(x="total_bill", y="tip", data=tips);
sns.lmplot(x="total_bill", y="tip", data=tips);
我们注意到除了图片的形状略有差异,其他地方都是一致的。需要知道的是,他们之间最主要的区别在于regplot()
中的x, y
参数接受多种数据类型,包括numpy
数组、pandas
序列(Series
),或者将pandas DataFrame
传递给data
参数。作为对比,lmplot()
的data
参数是不能为空的,同时x
和y
参数必须以字符串形式指定。这种数据格式(这里是指regplot()
支持而lmplot()
不支持的类似一维数组的数据格式)被称为“long-form data”或“tidy data”。除了这一输入格式的灵活性以外,regplot()
仅提供了lmplot()
特性的一部分,所以我们用后者来演示它们的使用。
seaborn
支持其中一个变量属于离散变量的情况,不过这种数据集产生的散点图效果往往一般:
sns.lmplot(x="size", y="tip", data=tips);
增加一些随机的偏移量会让这些分布看起来更清晰,需要注意的是这些偏移量仅仅会影响散点图的效果,不会对拟合的回归线产生干扰:
sns.lmplot(x="size", y="tip", data=tips, x_jitter=.05);
另一个选择是将每个离散桶内的观测值隐藏起来,用代表集中趋势的统计量以及置信区间作为替代:
sns.lmplot(x="size", y="tip", data=tips, x_estimator=np.mean);
二、拟合不同的模型
简单线性回归的模型非常容易拟合,然而它并不适用于所有数据集。Anscombe's quartet
数据集展示了一些例子,在这些例子中简单线性回归提供了变量间关系一致的估计,但是却与我们视觉上的直观判断存在一些差异。比如,在第一个例子中,线性回归是一个很不错的模型:
anscombe = sns.load_dataset("anscombe")
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'I'"),
ci=None, scatter_kws={"s": 80});
第二个数据集中有着同样的线性关系,但是我们瞬间就能判断线性回归并不是一个最佳的模型:
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
ci=None, scatter_kws={"s": 80});
在展示这种更高阶的关系时,lmplot()
和regplot()
可以通过拟合多项式回归模型来应对数据集中的一些简单的非线性趋势:
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
order=2, ci=None, scatter_kws={"s": 80});
另一个问题是由异常观测点导致的,这些观测点明显偏离了我们想要得到的主要趋势关系:
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'III'"),
ci=None, scatter_kws={"s": 80});
在异常观测值存在时,我们可以拟合一个鲁棒回归(稳定回归),它使用了不同的损失函数,对较大的残差做了降权:
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'III'"),
robust=True, ci=None, scatter_kws={"s": 80});
当因变量y
是二元变量时,简单线性回归会提供一个不太有说服力的预测:
tips["big_tip"] = (tips.tip / tips.total_bill) > .15
sns.lmplot(x="total_bill", y="big_tip", data=tips,
y_jitter=.03);
这种情况下的解决方案是拟合一个逻辑回归模型,此时回归线的含义是在给定的x
下y = 1
的概率:
sns.lmplot(x="total_bill", y="big_tip", data=tips,
logistic=True, y_jitter=.03);
要注意的是,相对于简单线性回归,逻辑回归估算具有相当高的计算复杂度(稳健回归也是)。考虑到围绕着回归线的置信区间是通过自主抽样过程计算的(也会花费大量时间),在拟合逻辑回归等模型时最好把置信区间的计算给关掉(ci=None
)。
一个完全不同的方法是通过局部加权回归散点平滑法(LOWESS)拟合一个非参数回归模型。这种方法有最少的假设前提,不过它也会耗费大量计算资源,因此置信区间并不会被计算:
sns.lmplot(x="total_bill", y="tip", data=tips, lowess=True);
residplot()
函数可以检查一个简单的回归模型对于某个数据集是否合适。它先拟合一个简单线性回归模型并移除它,然后将每个观测点与预测值的残差画出来。理想情况下,这些残差应该随机地分布在x轴上下方:
sns.residplot(x="x", y="y", data=anscombe.query("dataset == 'I'"),
scatter_kws={"s": 80});
如果残差的分布具有某种规律,那说明简单线性回归或许并不是一个好的选择:
sns.residplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
scatter_kws={"s": 80});
三、考虑其他变量的影响
上边我们展示了很多探索一对变量之间关系的可视化方式。然而在很多情况下,“两个变量之间的关系如何受到第三个变量的影响”是一个更加有趣的问题。在这里regplot()
和lmplot()
的差异就出现了:regplot()
只能展示简单的关系,而lmplot()
则融合了regplot()
和FacetGrid
。因此,lmplot()
可以轻易地探索最多三个额外变量带来的交互作用。
分离一个关系最好的方式就是将不通水平的数据绘制在同一个坐标轴体系内,并且用颜色来区分他们:
sns.lmplot(x="total_bill", y="tip", hue="smoker", data=tips);
除了颜色,我们还可以通过标记样式来区分不同的水平(分类),这样就可以更好的应对黑白色调或色盲读者。我们可以控制调色板:
sns.lmplot(x="total_bill", y="tip", hue="smoker", data=tips,
markers=["o", "x"], palette="Set1");
我们可以通过更多的子图(行、列)来引入新的变量:
sns.lmplot(x="total_bill", y="tip", hue="smoker", col="time", data=tips);
sns.lmplot(x="total_bill", y="tip", hue="smoker",
col="time", row="sex", data=tips);
四、控制图形大小和形状
前边我们提到regplot()
和lmplot()
所创建的图形在默认大小和形状上不一致。这是因为regplot()
是一个坐标轴级别的绘图函数,这意味着我们可以在一张图中生成多个子图,随心所欲地控制它们的大小和位置。如果我们没有指定坐标轴参数(ax
)的haul,它会默认使用当前活跃的坐标轴,这就是为什么它创建的图形有着和其他matplotlib
函数绘制的图形一样的默认大小和形状。想要控制它们的话,我们需要自己创建一个图形对象:
f, ax = plt.subplots(figsize=(5, 6))
sns.regplot(x="total_bill", y="tip", data=tips, ax=ax);
不同的是,lmplot()
绘制的图形是通过FacetGrid
接口中的height
和aspect
(宽高比)参数控制大小形状的,这些指定的大小和比例是针对每张子图而非整张图的:
sns.lmplot(x="total_bill", y="tip", col="day",
data=tips, col_wrap=2, height=3);
sns.lmplot(x="total_bill", y="tip", col="day",
data=tips, aspect=.5);
五、在其他背景中添加回归图
有一些seaborn
函数会在其他更大、更复杂的图形背景中使用regplot()
,第一个就是我们在数据分布教程中提到的jointplot()
函数。除了我们之前讨论过的其他样式,jointplot()
可以通过kind="reg"
来调用regplot()
绘制线性关系:
sns.jointplot(x="total_bill", y="tip", data=tips, kind="reg");
给pairplot()
传入kind="reg"
参数则会融合regplot()
与PairGrid
来展示变量间的线性关系。注意这里和lmplot()
的区别,lmplot()
绘制的行(或列)是将一个变量的多个水平(分类、取值)展开,而在这里,PairGrid
则是绘制了不同变量之间的线性关系:
sns.pairplot(tips, x_vars=["total_bill", "size"], y_vars=["tip"],
height=5, aspect=.8, kind="reg");
与lmplot()
类似,但是与jointplot()
不同,额外的分类变量是使用hue
参数传递给pairplot()
来处理的:
sns.pairplot(tips, x_vars=["total_bill", "size"], y_vars=["tip"],
hue="smoker", height=5, aspect=.8, kind="reg");