跳至主要内容

理解偏度与峰度及其可视化方法

一份关于偏度/峰度的全面可视化指南,以及它们如何影响分布,进而影响您的数据科学项目。
· 10分钟 阅读

使用 AI 探索

在收集数据并花费数小时清洗之后,您终于可以开始探索了!这一阶段通常称为探索性数据分析(EDA),也许是数据项目中最重要的一步。EDA 所获得的洞见会影响后续的方方面面。

例如,EDA 中必须执行的一步是检查分布的形状。正确识别形状会影响项目后续的诸多决策,例如:

  • 进一步的预处理步骤
  • 是否进行异常值检测以及可能的移除
  • 特征变换或缩放步骤
  • 特征选择
  • 算法选择

等等。虽然我们可以用可视化来完成这项任务,但您还需要更可靠的度量来量化分布的各种特征。两个这样的度量就是偏度和峰度。您可以用它们来评估自己的分布与完美正态分布之间的相似程度。

读完本文,您将深入了解:

  • 什么是偏度与峰度
  • 偏度与峰度的类型
  • 偏度与峰度对机器学习模型的影响
  • 在 Python 中手动和使用第三方包计算偏度与峰度
  • 可视化分布以验证偏度与峰度分数(全面且有趣的部分)

让我们开始吧!

什么是偏度?

我们到处都能看到正态分布:人体测量、物体重量、IQ 分数、测试成绩,甚至在健身房也能见到:

健身房里的配重器械图片。配重盘上的插销孔由于长期高频使用,在整体上呈现出接近正态分布的形状。

来源

除了是自然界的“宠儿”,几乎所有机器学习算法也都偏爱它。有些算法希望借此提升并稳定性能,有些则对非正态分布“水土不服”(没错,说的就是你们,线性模型)。

因此,为了满足算法对“正态性”的偏好,我们需要一种方法来衡量自己的分布与完美钟形曲线的相似(或不相似)程度。

完美的正态分布钟形曲线。

先从尾部说起。在完美的正态分布中,左右两条尾巴长度相等。但当尾部存在不对称,使分布呈现向一侧倾斜、被“挤压”的样子时,我们称其为偏态。没错,我们用偏度来衡量这种不对称的程度。

在一个无偏的正态分布两侧展示不同类型的偏度。

正确归类并测量偏度可以帮助我们了解数值在均值周围的分布情况,并影响统计技术与数据变换的选择。比如,高度偏态的分布可能需要归一化或缩放,以让其更接近正态分布,从而有助于提升模型性能。

偏度的类型

偏度有三种类型:正偏、负偏和零偏。

先从最后一种说起。零偏的分布具有以下特征:

  • 分布对称,数值围绕均值均匀集中。
  • 不存在向任一侧的偏斜、倾斜或尾部拉长。
  • 均值、中位数和众数都位于中心点。

一个正态分布(零偏),均值、中位数和众数在同一直线上对齐。

在实践中,均值、中位数和众数未必能完美重合成一直线。它们可能会略有偏离,但差异通常小到可以忽略不计。

在正偏(右偏)分布中:

  • 分布的右尾比左尾更长或更厚。
  • 均值大于中位数,而众数小于均值和中位数。
  • 较低的数值集中在分布的“山丘”处,而极端值位于长长的右尾中。
  • 也称为右偏分布。

右偏分布,并标注了均值、中位数和众数。

在负偏(左偏)分布中:

  • 分布的左尾比右尾更长或更厚。
  • 均值小于中位数,而众数大于均值和中位数。
  • 较高的数值集中在分布的“山丘”处,而极端值位于长长的左尾中。
  • 也称为左偏分布。

左偏分布,并标注了均值、中位数和众数

记忆正偏与负偏差异的一个方法是这样想:如果您想提高分布的均值,就应往分布中加入远高于均值的数值;要降低均值,则反过来——加入远低于均值的数值。因此,如果大多数极端值高于均值,偏度将为正,因为它们会拉高均值;如果大多数极端值低于均值,偏度将为负,因为它们会拉低均值。

如何在 Python 中计算偏度

计算偏度的方法很多,但最简单的是皮尔逊第二偏度系数,也称为中位数偏度。

image.png

让我们在 Python 中手动实现这个公式:

import numpy as np
import pandas as pd
import seaborn as sns

# Example dataset
diamonds = sns.load_dataset("diamonds")
diamond_prices = diamonds["price"]

mean_price = diamond_prices.mean()
median_price = diamond_prices.median()
std = diamond_prices.std()

skewness = (3 * (mean_price - median_price)) / std

>>> print(
   f"The Pierson's second skewness score of diamond prices distribution is {skewness:.5f}"
)

The Pierson's second skewness score of diamond prices distribution is 1.15189

另一种深受卡尔·皮尔逊研究影响的偏度近似公式是基于矩的公式。它更可靠,表达如下:

使用矩来计算偏度的另一种公式

其中:

  • n 表示分布中的数值个数
  • x_i 表示每个数据点

我们也在 Python 中实现一下:

def moment_based_skew(distribution):
   n = len(distribution)
   mean = np.mean(distribution)
   std = np.std(distribution)

   # Divide the formula into two parts
   first_part = n / ((n - 1) * (n - 2))
   second_part = np.sum(((distribution - mean) / std) ** 3)

   skewness = first_part * second_part

   return skewness

>>> moment_based_skew(diamond_prices)
1.618440289857168

如果您不想手动计算偏度(就像我一样),可以使用 pandas 或 scipy 的内置方法:

# Pandas version
diamond_prices.skew()
1.618395283383529

# SciPy version
from scipy.stats import skew

skew(diamond_prices)
1.6183502776053016

尽管不同的偏度近似公式会给出不同的分数,但差异通常微乎其微,不足以改变偏态的归类。例如,今天我们使用的各方法底层采用的公式不同,但结果非常接近。

计算出偏度后,您可以按以下标准对偏度程度进行归类:

  • (-0.5, 0.5) — 低偏或近似对称。
  • (-1, -0.5) ∪ (0.5, 1) — 中度偏态。
  • 小于 -1 或大于 1 — 高度偏态。

什么是峰度及其类型?

偏度关注的是正态分布的扩展(尾部),而峰度更关注高度。它告诉我们正态(或类正态)分布有多“尖”或多“平”。该术语源自希腊语,意为弯曲或拱形,最早由英国数学家卡尔·皮尔逊提出(不出所料,他一生致力于研究概率分布)。

高峰度表明:

  • 分布中心的峰非常尖。
  • 比正态分布有更多的数值集中在均值附近。
  • 尾部更重,因为尾部聚集了更多极端值或异常值。
  • 发生极端事件的可能性更大。

相反,低峰度表明:

  • 峰较平。
  • 在均值附近集中的数值更少,但仍多于正态分布。
  • 尾部更轻。
  • 发生极端事件的可能性更低。

按程度不同,分布的峰度有三种类型:

  1. 中峰(Mesokurtic)分布(峰度 = 3,超额峰度 = 0):完美正态分布或非常接近。
  2. 高峰(Leptokurtic)分布(峰度 > 3,超额峰度 > 0):尖峰、重尾
  3. 低峰(Platykurtic)分布(峰度 < 3,超额峰度 < 0):平峰、轻尾

三种不同类型的分布:高峰(高峰度)、中峰(无超额峰度)和低峰(低峰度)

注意,这里超额峰度定义为 峰度 - 3,将正态分布的峰度视为 0。这样一来,峰度分数更易解释。

如何在 Python 中计算峰度

您可以像计算偏度一样,在 Python 中使用 pandas 或 SciPy 计算峰度:

from scipy.stats import kurtosis

kurtosis(diamond_prices)
2.177382669056634

Pandas 提供了两个函数来计算峰度:kurt 和 kurtosis。前者仅适用于 Pandas 的 Series,后者可用于 DataFrame。

diamond_prices.kurt()
2.17769575924869

# Select numeric features and calculate kurtosis
diamonds.select_dtypes(include="number").kurtosis()
carat     1.256635
depth     5.739415
table     2.801857
price     2.177696
x        -0.618161
y        91.214557
z        47.086619
dtype: float64

同一分布的数值再次出现差异,是因为 pandas 与 SciPy 采用的公式不同。

如果您希望手动计算峰度,可以使用下面的公式:

受卡尔·皮尔逊影响、使用矩计算峰度的公式

其中:

  • n 表示数据集中的观测数量
  • x_i 表示每个具体的数据点

我们再把公式封装进一个函数:

def moment_based_kurtosis(distribution):
   n = len(distribution)
   mean = np.mean(distribution)
   std = np.std(distribution)

   kurtosis = (1 / n) * sum(((distribution - mean) / std) ** 4) - 3

   return kurtosis

>>> moment_based_kurtosis(diamond_prices)
2.1773826690576463

我们可以发现,钻石价格的超额峰度为 2.18,这意味着如果我们绘制其分布,峰会比正态分布更尖。

那就来画图吧!

在 Python 中可视化偏度与峰度

用于观察分布形状、从而查看偏度与峰度的最佳图形之一是核密度估计(KDE)图。它可以通过 Seaborn 直接绘制:

import matplotlib.pyplot as plt

sns.kdeplot(diamond_prices)

plt.title("KDE plot of diamond prices")
plt.xlabel("Price ($)")

钻石价格的 KDE 图

该图与我们目前看到的数据一致:分布有一条很长的右尾,表明正偏;同时峰非常尖,对应较高的峰度。

当然,KDE 并非唯一选择,我们也可以使用直方图:

sns.histplot(diamonds["carat"])

plt.xlabel("Carat")
plt.title("A histogram of the carat of diamonds")

钻石克拉数的直方图

直方图的劣势在于需要您自行选择组距数量(柱状条数)。这里柱子太多,给可视化带来了噪声——我们无法清晰定义形状。因此,减少一下组距数量:

sns.histplot(diamonds["carat"], bins=25)

plt.xlabel("Carat")
plt.title("A histogram of the carat of diamonds")

组距更少的钻石克拉数直方图

现在形状更清晰了,但我们还能继续改进。通过在 histplot 中设置 kde=True,我们可以在柱状图上叠加该分布的 KDE:

sns.histplot(diamonds["carat"], bins=25, kde=True)

plt.xlabel("Carat")
plt.title("A histogram of the carat of diamonds")

在钻石克拉数直方图上叠加 KDE

叠加的 KDE 线条看起来有些锯齿状,缺少那种能展示总体形状的平滑曲线。造成锯齿的原因是克拉数分布天然较为尖峰、多刺,离正态分布较远。

不过,我们可以通过调整带宽来降低 KDE 对这些波动的敏感度。可使用参数 bw_adjust 调整,默认值为 1:

# 将带宽从 1 改为 3
sns.kdeplot(diamonds["carat"], bw_adjust=3, color="red")

plt.title("KDE of diamond carats");

提高带宽调整后的钻石克拉数 KDE

这个版本比之前叠加的 KDE 明显不那么“多刺”。在使用叠加 KDE 的直方图时,如果要调整 KDE 的带宽,可以使用 kde_kws 参数:

ax = sns.histplot(
   diamonds["carat"],
   kde=True,
   kde_kws=dict(bw_adjust=3),
   bins=25,
)

plt.title("An overlaid histogram of diamond carats");

叠加了带宽已调整的 KDE 的直方图。

kde_kws 可接收所有 kdeplot 函数支持的、用于控制 KDE 计算的参数。

绘制 KDE 时,您可以使用一个小技巧:只保留 KDE 线条,移除其他元素。由于 KDE 的主要目的就是查看分布形状,坐标刻度、边框和标签等细节有时并非必要:

sns.kdeplot(diamond_prices, color="red")

# Remove the spine from three sides
sns.despine(top=True, right=True, left=True)

# Remove the ticks and ticklabels
plt.xticks([])
plt.yticks([])
plt.ylabel("")
plt.xlabel("")

# Set a title
plt.title("Diamond prices", fontdict=dict(fontsize=20));

一个只保留 KDE 线条和标题的 KDE 图。

这个图干净利落。您还可以通过添加均值、中位数和众数位置的标线来进一步改进:

sns.kdeplot(diamond_prices, color="red")

sns.despine(top=True, right=True, left=True)
plt.xticks([])
plt.yticks([])
plt.ylabel("")
plt.xlabel("")
plt.title("Diamond prices", fontdict=dict(fontsize=20))

# Find the mean, median, mode
mean_price = diamonds["price"].mean()
median_price = diamonds["price"].median()
mode_price = diamonds["price"].mode().squeeze()

# Add vertical lines at the position of mean, median, mode
plt.axvline(mean_price, label="Mean")
plt.axvline(median_price, color="black", label="Median")
plt.axvline(mode_price, color="green", label="Mode")

plt.legend();

一个仅保留 KDE 线条、标题,并标注均值、中位数、众数的 KDE 图。

该图验证了我们在偏度类型部分的讨论:在正偏分布中,均值高于中位数,而众数低于均值和中位数。

结论

偏度与峰度在探索性数据分析中常被忽视,却能揭示分布本质的关键信息。

偏度提示数据的“倾斜”方向,揭示是否存在不对称。正偏意味着尾部向右延伸,负偏则相反。

峰度关注峰与尾。高峰度使峰更尖、尾更重,低峰度则使数据更分散、尾更轻。

如果您想进一步了解偏度与峰度,可以查看 DataCamp 上由行业专家授课的这些优秀量化分析课程:

Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题