>>> from env_helper import info; info()
页面更新时间: 2024-01-06 20:45:56
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

5.7. 可视化数据分布

在对数据进行任何分析或建模时,一个早期的步骤应该是了解变量的分布方式。 分布可视化技术可以为许多重要问题提供快速答案。观测值涵盖多大范围?他们的核心趋势是什么? 它们是否严重偏向一个方向?有证据表明是双峰的吗?是否存在显著的异常值?这些问题的答案是否因其他变量定义的子集而异?

distributions 模块包含多个函数,旨在回答此类问题。 轴级函数为histplot()kdeplot()ecdfplot()rugplot() 。它们在图形级displot()jointplot()pairplot() 函数中组合在一起。

有几种不同的方法可以可视化分布,每种方法都有其相对的优点和缺点。了解这些因素非常重要,这样您就可以为您的特定目标选择最佳方法。

5.7.1. 绘制单变量直方图

也许可视化分布最常用的方法是直方图。这是displot()中的默认方法, 它使用与histplot()相同的底层代码。 直方图是一种柱状图,其中表示数据变量的轴被分成一组离散的桶,每个桶内的观测值计数使用相应柱的高度表示:

>>> import seaborn as sns
>>>
>>> sns.set_theme()
>>>
>>> penguins = sns.load_dataset("penguins")
>>> sns.displot(penguins, x="flipper_length_mm")
<seaborn.axisgrid.FacetGrid at 0x7f08ff2c5e50>
_images/sec07_distributions_2_1.png

这个图立即提供了关于flipper_length_mm变量的一些见解。例如,我们可以看到最常见的鳍长约为195毫米,但分布是双峰的,所以这个数字不能很好地代表数据。

选择箱子大小

箱子的大小是一个重要的参数,使用错误的箱子大小可以通过模糊数据的重要特征或通过创建随机可变性的明显特征来误导。默认情况下,displot()/histplot()根据数据的方差和观测值的数量选择默认的bin大小。但是您不应该过度依赖这种自动方法,因为它们依赖于对数据结构的特定假设。这总是明智的,以检查您的印象分布是一致的跨越不同的bin尺寸。要直接选择大小,请设置binwidth参数:

>>> sns.displot(penguins, x="flipper_length_mm", binwidth=3)
<seaborn.axisgrid.FacetGrid at 0x7efd66977d10>
_images/sec07_distributions_4_1.png

在其他情况下,指定条柱的数量可能更有意义,而不是它们的大小:

>>> sns.displot(penguins, x="flipper_length_mm", bins=20)
<seaborn.axisgrid.FacetGrid at 0x7efd5e90fd50>
_images/sec07_distributions_6_1.png

默认值失败的一个示例是变量采用相对较少的整数值。在这种情况下,默认图格宽度可能太小,从而在分布中产生尴尬的间隙:

>>> tips = sns.load_dataset("tips")
>>> sns.displot(tips, x="size")
<seaborn.axisgrid.FacetGrid at 0x7efd5e963c10>
_images/sec07_distributions_8_1.png

一种方法是通过将数组传递给 bins

>>> sns.displot(tips, x="size", bins=[1, 2, 3, 4, 5, 6, 7])
<seaborn.axisgrid.FacetGrid at 0x7efd5e6dca10>
_images/sec07_distributions_10_1.png

这也可以通过设置discrete=True 来实现,该设置选择表示数据集中唯一值的条柱分隔符,其条形图以其对应值为中心。

>>> sns.displot(tips, x="size", discrete=True)
<seaborn.axisgrid.FacetGrid at 0x7efd5e515250>
_images/sec07_distributions_12_1.png

还可以使用直方图的逻辑可视化分类变量的分布。为分类变量自动设置离散条柱,但稍微“缩小”条形以强调轴的分类性质也可能有所帮助:

>>> sns.displot(tips, x="day", shrink=.8)
<seaborn.axisgrid.FacetGrid at 0x7efd5e3ed010>
_images/sec07_distributions_14_1.png

其他变量的条件作用

一旦您了解了变量的分布,下一步通常是询问该分布的特征在数据集中的其他变量之间是否不同。例如,是什么解释了我们上面看到的鳍长度的双峰分布?displot()histplot()通过hue语义提供对条件子集的支持。将一个变量赋值给hue将为每个变量的唯一值绘制一个单独的直方图,并通过颜色来区分它们:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species")
<seaborn.axisgrid.FacetGrid at 0x7efd5e440290>
_images/sec07_distributions_16_1.png

默认情况下,不同的直方图相互“分层”,在某些情况下,它们可能难以区分。一种选择是将直方图的视觉表示从条形图更改为“阶梯”图:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", element="step")
<seaborn.axisgrid.FacetGrid at 0x7efd5e6de350>
_images/sec07_distributions_18_1.png

或者,它们可以“堆叠”或垂直移动,而不是分层每个条形。在此图中,完整直方图的轮廓将仅与单个变量的图匹配:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", multiple="stack")
<seaborn.axisgrid.FacetGrid at 0x7efd5e1c9e10>
_images/sec07_distributions_20_1.png

堆叠直方图强调了变量之间的部分-整体关系,但它可能会掩盖其他特征(例如,很难确定 Adelie 分布的模式)。另一种选择是“躲避”条形图,将其水平移动并减小其宽度。这确保了没有重叠,并且杆在高度方面保持可比性。但只有当分类变量具有少量级别时,它才有效:

>>> sns.displot(penguins, x="flipper_length_mm", hue="sex", multiple="dodge")
<seaborn.axisgrid.FacetGrid at 0x7efd5e33f690>
_images/sec07_distributions_22_1.png

因为displot()是一个图形级函数,并且绘制到FacetGrid上,所以也可以通过将第二个变量赋值为colrow而不是(或添加)hue来在单独的子图中绘制每个单独的分布。这很好地代表了每个子集的分布,但这使得进行直接比较变得更加困难:

>>> sns.displot(penguins, x="flipper_length_mm", col="sex")
<seaborn.axisgrid.FacetGrid at 0x7efd5dfe3090>
_images/sec07_distributions_24_1.png

这些方法都不是完美的,我们很快就会看到一些比直方图更适合比较任务的替代方法。

规范化直方图统计

在此之前,需要注意的另一点是,当子集具有不等数量的观测值时,根据计数比较它们的分布可能并不理想。一种解决方案是使用stat参数将计数规范化:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", stat="density")
<seaborn.axisgrid.FacetGrid at 0x7efd5dfc65d0>
_images/sec07_distributions_26_1.png

但是,默认情况下,归一化将应用于整个分布,因此这只会重新缩放条形的高度。通过设置common_norm=False ,每个子集将独立归一化:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", stat="density", common_norm=False)
<seaborn.axisgrid.FacetGrid at 0x7efd5df10490>
_images/sec07_distributions_28_1.png

密度归一化对条形进行缩放,使其面积之和为 1。因此,密度轴无法直接解释。另一种选择是将条形规范化为它们的高度总和为 1。当变量是离散的时,这是最有意义的,但它是所有直方图的一个选项:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", stat="probability")
<seaborn.axisgrid.FacetGrid at 0x7efd5df86810>
_images/sec07_distributions_30_1.png

5.7.2. 核密度估计

直方图旨在通过对观测值进行分箱和计数来近似生成数据的基础概率密度函数。核密度估计 (KDE) 为同一问题提供了不同的解决方案。KDE 图不是使用离散条柱,而是使用高斯核对观测值进行平滑处理,从而产生连续密度估计:

>>> sns.displot(penguins, x="flipper_length_mm", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7efd5da37090>
_images/sec07_distributions_32_1.png

选择平滑带宽

与直方图中的 bin 大小非常相似,KDE 准确表示数据的能力取决于平滑带宽的选择。过度平滑的估计可能会擦除有意义的特征,但平滑不足的估计可能会掩盖随机噪声中的真实形状。检查估计值鲁棒性的最简单方法是调整默认带宽:

>>> sns.displot(penguins, x="flipper_length_mm", kind="kde", bw_adjust=.25)
<seaborn.axisgrid.FacetGrid at 0x7efd5d7cdd50>
_images/sec07_distributions_34_1.png

请注意,窄带宽如何使双峰更加明显,但曲线却不那么平滑。相比之下,较大的带宽几乎完全掩盖了双模态:

>>> sns.displot(penguins, x="flipper_length_mm", kind="kde", bw_adjust=2)
<seaborn.axisgrid.FacetGrid at 0x7efd5d77d7d0>
_images/sec07_distributions_36_1.png

调节其他变量

与直方图一样,如果分配一个hue变量,则将为该变量的每个级别计算单独的密度估计值:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7efd5d6aabd0>
_images/sec07_distributions_38_1.png

在许多情况下,分层的 KDE 比分层的直方图更容易解释,因此它通常是比较任务的不错选择。然而,许多用于解析多个发行版的相同选项也适用于 KDE:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", kind="kde", multiple="stack")
<seaborn.axisgrid.FacetGrid at 0x7efd5d71cbd0>
_images/sec07_distributions_40_1.png

请注意,默认情况下,堆积图是如何填充每条曲线之间的区域的。也可以填充单个或分层密度的曲线,尽管默认的 alpha 值(不透明度)会有所不同,以便更容易解析单个密度。

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", kind="kde", fill=True)
<seaborn.axisgrid.FacetGrid at 0x7efd5d6b49d0>
_images/sec07_distributions_42_1.png

核密度估计陷阱

KDE 绘图有很多优点。数据的重要特征很容易辨别(集中趋势、双峰、偏斜),并且它们提供了子集之间的轻松比较。但在某些情况下,KDE 对底层数据的表示能力很差。这是因为 KDE 的逻辑假设底层发行版是平滑且无界的。这种假设可能失败的一种方式是,当变量反映一个自然有界的量时。如果存在接近边界的观测值(例如,变量的小值不能为负),KDE 曲线可能会扩展到不切实际的值:

>>> sns.displot(tips, x="total_bill", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7efd5d764ad0>
_images/sec07_distributions_44_1.png

使用cut参数可以部分避免这种情况,该参数指定曲线在极端数据点之外应该延伸多远。但这只影响曲线的绘制位置;密度估计仍然会在没有数据存在的范围内平滑,导致它在分布的极端情况下人为地降低:

>>> sns.displot(tips, x="total_bill", kind="kde", cut=0)
<seaborn.axisgrid.FacetGrid at 0x7efd5d5ca010>
_images/sec07_distributions_46_1.png

KDE 方法对于离散数据或数据自然连续但特定值被过度表示时也失败了。需要记住的重要一点是,KDE 将始终显示平滑的曲线,即使数据本身并不平滑。例如,考虑以下钻石重量分布:

>>> diamonds = sns.load_dataset("diamonds")
>>> sns.displot(diamonds, x="carat", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7f08f531f010>
_images/sec07_distributions_48_1.png

虽然 KDE 表明在特定值周围存在峰值,但直方图揭示了一个更加锯齿状的分布:

>>> sns.displot(diamonds, x="carat")
<seaborn.axisgrid.FacetGrid at 0x7efd5d563910>
_images/sec07_distributions_50_1.png

作为一种妥协,可以将这两种方法结合起来。在直方图模式下,displot()(与histplot()一样)可以选择包括平滑的KDE曲线(注意kde =True,而不是kind="kde"):

>>> sns.displot(diamonds, x="carat", kde=True)
<seaborn.axisgrid.FacetGrid at 0x7efd5d419990>
_images/sec07_distributions_52_1.png

5.7.3. 经验累积分布

可视化分布的第三个选项是计算“经验累积分布函数”(ECDF)。此图在每个数据点中绘制一条单调递增的曲线,使曲线的高度反映具有较小值的观测值的比例:

>>> sns.displot(penguins, x="flipper_length_mm", kind="ecdf")
<seaborn.axisgrid.FacetGrid at 0x7efd5cdd0450>
_images/sec07_distributions_54_1.png

ECDF图有两个关键优点。与直方图或 KDE 不同,它直接表示每个数据点。这意味着无需考虑条柱大小或平滑参数。此外,由于曲线是单调递增的,因此非常适合比较多个分布:

>>> sns.displot(penguins, x="flipper_length_mm", hue="species", kind="ecdf")
<seaborn.axisgrid.FacetGrid at 0x7efd5ce4a550>
_images/sec07_distributions_56_1.png

ECDF 图的主要缺点是它不如直方图或密度曲线直观地表示分布形状。考虑鳍状肢长度的双峰性如何在直方图中立即显现出来,但要在 ECDF 图中查看它,您必须寻找不同的斜率。然而,通过练习,你可以通过检查 ECDF 来学习回答有关分布的所有重要问题,这样做可能是一种有效的方法。

5.7.4. 可视化双变量分布

到目前为止,所有的例子都考虑了单变量分布:单个变量的分布,可能以分配给hue的第二个变量为条件。然而,将第二个变量赋值给y,将绘制一个双变量分布:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm")
<seaborn.axisgrid.FacetGrid at 0x7efd5ce3eb10>
_images/sec07_distributions_58_1.png

二元直方图将数据装箱到平铺绘图的矩形内,然后使用填充颜色(类似于heatmap() )显示每个矩形内的观测值计数。类似地,双变量 KDE 图使用二维高斯平滑 (x, y) 观测值。然后,默认表示显示 2D 密度的等值线:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7f08ff2ff5d0>
_images/sec07_distributions_60_1.png

分配一个hue变量将使用不同的颜色绘制多个热图或轮廓集。对于双变量直方图,只有在条件分布之间有最小的重叠时,这才会有效:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", hue="species")
<seaborn.axisgrid.FacetGrid at 0x7f08ff2eedd0>
_images/sec07_distributions_62_1.png

双变量 KDE 图的等值线方法更适合评估重叠,尽管等值线过多的图可能会很忙:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", hue="species", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x7f08f6f42cd0>
_images/sec07_distributions_64_1.png

与单变量图一样,图格大小或平滑带宽的选择将决定图对基础二元分布的表示程度。相同的参数适用,但可以通过传递一对值来针对每个变量进行调整:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", binwidth=(2, .5))
<seaborn.axisgrid.FacetGrid at 0x7f08f6fe7410>
_images/sec07_distributions_66_1.png

为了帮助解释热图,请添加一个颜色条来显示计数和颜色强度之间的映射:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", binwidth=(2, .5), cbar=True)
<seaborn.axisgrid.FacetGrid at 0x7f08f5344610>
_images/sec07_distributions_68_1.png

二元密度等值线的含义就不那么简单了。因为密度不能直接解释,所以等高线是按密度的等比例绘制的,这意味着每条曲线都显示了一个水平集,使得密度的某个比例p低于它。p值间隔均匀,最低级别由thresh参数控制,数量由levels控制:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", kind="kde", thresh=.2, levels=4)
<seaborn.axisgrid.FacetGrid at 0x7f08f6ff4b50>
_images/sec07_distributions_70_1.png

levels参数还接受值列表,以便进行更多控制:

>>> sns.displot(penguins, x="bill_length_mm", y="bill_depth_mm", kind="kde", levels=[.01, .05, .1, .8])
<seaborn.axisgrid.FacetGrid at 0x7f08f524f6d0>
_images/sec07_distributions_72_1.png

双变量直方图允许一个或两个变量是离散的。绘制一个离散变量和一个连续变量提供了另一种比较条件单变量分布的方法:

>>> sns.displot(diamonds, x="price", y="clarity", log_scale=(True, False))
<seaborn.axisgrid.FacetGrid at 0x7f08f4e02e90>
_images/sec07_distributions_74_1.png

相比之下,绘制两个离散变量是一种简单的方法来显示观测值的交叉表:

>>> sns.displot(diamonds, x="color", y="clarity")
<seaborn.axisgrid.FacetGrid at 0x7f08f4bb8210>
_images/sec07_distributions_76_1.png

5.7.5. 分布在其他设置中的可视化

seaborn中的其他几个图形级绘图函数使用了histplot()kdeploy()函数。

绘制关节和边缘分布

第一个是jointplot(),它用两个变量的边际分布增加二元关系图或分布图。默认情况下,jointplot()使用scatterplot()表示双变量分布,使用histplot()表示边缘分布:

>>> sns.jointplot(data=penguins, x="bill_length_mm", y="bill_depth_mm")
<seaborn.axisgrid.JointGrid at 0x7f08f4dc6f50>
_images/sec07_distributions_78_1.png

displot()类似,在jointplot()中设置不同的kind="kde"将改变使用kdeploy()的联合和边缘图:

>>> sns.jointplot(
>>>     data=penguins,
>>>     x="bill_length_mm", y="bill_depth_mm", hue="species",
>>>     kind="kde"
>>> )
<seaborn.axisgrid.JointGrid at 0x7f08f4b79490>
_images/sec07_distributions_80_1.png

jointplot()JointGrid类的一个方便接口,直接使用时提供了更大的灵活性:

>>> g = sns.JointGrid(data=penguins, x="bill_length_mm", y="bill_depth_mm")
>>> g.plot_joint(sns.histplot)
>>> g.plot_marginals(sns.boxplot)
<seaborn.axisgrid.JointGrid at 0x7f08f5311390>
_images/sec07_distributions_82_1.png

显示边际分布的一种不那么突兀的方法使用“地毯”图,该图在图的边缘添加一个小刻度来表示每个单独的观测值。这是内置的displot()

>>> sns.displot(
>>>     penguins, x="bill_length_mm", y="bill_depth_mm",
>>>     kind="kde", rug=True
>>> )
<seaborn.axisgrid.FacetGrid at 0x7f08f48e1090>
_images/sec07_distributions_84_1.png

轴级函数rugplot()可用于在任何其他类型的图的侧面添加地毯:

>>> sns.relplot(data=penguins, x="bill_length_mm", y="bill_depth_mm")
>>> sns.rugplot(data=penguins, x="bill_length_mm", y="bill_depth_mm")
<AxesSubplot: xlabel='bill_length_mm', ylabel='bill_depth_mm'>
_images/sec07_distributions_86_1.png

绘制多个分布

pairplot()函数提供了类似的联合分布和边际分布的混合。然而,pairplot()不是专注于单个关系,而是使用“小倍数”方法来可视化数据集中所有变量的单变量分布及其所有成对关系:

>>> sns.pairplot(penguins)
<seaborn.axisgrid.PairGrid at 0x7f08f42e69d0>
_images/sec07_distributions_88_1.png

jointplot()/JointGrid一样,直接使用底层的PairGrid将提供更多的灵活性,只需要更多的类型:

>>> g = sns.PairGrid(penguins)
>>> g.map_upper(sns.histplot)
>>> g.map_lower(sns.kdeplot, fill=True)
>>> g.map_diag(sns.histplot, kde=True)
<seaborn.axisgrid.PairGrid at 0x7f08ef81fed0>
_images/sec07_distributions_90_1.png