写点什么

漫谈数据分布可视化分析

作者:百度Geek说
  • 2024-02-22
    上海
  • 本文字数:7029 字

    阅读完需:约 23 分钟

作者 | FesianXu


导读在实际工作中,我们经常会遇到一堆数据,对数据的有效分析至为关键,而数据的分布就是一种非常重要的数据属性,需要通过合适的可视化手段进行分析。本文参考[1],基于 seaborn 库介绍一些常用的数据分布可视化方法。


全文 8720 字,预计阅读时间 22 分钟


数据的分布,我们可以理解为是“数据的形状”。一个“完美”的数据分布,会将数据所有可能的数据点都囊括其中,因此数据的分布表征了不同数据之间的本质区别。然而现实生活的数据不可能对所有可能的数据点都进行遍历(因为通常会有无限个数据点),因此我们通常都是在某个采样的子集中,尝试对数据本原的分布进行分析。常见的数据分布可视化方法有以下几种:


1.直方图(Histogram)

2.条件直方图(Conditional Histogram)

3.核密度估计图(Kernel Density Estimation,KDE)

4.累积分布函数图(Empirical Cumulative Distribution Function, ECDF)

5.箱型图(boxplot)

6.提琴图(violin plot)

7.二元直方图(bivariate histogram)

8.联合概率分布曲线(Joint Distribution Plot)

9.边缘概率分布曲线(Marginal Distribution Plot)


如 Fig 1.所示,我们以 penguins 数据集为例子分别进行介绍



△Fig 1. penguins 数据集,一共有 344 条数据,每条数据有 7 个维度的属性。

01 单变量直方图

单变量直方图(univariate histogram)是一种单变量的分布可视化方法,将所有数据点进行分桶,然后统计落在某个桶里的数据点的频次,以柱形图的形式将每个桶的频次绘制出来。如 Fig 1.1 所示,我们对 penguins 数据中的 flipper_length_mm 属性进行直方图绘制。



import seaborn as snsdata = sns.load_dataset("penguins", data_home="./data/seaborn-data")# 可以挑选不同的分桶数量bins,或者每个桶的宽度ret = sns.displot(data, x='flipper_length_mm', bins=20)ret = sns.displot(data, x='flipper_length_mm', bins=50)ret = sns.displot(data, x='flipper_length_mm', binwidth=5)
复制代码



△Fig 1.1 对 penguins 数据的flipper_length_mm属性进行绘制直方图,从左到右,分别是(a)分桶数是 20,(b)分桶数 50,(c)分桶宽度是 5。


我们发现,选取不同的分桶数量和分桶宽度对于整个分布可视化结果影响很大。分桶越多,分布越细致,但也越容易被某些噪声影响我们分析整体分布趋势,一般在实际中我们通常会选取多个分桶数进行尝试。原始的直方图统计的是分桶中的数据频次,不同数据的总数不同因此频次并不可比,通常可以考虑进行归一化处理。如 Fig 1.2 所示,通常有两种类型的归一化:密度归一化,概率归一化。密度归一化指的是所有柱形面积和为 1,概率归一化指的是所有柱形的高度和为 1。密度归一化的情况下,由于纵坐标的数值会受到横坐标数值尺度的影响,通常是不可比,而概率归一化不需要考虑横坐标的数值尺度,因此通常是可比的。


ret = sns.displot(data, x='flipper_length_mm', bins=20, stat='density') # 密度归一形式的归一化ret = sns.displot(data, x='flipper_length_mm', bins=20, stat='probability') # 概率归一形式的归一化
复制代码



△Fig 1.2 (a) 密度归一形式的归一化; (b)概率归一形式的归一化。


我们既可以对连续变量进行直方图统计,也可以对离散变量进行直方图统计,如 Fig 1.3 所示,我们通过设置 shrink 参数,可以控制柱形的宽度,使得可视化效果更像是一个离散变量的直方图。



tips = sns.load_dataset('tips')ret = sns.displot(tips, x='day', stat='probability')ret = sns.displot(tips, x='day', stat='probability', shrink=.8)
复制代码



△Fig 1.3 (a)shrink=1.0 的情况,柱形之间“肩靠肩”,没有间隙;(b)通过设置 shrink=0.8,可以使得离散变量的柱形之间存在间隔,看起来更“离散”一些,与连续变量直方图有所区别。

02 条件直方图

再回到 Fig 1.2,我们能明显地发现这个分布有明显的两个峰,这个 flipper_length_mm 代表的是企鹅的鳍肢的长度,这个属性会受到其他什么属性的影响呢?在之前的直方图中,我们绘制的是概率分布如今我们需要绘制条件概率分布,以考察到底是其他哪些属性影响了企鹅的鳍肢的长度。如 Fig 2.1 所示,由于笔者觉得种类,性别,和居住的岛屿可能会影响到企鹅的鳍肢长度,我绘制了 ,,的几种条件分布。从 Fig 2.1(a)可以发现企鹅的种类的确有所影响(Adelie 种类和 Gentoo 种类的企鹅的鳍肢分布显著不同),而雌性雄性企鹅的鳍肢长度都呈现双峰分布,因此并不是导致 Fig 1.2 中出现双峰分布的原因,同理,从 Fig 2.1(c)中可以发现企鹅居住的岛屿也是导致出现双峰的原因。这些条件分布都可以通过设置 displot()中的 hue 参数实现。


ret = sns.displot(data, x="flipper_length_mm", hue='species')ret = sns.displot(data, x="flipper_length_mm", hue='sex')ret = sns.displot(data, x="flipper_length_mm", hue='island')
复制代码



△Fig 2.1 (a)-(c)是不同的条件分布直方图。


但是居住地会影响企鹅的鳍肢生长发育,这一点比较奇怪,可能并不是一个直接原因,也许是不同岛屿居住的企鹅种类不同导致的?我们可以绘制 条件分布进行考察,如 Fig 2.2 所示,其中的(a)是一般形式的条件分布柱形图,我们发现不同条件下的柱形会存在层叠,容易看不清楚,此时可以通过设置 multiple="stack"将其设置为 stack 模式,柱形图之间以堆叠形式呈现,不会存在层叠。我们可以发现在 Fig 2.1(c)中的 Biscoe 和 Dream 岛屿呈现的两个峰,来自于这两个岛屿中的两大企鹅种群——Gentoo,Chinstrap+Adelie(这两个种类的分布较为接近)分布导致。也就是说,鳍肢和企鹅种类的关联才是本质的因果关系。


ret = sns.displot(data, x="island", hue='species')ret = sns.displot(data, x="island", hue='species', multiple="stack", discrete=False
复制代码



△Fig 2.2 (a)一般形式的条件分布绘制,有时候由于柱形图的层叠,容易看不出清楚,此时可以采用 stack 模式,如(b)所示,此时不同柱形之间不会有层叠。


我们在 Fig 1.2 中说到过直方图的归一化,由于 Fig 1.2 中是单变量的分布归一化,因此体现到图中只是纵坐标的尺度变化,而整个图的形状是不会有变化的。在条件直方图中,我们可以将 common_norm 设置为 False,此时进行归一化会将不同条件下的条件分布进行独立的归一化,如 Fig 2.3 所示,其中(a)和(b)只有纵坐标上的区别,而(c)是将不同条件下的条件分布进行各自的归一化。


ret = sns.displot(data, x="flipper_length_mm", hue='species')ret = sns.displot(data, x="flipper_length_mm", hue='species', stat="probability")ret = sns.displot(data, x="flipper_length_mm", hue='species', stat="probability", common_norm=False)
复制代码



△Fig 2.3 (a)条件分布原图;(b)对条件分布进行归一化,不考虑不同条件下的区别;(c)进行不同条件下的分别归一化。

03 核密度估计曲线

直方图对数据进行分箱后,统计每个箱子中的数据点频次,因此绘制出来的直方图是离散的柱形图,即便数据是连续型数据。有什么方法可以更好地体现数据的连续性质呢?核密度估计(Kernel Density Estimation,KDE)是一种可行的方法,我们假设每个数据点都是对数据分布的一次随机采样,采样自均值为观察值 ,方差为的核分布,如公式(3-1)所示,我们对所有数据点的核密度估计曲线进行叠加,得到整个数据分布的核密度估计曲线。我们的核分布通常可以采用高斯分布,如公式(3-2)所示。



(3-1)



(3-2)


如 Fig 3.1 所示,在给定了 6 个观察值,绘制出的直方图如 Fig 3.1(a)所示,如 Fig 3.1(b)所示,其中的红色曲线表示每个样本的高斯核密度估计,叠加起来得到蓝色曲线,为整个数据分布的核密度估计曲线。




△Fig 3.1 (a)直方图;(b)对每个样本进行核密度估计,然后叠加得到数据分布的拟合曲线。


回到我们的 seaborn,我们通过设置参数 kind="kde"进行设置,同时可以通过 bw_adjust 控制其方差,我们通常把此处的方差称之为带宽(bandwidth)。如 Fig 3.2 所示,我们发现不同的带宽下,核密度估计曲线的形状天差地别,Fig 3.2(b)中,过小的带宽可以看到分布的更多细节,但是也会收到更多数据噪声的影响,出现过多的毛刺。如 Fig 3.2(c)所示,过大的带宽会使得曲线过于平滑,使得一些分布细节被掩饰了,比如其中的双峰分布就被平滑得看不出来了。


sns.displot(data, x="flipper_length_mm", kind="kde", bw_adjust=1)sns.displot(data, x="flipper_length_mm", kind="kde", bw_adjust=0.25)sns.displot(data, x="flipper_length_mm", kind="kde", bw_adjust=1.5)
复制代码



△Fig 3.2 不同的带宽参数对于核密度估计函数曲线的影响。


当然,核密度估计也可以在条件分布的情况下使用,如 Fig 3.3 所示,同样有几种不同的参数配置曲线的显式效果。


sns.displot(data, x="flipper_length_mm", hue="species", kind="kde")sns.displot(data, x="flipper_length_mm", hue="species", kind="kde", multiple="stack")sns.displot(data, x="flipper_length_mm", hue="species", kind="kde", fill=True)
复制代码



△Fig 3.3 (a) 条件分布下的核密度估计曲线;(b)stack 模式的条件分布核密度估计曲线;(c)填充曲线下面积的条件分布核密度估计曲线。


可以将直方图和核密度估计曲线绘制在同一张图中以便于分析,如 Fig 3.4 所示。


sns.displot(data, x="flipper_length_mm", kde=True)sns.displot(data, x="flipper_length_mm", hue="species", kde=True)
复制代码



△Fig 3.4 将直方图和核密度估计曲线绘制在同一张图中。

04 箱形图

直方图和核密度估计都太“重”了,很多时候在刚接触某个数据集的时候,一些统计性指标就足够让我们对这份数据有足够的了解。常用的统计指标有:中位数(50 分位线数),均值,方差,25 分位线数,75 分位线数等,而这些指标大多数情况可以从箱型图(Boxplot)中一目了然。如 Fig 4.1 所示,一个箱型图由五根线构成,Q1 是 25 分位线,Q3 是 75 分位线,指的是将数据从低到高排序(升序),前 25%称之为 25 分位线,前 75%称之为 75 分位线。Q3-Q1 称之为四分位距(Inter Quartile Range,IQR),Q2 表示 50 分位线,也即是中位线,小于 Q1-1.5IQR 和 大于 Q3+1.5IQR 的数据称之为离群点。



△Fig 4.1 箱型图的几种基本分位线。


如 Fig 4.2 所示,我们可以用 seaborn 绘制箱型图,其中用红点表示均值,可以发现 Fig 4.2(a)其实绘制了 ,当然也可以和 Fig 4.2(c)一样绘制单变量的箱型图,这种也是我们最常见到的形式。如 Fig 4.2(b)所示,我们还可以绘制 2 个条件下的箱型图,也即是通过箱型图,我们可以非常直观地观察到不同数据分布之间的差别,是一种轻量化的数据分布分析方法。


sns.boxplot(data=data,             x="flipper_length_mm", y='species',             showmeans=True,            meanprops={"marker":"o",                       "markerfacecolor":"red",                        "markeredgecolor":"black",                      "markersize":"5"})sns.boxplot(data=data,             x="flipper_length_mm", y='species', hue='sex',            showmeans=True,            meanprops={"marker":"o",                       "markerfacecolor":"red",                        "markeredgecolor":"black",                      "markersize":"5"})sns.boxplot(data=data,             x="flipper_length_mm",             showmeans=True,            meanprops={"marker":"o",                       "markerfacecolor":"red",                        "markeredgecolor":"black",                      "markersize":"5"})
复制代码



△Fig 4.2 (a)箱型图;(b)多个条件下的箱型图;(c)单变量箱型图。

05 提琴图

箱型图可以提供数据的直观印象,为了进一步分析数据,我们还是需要引入分布曲线。我们希望可以以箱型图的形式,同时把数据分布也可视化出来,这样我们既可以复用箱型图得到的结论,而且可以进一步探索数据分布的细致区别。提琴图(Violin Plot)就是为此设计的,如 Fig 5.1 所示,其将数据的核密度估计曲线以类似于箱型图的排版进行展示,其中的每条黑色竖线是每个真实的样本点数据。注意到提琴图本质是核密度估计曲线,因此如果样本数据过少其曲线是不准确的,所以通常我们会把样本点也绘制出来(也即是黑色竖线),以判断数据数量是否会过于稀疏导致 KDE 不置信。


sns.violinplot(data=data, x="flipper_length_mm", y='species', inner="stick")sns.violinplot(data=data, x="flipper_length_mm", y='species', hue='sex', split=True, inner="stick")sns.violinplot(data=data, x="flipper_length_mm", inner="stick")
复制代码



△Fig 5.1 提琴图的不同形式,(a)提琴图;(b)两个条件下的提琴图;(c)单变量提琴图。

06 累计分布函数曲线

直方图需要选择合适的分箱数,而 KDE 需要选择合适的带宽,否则可能会影响数据分布的可视化效果进而影响分析。有没有一种方法可以不用选择任何参数就能表征数据的分布特性呢?经验累积分布函数(Empirical Cumulative Distribution Function,ECDF)也许是一种可行的选择。累积分布函数(Cumulative Distribution Function,CDF)对概率分布进行积分或者求和得到,如公式(6-1)所示。当对实际数据进行处理时候,由于我们的数据有限且来自于实际观察,因此我们通常称之为“经验”[2],并且 ecdf 是采用求和,而不是积分。



如 Fig 6.1 所示,ecdf 曲线是一个单调递增的曲线,其会考虑每一个观察到的数据点,不需要指定其他额外的参数,如 Fig 6.1(b)所示,我们也可以绘制条件分布下的 ecdf。ECDF 的缺点在于不如直方图和核密度估计一样直观地表征了数据分布,但是理论上 ECDF 同样可以“坍缩”到概率分布,如公式(6-2)所示 。



sns.displot(data, x="flipper_length_mm", kind="ecdf")sns.displot(data, x="flipper_length_mm", hue="species", kind="ecdf")
复制代码



△Fig 6.1 ECDF 曲线,(a)单变量的 ECDF 曲线;(b)条件分布下的 ECDF 曲线。

07 二元直方图

有时候我们需要考察数据的联合概率分布,比如 ,此时可以绘制二元直方图。我们可以指定 displot()的 y 参数,绘制两元变量的直方图。这种类型的直方图类似于热值图(heatmap),以颜色的深浅表示数值的大小。如 Fig 7.1(c)所示,同样可以指定 kind 参数进而绘制二元核密度估计曲线。在指定了 hue 的情况下,同样可以实现条件分布的绘制。


sns.displot(data, x="flipper_length_mm", y="species",cbar=True)sns.displot(data, x="bill_length_mm", y="bill_depth_mm", hue="species")sns.displot(data, x="bill_length_mm", y="bill_depth_mm", hue="species", kind="kde")
复制代码



△Fig 7.1 二元直方图以及二元核密度估计曲线。

08 联合概率分布和边缘概率分布曲线

考察二元变量的联合概率分布,采用散点图(scatter plot)也是一种不错的选择,如 Fig 8.1 所示,可以将 通过散点图的形式进行可视化,直观地考察两个变量之间的相关关系。散点图上面的直方图和右边的直方图分别是,分布的直方图,在这种情形下,我们称之为边缘概率分布曲线(Marginal Distribution),其计算公式见(8-1)。



我们也可以对联合概率分布的散点图和边缘概率分布的直方图进行核密度估计,如 Fig 8.1(b)所示。


sns.jointplot(data=data, x="bill_length_mm", y="bill_depth_mm")sns.jointplot(    data=data,    x="bill_length_mm", y="bill_depth_mm", hue="species",    kind="kde")
复制代码



△Fig 8.1 联合概率分布与边缘概率分布的可视化。


通过使用 seaborn 的 JointGrid 功能,可以对联合概率分布和边缘概率分布的表示形式进行自定义组合(散点图,KDE,箱型图等),如 Fig 8.2 所示。


g = sns.JointGrid(data=data, x="bill_length_mm", y="bill_depth_mm")g.plot_joint(sns.histplot)g.plot_marginals(sns.kdeplot)
g = sns.JointGrid(data=data, x="bill_length_mm", y="bill_depth_mm")g.plot_joint(sns.histplot)g.plot_marginals(sns.boxplot)
g = sns.JointGrid(data=data, x="bill_length_mm", y="bill_depth_mm")g.plot_joint(sns.scatterplot)g.plot_marginals(sns.boxplot)
复制代码



△Fig 8.2 通过 JointGrid 进行直方图,KDE,散点图,箱型图的组合。


END


参考资料:

[1]. https://seaborn.pydata.org/tutorial/distributions.html#plotting-univariate-histograms

[2]. https://blog.csdn.net/LoseInVain/article/details/78746520,经验误差,泛化误差


推荐阅读:

数据库运维工作量直接减少 50%,基于大模型构建智能问答系统的技术分享


千万级高性能长连接Go服务架构实践


百度搜索Push个性化:新的突破


智算让大模型触手可及


通过Python脚本支持OC代码重构实践

用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
漫谈数据分布可视化分析_数据分析_百度Geek说_InfoQ写作社区