0%

机器学习之数据预处理:Transformation变换

数据预处理作为机器学习中的关键一环,这里介绍如何改变特征的分布形状——Transformation变换

abstract.png

分析诊断

可通过下述常见的可视化工具来判断、分析数据的分布特征:

  • Histogram直方图:统计、展示各数据区间范围内的样本频数。较为简单、直观
  • KDE核密度图:KDE核密度估计是一种非参的概率密度函数估计方法。通过平滑的概率密度曲线,可以直观的看出数据的分布特征
  • Q-Q(Quantile-Quantile)图:Q-Q图将样本数据分布与指定理论分布进行比较,来判断样本数据是否服从指定分布。具体地,Q-Q图中的散点是以理论分布的百分位数为x坐标、以样本数据的百分位数为y坐标。同时,图中还有一条参考线。如果上述散点都紧密地落在参考线附近,则说明该样本数据的分布 与 指定分布吻合。该图的典型应用场景:将理论分布指定为正态分布,用于判断样本数据是否符合正态分布

这里介绍用于衡量数据分布的形状、形态指标:Skewness 偏态系数、Kurtosis 峰度系数

Skewness 偏态系数:用于衡量数据分布的偏移方向、程度

  • 偏态系数 > 0:即所谓的正偏态。数据分布向右侧延伸得更长,故又被称为右偏态。由于大部分数据集中在左侧,但右侧存在少量的极大值。故导致 均值 > 中位数。典型的例子:个人收入
  • 偏态系数 < 0:即所谓的负偏态。数据分布向左侧延伸得更长,故又被称为左偏态。由于大部分数据集中在右侧,但左侧存在少量的极小值。故导致 均值 < 中位数。典型的例子:考试成绩
  • 偏态系数 = 0: 此时数据分布左右两侧是对称的,故导致 均值 = 中位数。典型的例子:身高数据的偏态系数近似为0

Kurtosis 峰度系数:其衡量的是相对于正态分布而言,数据样本中的极端值(即尾部,特别大的值和特别小的值)的数量是多还是少。如果多,则说明是重尾;反之,则说明是轻尾

  • 峰度系数 > 0:即所谓的重尾相比于正态分布而言,样本中存在更多的极端值。典型的例子:自然灾害(例如:地震、洪水等)的强度。现实世界一般每天发生弱震的次数很多,极端剧烈强震虽然的次数很少。但事实上发生的极端强震概率还是比正态分布模型预测的概率要高很多
  • 峰度系数 < 0:即所谓的轻尾相比于正态分布而言,样本中存在更少的极端值。典型的例子:健康人群的体温数据。由于生理系统的调节机制,健康人群的体温基本都维持在37度附近
  • 峰度系数 = 0:样本中极端值的频率与正态分布一致。典型的例子:身高数据由于近似符合正态分布,故峰度系数近似为0

下面将展示正态分布、右偏态分布、左偏态分布的数据在Histogram直方图、KDE核密度图、Q-Q(Quantile-Quantile)图的表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import pandas as pd
import numpy as np
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats


def plot(data: pd.Series, data_name: str):
"""
对指定数据绘制: Histogram直方图、KDE核密度图、Q-Q图
"""
# 计算偏态系数
skewness = data.skew()
# 计算峰度
kurtosis = data.kurt()

# 创建一个 1x3 的子图布局
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)

# 整个图的标题
fig.suptitle(f"{data_name}\n(Skewness: {skewness:.2f} | Kurtosis: {kurtosis:.2f})", fontsize=16)

# 绘制直方图
sns.histplot(data, ax=ax1)
ax1.set_title("Histogram")
ax1.set_xlabel("Value")
ax1.set_ylabel("Freq")

# 绘制KDE核密度图, 并使用指定颜色填充曲线下方区域
sns.kdeplot(data, ax=ax2, fill=True, color='orange')
ax2.set_title("KDE")
ax2.set_xlabel("Value")
ax2.set_ylabel("Density")

# 绘制Q-Q图的数据点、参考线, 理论分布指定为正态分布
stats.probplot(data, dist="norm", plot=ax3)
ax3.set_title("Normal Q-Q")
# 散点的边缘、填充 设置为红色
ax3.get_lines()[0].set_markerfacecolor('r')
ax3.get_lines()[0].set_markeredgecolor('r')
# 参考线的设置为黑色
ax3.get_lines()[1].set_color('k')

# 调整子图布局
plt.tight_layout()
plt.show()


# iris鸢尾花据集中的sepal_width萼片宽度特征, 该数据近似于正态分布
iris_dataset = sns.load_dataset('iris')
sepal_width_data = iris_dataset['sepal_width']

# titanic泰坦尼克号数据集的fare船票费用特征, 该数据呈右偏态分布
titanic_dataset = sns.load_dataset('titanic')
fare_data = titanic_dataset['fare']

# 创建固定种子的随机数生成器,保证可重复
rng = np.random.default_rng(seed=42)
# 创建满足指定Beta分布的数据,该数据呈左偏态分布
data = rng.beta(a=9967, b=3, size=40000)
left_skewed_data = pd.Series(data)

plot(sepal_width_data, "Sepal Width of Iris Data")
plot(fare_data, "Fare of Titanic Data")
plot(left_skewed_data, "Left Skewed Data")

从下图不难看出,无论是那种分布形状在Histogram直方图、KDE核密度图中的表现都是比较明显、直观的;而在Q-Q图(理论分布为正态分布时)中,对于正态分布而言,散点基本可以很好贴合在参考线上;对于右偏分布而言,可以看到散点的形状像是一个笑脸的嘴形,右侧散点逐渐向参考线的上方翘起;对于左偏分布而言,可以看到散点的形状像是一个哭脸的嘴形,右侧散点逐渐向参考线的下方落下去

figure 1.png

figure 2.png

figure 3.png

调整方法

Log Transformation 对数变换

对数变换,顾名思义就是直接对数据取对数。根据对数函数的特性,不难知道,其适用于处理右偏态数据。一般底数取e自然对数,公式如下。这里x加1的目的在于使其可以处理数据为0的场景。这样其适用条件就从 要求数据均为正数 变为 要求数据非负 即可。毕竟现实世界中,数据为0很常见

Box-Cox 变换

Box-Cox 变换由统计学家George Box、David Cox于1964年提出的。其是一种广义的幂变换方法,通过λ参数来控制变换的强度。SciPy、Sklearn中均提供了该变换方式,其能够自动确定一个最佳的λ值,实现将数据变换为更接近正态分布的效果。变换公式如下所示。特别地,当λ为0时,该变换就等价于对数变换(取极限可得)。但该变换最大的限制在于,其要求所有数据都必须是正数

Yeo-Johnson 变换

Yeo-Johnson变换是对Box-Cox变换的一个扩展,由In-Kwon Yeo、Richard A. Johnson于2000年提出。其不仅可以处理正数、零,还可以处理负数。故Sklearn中的PowerTransformer类默认使用的就是 Yeo-Johnson 变换

实践

下面来展示如何通过Sklearn调整右偏态的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import FunctionTransformer
import seaborn as sns
import scipy.stats as stats
from sklearn.preprocessing import PowerTransformer


def plot(data: np.ndarray, data_name: str):
"""
对指定数据绘制: Histogram直方图、KDE核密度图、Q-Q图
"""
# 计算偏态系数
skewness = stats.skew(data)[0]
# 计算峰度
kurtosis = stats.kurtosis(data)[0]

# 创建一个 1x3 的子图布局
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)

# 整个图的标题
fig.suptitle(f"{data_name}\n(Skewness: {skewness:.2f} | Kurtosis: {kurtosis:.2f})", fontsize=16)

# 绘制直方图
sns.histplot(data, ax=ax1, legend=False)
ax1.set_title("Histogram")
ax1.set_xlabel("Value")
ax1.set_ylabel("Freq")

# 绘制KDE核密度图, 并使用指定颜色填充曲线下方区域
sns.kdeplot(data, ax=ax2, fill=True, facecolor='orange', alpha=0.6, legend=False)
# sns.kdeplot(data, ax=ax2, fill=True, facecolor='orange', legend=False)
ax2.set_title("KDE")
ax2.set_xlabel("Value")
ax2.set_ylabel("Density")

# 绘制Q-Q图的数据点、参考线, 理论分布指定为正态分布
stats.probplot(data.ravel(), dist="norm", plot=ax3)
ax3.set_title("Normal Q-Q")
# 散点的边缘、填充 设置为红色
ax3.get_lines()[0].set_markerfacecolor('r')
ax3.get_lines()[0].set_markeredgecolor('r')
# 参考线的设置为黑色
ax3.get_lines()[1].set_color('k')

# 调整子图布局
plt.tight_layout()
plt.show()


# 加载 titanic泰坦尼克号数据集的fare船票费用特征, 该数据呈右偏态分布
titanic_dataset = sns.load_dataset('titanic')
fare_data_series = titanic_dataset['fare']

# Transformer入参要求是二维结构,形状为 (样本数, 特征数)
fare_data = fare_data_series.to_numpy().reshape(-1,1)
print(f"fare_data: 类型: {type(fare_data)}, 形状: {fare_data.shape}")
plot(fare_data, "Fare Data: Origin")


print("-"*45, "对数变换", "-"*45)
# 通过 FunctionTransformer 将指定函数包装成一个 Transformer。func 参数: 指定变换函数。np.log1p 函数为: ln(1+x)
log_transformer = FunctionTransformer(func=np.log1p)
# 变换
fare_data_by_log = log_transformer.fit_transform(fare_data)
plot(fare_data_by_log, "Fare Data: Log Transformation")


print("-"*45, "Box-Cox变换", "-"*45)
# +1 保证样本数据大于零
fare_plus_one = fare_data + 1
# 创建PowerTransformer实例: 使用Box-Cox变换; standardize设置为False, 不进行后续的标准化
boxcox_transformer = PowerTransformer(method='box-cox', standardize=False)
# 变换
fare_data_by_boxcox = boxcox_transformer.fit_transform(fare_plus_one)
print(f"Box-Cox变换 λ参数: {boxcox_transformer.lambdas_}")
plot(fare_data_by_boxcox, "Fare Data: Box-Cox Transformation")


print("-"*45, "Yeo-Johnson变换", "-"*45)
# 创建PowerTransformer实例: 使用Yeo-Johnson变换; standardize设置为False, 不进行后续的标准化
yeo_johnson_transformer = PowerTransformer(method='yeo-johnson', standardize=False)
# 变换
fare_data_by_yeo_johnson = yeo_johnson_transformer.fit_transform(fare_data)
print(f"Yeo-Johnson变换 λ参数: {yeo_johnson_transformer.lambdas_}")
plot(fare_data_by_yeo_johnson, "Fare Data: Yeo-Johnson Transformation")

输出结果如下

1
2
3
4
5
6
fare_data: 类型: <class 'numpy.ndarray'>, 形状: (891, 1)
--------------------------------------------- 对数变换 ---------------------------------------------
--------------------------------------------- Box-Cox变换 ---------------------------------------------
Box-Cox变换 λ参数: [-0.09778702]
--------------------------------------------- Yeo-Johnson变换 ---------------------------------------------
Yeo-Johnson变换 λ参数: [-0.09778703]

效果如下所示

figure 4.png

Figure 5.png

Figure_6.png

Figure_7.png

参考文献

  • 机器学习 周志华著
  • 机器学习公式详解 谢文睿、秦州著
  • 图解机器学习和深度学习入门 山口达辉、松田洋之著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝