原文:
annas-archive.org/md5/5532fd447031f1db26ab91548948a023
译者:飞龙
第八章:检测和处理缺失值与离群值
本章讨论了处理缺失值和离群值的技术,这两个问题是数据分析中两个关键挑战,可能会显著影响我们数据产品的完整性和准确性。我们将探讨从统计方法到先进机器学习模型的广泛技术,以识别和管理这些数据异常。通过实践示例和真实数据集,我们将提出应对这些问题的策略,确保我们的分析具有稳健性、可靠性,并能够生成有意义的洞察。
本章的关键点如下:
-
检测和处理缺失数据
-
检测单变量和多变量离群值
-
处理单变量和多变量离群值
技术要求
你可以在以下链接中找到本章的所有代码:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter08
不同的代码文件对应章节的不同部分。让我们安装以下库:
pip install spacy==3.7.5
检测缺失数据
缺失数据是现实世界数据集中的常见且不可避免的问题。它发生在某个观察或记录中缺少一个或多个值。这种数据缺失可能会严重影响任何基于这些数据构建的分析或模型的有效性和可靠性。正如我们在数据领域所说:垃圾进,垃圾出,意味着如果你的数据不正确,那么基于这些数据创建的模型或分析也不会正确。
在接下来的部分中,我们将通过一个场景来演示如何检测缺失数据以及不同的填补方法是如何工作的。这个场景如下:
假设你正在分析一个包含学生信息的数据集,包括他们的年龄和测试分数。然而,由于各种原因,一些年龄和测试分数 是缺失的。
在这个脚本中,我们创建了整个章节中将使用的数据。让我们从导入语句开始:
import pandas as pd
让我们生成一些缺失年龄和测试分数的学生数据。这个字典数据包含两个键,Age
和 Test_Score
,每个键都有一个值列表。其中一些值为 None
,表示缺失的数据:
data = {
'Age': [18, 20, None, 22, 21, 19, None, 23, 18, 24, 40, 41, 45, None, 34, None, 25, 30, 32, 24, 35, 38, 76, 90],
'Test_Score': [85, None, 90, 92, None, 88, 94, 91, None, 87, 75, 78, 80, None, 74, 20, 50, 68, None, 58, 48, 59, 10, 5]}
df = pd.DataFrame(data)
数据集的前五行如下:
Age Test_Score
0 18.0 85.0
1 20.0 NaN
2 NaN 90.0
3 22.0 92.0
4 21.0 NaN
正如我们所看到的,数据集的两列中都有 NaN 值。为了了解数据集中缺失值的程度,让我们统计一下整个 DataFrame 中有多少缺失值:
missing_values = df.isnull()
df.isnull()
方法会创建一个与 df
形状相同的 missing_values
DataFrame,其中每个单元格如果对应的 df
单元格是 None
(缺失值),则为 True
,否则为 False
,如图所示:
Age Test_Score
0 False False
1 False True
2 True False
3 False False
4 False True
在之前的 DataFrame 中,任何包含 NaN
值的单元格现在都被替换为 True
。以这种格式存储数据有助于我们计算出有多少个 NaN
值:
null_rows_count = missing_values.any(axis=1).sum()
print("Count of Rows with at least one Missing Value:", null_rows_count)
print(8/len(df))
missing_values.any(axis=1)
参数检查每一行是否包含缺失值,返回一个 True
或 False
的 Series 来表示每一行。然后 .sum()
统计这个 Series 中 True
的个数,从而得出至少有一个缺失值的行数:
Count of Rows with at least one Missing Value: 8
% of rows with at least one missing value: 33%
现在我们知道数据集中缺失了多少数据。这个练习的下一个目标是找到最好的填补方法来补充这些缺失值。
处理缺失数据
处理缺失数据涉及做出谨慎的决策,以最小化其对分析和模型的影响。最常见的策略包括以下几种:
-
移除包含缺失值的记录
-
使用各种技术来填补缺失值,比如均值、中位数、众数填补,或更先进的方法,如基于回归的填补或 k-近邻填补
-
引入二进制指示变量来标记缺失数据;这可以告诉模型哪些值是缺失的
-
利用主题领域的专业知识来理解缺失数据的原因,并做出有关如何处理缺失值的明智决策
让我们深入研究这些方法,并详细观察它们在前面部分中展示的数据集上的结果。
删除缺失数据
处理缺失数据的一种方法是简单地删除包含缺失值的记录(行)。这是一种快捷且简单的策略,通常在缺失数据的百分比较低且缺失数据随机分布时更为合适。
在开始删除数据之前,我们需要更好地了解我们的数据集。继续使用之前示例中的数据,我们先打印描述性统计信息,再开始删除数据点。该部分的代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/2.delete_missing_data.py
找到。
注意
为了使本章的篇幅适中,我们仅展示了关键的代码片段。要查看所有示例,请访问仓库。
为了生成描述性统计,我们可以直接调用 pandas 中的 .describe()
方法:
print(df.describe())
这里展示了描述性统计信息:
Age Test_Score
count 20.000000 19.000000
mean 33.750000 65.894737
std 18.903843 27.989869
min 18.000000 5.000000
25% 21.750000 54.000000
50% 27.500000 75.000000
75% 38.500000 87.500000
max 90.000000 94.000000
让我们也为数据集的每一列创建分布图。
图 8.1 – 变动前特征的分布
完成此分析后,我们可以获得一些关于数据集的关键见解。对于年龄
,数据量为 20,平均年龄大约为 33.7 岁,标准差为 18.9 岁,显示出中等程度的变异性。年龄范围从 18 岁到 90 岁,年龄的中间 50%落在 21.75 岁到 38.5 岁之间。对于测试分数
,基于 19 个值,均值约为 65.8,标准差为 27.9,显示出较高的变异性。测试分数范围从 5 到 94 分,四分位差(IQR)从 54 到 87.5。
现在,让我们看看如何删除缺失数据。让我们关注数据集的变化:
df_no_missing = df.dropna()
让我们探索数据删除后的特征分布:
图 8.2 – 数据删除后的特征分布
让我们再看一下修改后的数据集的总结统计:
print(df_no_missing.describe())
Age Test_Score
count 16.000000 16.000000
mean 36.500000 65.500000
std 20.109699 26.610775
min 18.000000 5.000000
25% 23.750000 56.000000
50% 32.000000 74.500000
75% 40.250000 85.500000
max 90.000000 92.000000
看到两个数据集的描述性统计后,观察到的变化如下:
-
计数变化:删除缺失值的行后,年龄和测试分数的观测数量都从 20 减少到 16。
-
均值变化:平均年龄从 33.75 增加到 36.50,而平均测试分数略微下降,从 65.89 降至 65.50。这一变化反映了删除数据后剩余数据集中的值。
-
标准差变化:年龄的标准差从 18.90 增加到 20.11,表明年龄的分布范围更广,而测试分数的标准差则从 27.99 降至 26.61。
-
最小值和最大值:最小年龄保持不变,仍为 18 岁,而最小测试分数保持为 5 分。年龄和测试分数的最大值都有轻微变化,测试分数的最大值从 94 降至 92。
-
百分位变化:由于数据集的变化,百分位值(25%、50%、75%)发生了变化:
-
年龄的第 25 百分位从 21.75 增加到 23.75,测试分数的第 25 百分位从 54.00 增加到 56.00。
-
年龄的中位数(第 50 百分位)从 27.50 增加到 32.00,而测试分数的中位数从 75.00 略微下降到 74.50。
-
年龄的第 75 百分位从 38.50 增加到 40.25,而测试分数的第 75 百分位从 87.50 降至 85.50。
-
删除缺失值的行导致数据集变小,剩余数据现在具有不同的统计特性。当缺失值占数据集的比例较小且删除它们对数据没有显著影响时,这种方法是适用的。
那么,什么算是一个小比例呢?
一个常见的经验法则是,如果数据缺失少于 5%,通常认为缺失比例较小,删除这些数据可能不会对分析产生重大影响。通过比较有缺失数据和无缺失数据的分析结果,可以评估删除数据所造成的变化的显著性。如果结果一致,删除可能就不那么重要。
在这些缺失数据较为严重的情况下,我们将在下一部分探讨其他填充方法或更高级的技术,可能会更为适用。
缺失数据的填充
填充通常用于当删除缺失记录会导致显著信息丢失的情况。填充是指用估算或计算出的值替代缺失值。常见的填充方法包括均值填充、中位数填充和众数填充,或使用更高级的技术。
让我们来看看针对我们场景的不同填充方法。
均值填充
均值填充将缺失值替换为观察到的值的均值。这是一种非常简单的方法,当缺失值完全随机时,它不会引入偏差。然而,该方法对异常值敏感,并且可能会扭曲特征的分布。你可以在这个链接找到相关代码。
让我们看看均值填充的代码示例。在这个示例中,我们将使用之前解释过的相同数据集:
df_mean_imputed = df.copy()
df_mean_imputed['Age'].fillna(round(df['Age'].mean()), inplace=True)
前一行将Age
列中的任何缺失值填充为原始df
数据框中Age
列的均值。df['Age'].mean()
参数计算Age
列的均值,并将该均值四舍五入到最接近的整数。然后,fillna()
方法使用这个四舍五入后的均值替换Age
列中的任何NaN
值。inplace=True
参数确保更改直接在df_mean_imputed
中进行,而不会创建新的数据框。
df_mean_imputed['Test_Score'].fillna(df['Test_Score'].mean(), inplace=True)
同样,前一行将df_mean_imputed
中Test_Score
列的任何缺失值填充为原始df
数据框中Test_Score
列的均值。
让我们来看一下填充后的数据集:
print(df_mean_imputed)
Age Test_Score
0 18.0 85.000000
1 20.0 65.894737
2 34.0 90.000000
3 22.0 92.000000
4 21.0 65.894737
5 19.0 88.000000
6 34.0 94.000000
7 23.0 91.000000
8 18.0 65.894737
9 24.0 87.000000
10 40.0 75.000000
如我们所见,四舍五入后的均值已替代了年龄特征中的所有NaN
值,而绝对均值(abs mean)则替代了Test_Score
列中的NaN
值。我们对Age
列的均值进行了四舍五入,以确保它表示的是有意义的内容。
这里展示的是更新后的分布:
图 8.3 – 均值填充后的特征分布
从图表中可以看到,两个变量的分布都有些微变化。让我们来看看填补数据集的描述性统计:
print(df_mean_imputed.describe())
Age Test_Score
count 24.000000 24.000000
mean 33.791667 65.894737
std 17.181839 24.761286
min 18.000000 5.000000
25% 22.750000 58.750000
50% 33.000000 66.947368
75% 35.750000 85.500000
max 90.000000 94.000000
在查看了两个数据集的描述性统计后,观察到的变化如下:
-
Age
和Test_Score
在填补后从 20 增加到 24,表示缺失值已经成功填补。 -
均值和中位数的变化:均值年龄保持稳定,略微从 33.75 增加到 33.79。均值测试分数保持在 65.89 不变。中位数年龄从 27.50 增加到 33.00,反映了年龄分布的变化。中位数测试分数略微从 75.00 下降到 66.95。
-
Age
从 18.90 下降到 17.18,表明填补后的年龄变异性减小。Test_Score
的标准差也从 27.99 下降到 24.76,反映出测试分数的变异性减少。 -
Age
从 21.75 增加到 22.75,Test_Score
的 Q1 从 54.00 增加到 58.75。Age
从 38.50 略微下降到 35.75,而Test_Score
的 Q3 则保持相对稳定,从 87.50 略微下降到 85.50。
均值填补保持了整体均值,并通过填补缺失值增加了数据集的大小。然而,它减少了变异性(如Age
和Test_Score
的标准差减少所示),并改变了数据的分布(尤其是在四分位数上)。这些变化是均值填补的典型特征,因为它倾向于低估变异性并平滑数据中的差异,这可能会影响某些对数据分布敏感的分析。
现在,让我们继续进行中位数填补,看看它如何影响数据集。
中位数填补
中位数填补通过填补缺失值为数据集的中位数,即将数据按顺序排列后的中间值。中位数填补在存在离群值时更为稳健,且在数据分布偏斜时是一个不错的选择。它能够保持分布的形状,除非遇到复杂的分布。相关代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/4.median_imputation.py
找到。
让我们看一下中位数填补的代码示例:
df_median_imputed = df.copy()
以下代码行将df_median_imputed
数据框中Age
列的缺失值填补为原始df
数据框中Age
列的中位数。df['Age'].median()
参数计算Age
列的中位数(即中间值)。然后,fillna()
方法将Age
列中的任何NaN
值替换为这个中位数。inplace=True
参数确保更改直接应用到df_median_imputed
中,而不创建新的数据框:
df_median_imputed['Age'].fillna(df['Age'].median(), inplace=True)
同样,以下行填充了 Test_Score
中的任何缺失值:
df_median_imputed['Test_Score'].fillna(df['Test_Score'].median(), inplace=True)
让我们来看看经过中位数填充后的数据集:
print(df_median_imputed)
Age Test_Score
0 18.0 85.0
1 20.0 75.0
2 27.5 90.0
3 22.0 92.0
4 21.0 75.0
5 19.0 88.0
6 27.5 94.0
7 23.0 91.0
8 18.0 75.0
9 24.0 87.0
10 40.0 75.0
如我们所见,中位数填充已替换了 Age
特征(27.5)和 Test_Score
列(75)中的所有 NaN
值。更新后的分布如下。
图 8.4 – 中位数填充后的特征分布
我们从图表中可以看到,这两个变量的分布略有变化。让我们看看填充后数据集的描述性统计:
print(df_median_imputed.describe())
Age Test_Score
count 24.000000 24.000000
mean 32.708333 67.791667
std 17.345540 25.047744
min 18.000000 5.000000
25% 22.750000 58.750000
50% 27.500000 75.000000
75% 35.750000 85.500000
max 90.000000 94.000000
在查看了两个数据集的描述性统计后,观察到的变化在这里呈现:
-
Age
和Test_Score
在经过中位数填充后分别从 20(年龄)和 19(测试分数)增加到 24,表明缺失值已成功填充。 -
均值变化:填充后,均值年龄从 33.75 降低到 32.71,均值测试分数略微增加,从 65.89 增加到 67.79。这些变化反映了填充后数据的特性。
-
Age
从 18.90 降低到 17.35,表明年龄的变异性有所减小。Test_Score
的标准差也从 27.99 降低到 25.05,反映出在填充后测试成绩的变异性较小。 -
Age
从 21.75 稍微增加到 22.75,而Test_Score
的 Q1 从 54.00 增加到 58.75。Age
的 Q3(75%)从 38.50 降低到 35.75,Test_Score
的 Q3 也略微减少,从 87.50 降低到 85.50。 -
Age
保持稳定在 27.50,而Test_Score
的中位数也保持在 75.00,突出了填充后数据的中央趋势得到了保持。
中位数填充成功地填补了缺失值,同时保持了 Age
和 Test_Score
的中位数。这导致均值发生了轻微变化并减少了变异性,这是中位数填充的典型特征。中央趋势(中位数)得到了保持,这是中位数填充的一个重要优势,特别是在偏斜分布的情况下。但它也减少了数据的分布,这对某些类型的分析可能具有影响。
在接下来的部分,我们将使用到目前为止学到的关于填充的内容。我们还将增加一个额外的步骤,即标记数据集中缺失值所在的位置,供后续参考。
创建指示变量
指标变量补全,也叫标志变量或虚拟变量补全,涉及创建一个二进制指标变量,标记某个观测值在特定变量中是否缺失。这个单独的虚拟变量在缺失值时取值为 1,在观察到的值时取值为 0。当缺失值存在某种模式时,指标变量补全很有用,它能帮助你明确建模并捕捉缺失值的情况。记住,我们是在添加一个全新的变量,创建了一个更高维的数据集。在创建完指标变量后,它们的作用是提醒我们哪些值是被补全的,哪些值不是,然后我们可以使用任意方法(例如中位数或均值)补全数据集。
让我们看看这种补全方法的代码示例。和往常一样,你可以在仓库中看到完整的代码:
另外,记住我们在整章中使用的是完全相同的数据框,因此这里省略了数据框的创建部分:
df['Age_missing'] = df['Age'].isnull().astype(int)
df['Test_Score_missing'] = df['Test_Score'].isnull().astype(int)
这段代码在 df
数据框中创建了新列,用以指示 Age
和 Test_Score
列中是否有缺失值(NaN
)。df['Age'].isnull()
检查 Age
列中的每个值是否为 NaN
(缺失)。它返回一个布尔型的序列,其中 True
表示缺失值,False
表示非缺失值。.astype(int)
方法将布尔型序列转换为整数型序列,True
变为 1(表示缺失值),False
变为 0(表示非缺失值)。df['Age_missing']
数据框将这个整数序列存储在一个名为 Age_missing
的新列中。
类似地,df['Test_Score_missing']
是用来指示 Test_Score
列中的缺失值:
df_imputed['Age'].fillna(df_imputed['Age'].mean(), inplace=True)
df_imputed['Test_Score'].fillna(df_imputed['Test_Score'].mean(), inplace=True)
这段代码将 df_imputed
数据框中 Age
和 Test_Score
列中的缺失值填充为各自列的均值,就像我们在前一部分学习的那样。让我们看看经过指标变量补全后的数据集:
print(df_imputed)
Age Test_Score Age_missing Test_Score_missing
0 18.00 85.000000 0 0
1 20.00 65.894737 0 1
2 33.75 90.000000 1 0
3 22.00 92.000000 0 0
4 21.00 65.894737 0 1
5 19.00 88.000000 0 0
6 33.75 94.000000 1 0
7 23.00 91.000000 0 0
8 18.00 65.894737 0 1
9 24.00 87.000000 0 0
10 40.00 75.000000 0 0
从补全后的数据集可以看出,我们添加了两个指标变量(Age_missing
和 Test_Score_missing
),如果对应的变量缺失,则其值为 1,否则为 0。所以,我们主要标记了哪些原始行的值 是被补全的。
让我们看看指标变量的分布情况:
图 8.5 – 指标变量的分布
现在,让我们通过构建一些箱型图来探索指标变量与数据集中其他特征之间的关系:
import seaborn as sns
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.boxplot(x='Age_missing', y='Test_Score', data=df_imputed)
plt.title("Boxplot of Test_Score by Age_missing")
plt.subplot(1, 2, 2)
sns.boxplot(x='Test_Score_missing', y='Age', data=df_imputed)
plt.title("Boxplot of Age by Test_Score_missing")
plt.tight_layout()
plt.show()
创建的箱型图可以在图 8.6中看到:
图 8.6 – 箱型图比较指示变量与其他特征之间的关系
提示 – 如何读取箱型图
箱体范围:箱型图中的箱体表示四分位距(IQR),其中包含数据的中心 50%。箱内的值被视为典型值或正常范围内的值。
胡须:胡须从箱体延伸,显示典型值的范围。异常值通常定义为超出某一倍数(例如 1.5 倍)四分位距(IQR)的值。
异常值:超出胡须之外的个别数据点被视为潜在的异常值。异常值通常以单独的点或星号表示。
疑似异常值:有时,位于胡须之外的点可能被标记为疑似异常值,单独标记以表明它们是潜在的异常值,但并非极端值。
回到我们的例子,Test_Score
按 Age Missing
的箱型图显示,当数据中的年龄缺失时,Test_Score
的均值大约为 80,分布值介于 55 到 85 之间。当 Age
不缺失时,均值大约为 65,大部分值集中在 60 和 80 之间,少数异常值集中在 20 附近。现在,当分数缺失时,学生的平均年龄约为 20,而有分数的学生的平均年龄约为 35。
注意
在构建预测模型时,将指示变量作为附加特征以捕捉缺失值对目标变量的影响。评估包含和不包含指示变量的模型表现,以评估它们的贡献。
插补方法的比较
以下表格提供了根据数据特征和任务目标选择合适插补方法的指南。
记住,没有一种方法适用于所有情况!
插补方法 | 使用场景 | 优点 | 缺点 |
---|---|---|---|
均值插补 | 正态分布数据,缺失值为 MCAR 或 MAR | 简单易行,保留分布的均值 | 对异常值敏感,若缺失不是随机的,可能扭曲分布 |
中位数插补 | 偏斜或非正态分布数据,存在异常值 | 对异常值具有鲁棒性,保留分布的中位数 | 忽略变量之间的潜在关系,可能对非偏斜数据精度较低 |
指示变量插补 | 缺失数据中的系统性模式 | 捕捉缺失模式 | 增加维度性 假设缺失模式有意义,但这并不总是成立 |
删除行 | MCAR 或 MAR 缺失机制,存在异常值 | 保留现有数据结构,当缺失是随机时有效 | 减少样本量,如果缺失不是完全随机,可能导致偏倚结果 |
表 8.1 – 各种填补方法的比较
在提供的示例中,我们一致地对数据集的每一列应用了相同的填补方法。然而,正如我们所展示的那样,我们的分析和考虑是针对每一列单独量身定制的。这意味着我们可以根据每一列的具体特征和需求来定制填补策略。作为一个实际练习,花些时间尝试为数据集中的不同列使用不同的填补方法,并观察这些选择如何影响你的结果。
为了在我们已经建立的填补策略基础上进一步发展,必须认识到数据清理不仅仅是处理缺失值。数据预处理的另一个关键方面是识别和管理离群值。在接下来的部分,我们将深入探讨如何检测和处理离群值,确保我们的数据集尽可能准确和可靠。
检测和处理离群值
离群值是指在数据集中与大多数数据点显示的总体模式或趋势显著偏离的数据点。它们位于数据分布中心异常远的位置,并且可能对统计分析、可视化和模型性能产生重大影响。定义离群值包括识别那些不符合数据预期行为的数据点,并理解它们发生的背景。
离群值的影响
离群值虽然通常只占数据集的一小部分,但它们对数据集的影响不成比例,可能会破坏数据集的完整性。它们的存在可能会扭曲统计总结、误导可视化,并对模型的性能产生负面影响。
让我们深入探讨离群值如何扭曲事实:
-
扭曲的统计汇总:离群值可能会显著扭曲统计汇总,给出数据中心趋势的误导性印象:
-
均值和中位数:均值作为一种常见的集中趋势测量,可能会受到离群值的极大影响。一个远高于或低于其他数据点的离群值可能会将均值拉向它。另一方面,中位数是通过排序数据集中的中间值来确定的。它有效地作为数据的中心点,将数据分为两等部分,因此不容易受到极端值的影响。
-
方差和标准差:离群值可能会膨胀方差和标准差,使数据看起来比实际更为分散。这可能会误导数据的大多数变异性。
-
-
误导性的可视化:离群值可能会扭曲可视化的尺度和形状,导致误解:
-
箱型图:离群值可能会导致箱型图过度延伸,使数据的大部分看起来被压缩。这可能会使分布看起来不如实际情况那样分散。
-
直方图:异常值可能导致创建仅包含少数极端值的区间,导致其他区间显得不成比例地小,且分布形态被扭曲。
-
-
对模型性能的影响:异常值可能会对预测模型的性能产生负面影响:
-
回归:异常值可能会严重影响回归线的斜率和截距,从而导致模型过度受到极端值的影响。
-
聚类:异常值可能会影响聚类的中心点和边界,可能导致创建无法准确表示数据分布的聚类。
-
异常值可以根据维度分为单变量异常值和多变量异常值。在下一节中,我们将使用第一部分中的示例,看看如何处理单变量异常值。
识别单变量异常值
单变量异常值发生在单个变量中观察到极端值时,与其他变量的值无关。它们基于单个变量的分布进行检测,通常使用可视化或统计方法(如 Z 分数或四分位距)来识别。
在下一部分中,我们将构建最常见的可视化图形之一,用于识别异常值。
识别异常值的经典可视化方法
在深入讨论识别异常值的统计方法之前,我们可以先创建一些简单的可视化图形来帮助识别它们。我们一直使用的数据示例仍然适用于这一部分,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/6.outliers_visualisation.py
找到完整的代码。
我们从第一个可视化图——箱线图开始,其中异常值表现为箱须两侧的点。以下代码片段为每个变量创建箱线图:
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.title("Box Plot for 'Age'")
plt.boxplot(df['Age'].dropna(), vert=False)
plt.subplot(1, 2, 2)
plt.title("Box Plot for 'Test_Score'")
plt.boxplot(df['Test_Score'].dropna(), vert=False)
plt.tight_layout()
plt.show()
创建的箱线图如下所示:
图 8.7 – 箱线图用来识别异常值
在我们的示例中,我们可以看到Age
特征有一些明显的异常值。
另一个经典的图形是小提琴图,如图 8.8所示。小提琴图是一种强大的可视化工具,结合了箱线图和核密度图的特点。要创建小提琴图,请运行以下代码片段:
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.title("Violin Plot for 'Age'")
plt.violinplot(df['Age'].dropna(), vert=False)
plt.subplot(1, 2, 2)
plt.title("Violin Plot for 'Test_Score'")
plt.violinplot(df['Test_Score'].dropna(), vert=False)
plt.tight_layout()
plt.show()
创建的小提琴图如下所示:
图 8.8 – 小提琴图用来识别异常值
提示 – 如何阅读小提琴图:
小提琴的宽度:小提琴的宽度表示数据在不同值处的密度。较宽的部分表示在特定值处数据点的密度较高,意味着该值在总体中出现的概率较高;而较窄的部分表示概率较低。
箱线图元素:在小提琴图内,你可能会看到一个类似于传统箱线图的箱线图。箱子表示 IQR(四分位距),而中位数通常以一条水平线显示在箱内。胡须从箱子延伸,表示数据的范围。
核密度估计(KDE):小提琴图的整体形状是 KDE 的镜像表示。KDE 提供了数据分布的平滑表示,帮助你观察数据的峰值和谷值。
异常值:异常值可能表现为超出胡须末端的点,或超出小提琴整体形状的点。
现在我们已经看过这些图表,开始对Age
列中异常值的存在形成一些假设。下一步是使用一些统计方法验证这些假设,首先从 Z 分数方法开始。
Z 分数方法
Z 分数方法是一种统计技术,通过衡量单个数据点相对于均值的标准差偏离程度,用于识别数据集中的单变量异常值。数据点的 Z 分数使用以下公式计算:
Z = (X − Mean) / Standard Deviation
其中,X是数据点,Mean是数据集的平均值,Standard Deviation量化数据的离散程度。
通常,选择一个阈值 Z 分数来确定异常值。常用的阈值是Z > 3或Z < −3,表示偏离均值超过三个标准差的数据点被视为异常值。
让我们回到之前的代码示例,计算Age
和Test_Score
列的 Z 分数。我们将继续之前开始的示例。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/7.identify_univariate_outliers.py
找到完整的代码。
让我们计算 Z 分数:
z_scores_age = np.abs(stats.zscore(df['Age'].dropna()))
stats.zscore(df['Age'].dropna())
函数计算Age
列的 Z 分数。Z 分数表示一个数据点距离均值多少个标准差。dropna()
函数用于在计算 Z 分数之前排除NaN
值:
z_scores_test_score = np.abs(stats.zscore(df['Test_Score'].dropna()))
np.abs()
函数用于计算 Z 分数的绝对值。这是因为 Z 分数可以为负(表示值低于均值)或为正(表示值高于均值)。通过使用绝对值,我们只关注偏离均值的大小,而不考虑方向。
z_threshold = 3
outliers_age = np.where(z_scores_age > z_threshold)[0]
outliers_test_score = np.where(z_scores_test_score > z_threshold)[0]
np.where(z_scores_age > z_threshold)[0]
识别年龄
列中 Z-score 大于3
的那些数据点的索引。最后的[0]
用于提取索引作为数组。outliers_age
和outliers_test_score
变量分别存储年龄
和测试成绩
列中的异常值数据点索引。
如果我们绘制每个观察值和特征的 Z-scores,就可以开始发现一些异常值了。
图 8.9 – 使用 Z-score 进行异常值检测
在这些 Z-score 的散点图中,每个点代表一个数据点的 Z-score。红色虚线表示所选的 Z-score 阈值(在此案例中为3
)。异常值被标识为高于此阈值的点。如我们所见,在年龄
上,清晰地捕捉到了一个异常值。
如何选择合适的 Z-score 阈值?
Z-score 告诉你一个数据点距离均值有多少个标准差。在正态分布中,以下是成立的:
-
大约 68%的数据落在均值的一个标准差范围内。
-
大约 95%的数据落在两个 标准差内。
-
大约 99.7%的数据落在三个 标准差内。
这意味着3
的 Z-score 阈值通常被使用,因为它捕捉到的是极度偏离均值的值,识别出最极端的异常值。在完美的正态分布中,只有 0.3%的数据点会有 Z-score 大于 3 或小于-3。这使得它成为检测不太可能属于正常数据分布的异常值的合理阈值。
现在,除了 Z-score,另一种常见的方法是 IQR,我们将在接下来的部分讨论这一方法。
IQR 方法
IQR 是统计离散度的一个衡量标准,表示数据集中 Q1 和 Q3 之间的范围。IQR 是一种稳健的离散度衡量方式,因为它对异常值的敏感性较低。此时,可以清楚地看出 IQR 是基于四分位数的。四分位数将数据集分为几个区间,由于 Q1 和 Q3 对极端值不那么敏感,因此 IQR 不容易受到异常值的影响。另一方面,标准差会受到每个数据点与均值偏差的影响。偏差较大的异常值会对标准差产生不成比例的影响。
提示 – 如何计算 IQR
计算 Q1(25 百分位数):确定数据中有 25%落在其下方的值。
计算 Q3(75 百分位数):确定数据中有 75%落在其下方的值。
计算 IQR:IQR = Q3 - Q1。
使用 IQR 识别潜在的异常值,请按以下步骤操作:
-
按照以下方式计算上下界:下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR。
-
任何低于或高于上下界的数据点都被视为潜在的异常值。
需要注意的是,乘数的选择(在本例中为1.5
)是有些任意的,但在实际中已经广泛采用。调整这个乘数可以使得该方法对潜在离群值的敏感度更高或更低。例如,使用更大的乘数会导致边界更广,可能会识别出更多的潜在离群值,而较小的乘数则会使该方法对离群值的敏感度降低。
我们将使用之前的脚本,脚本可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/7.identify_univariate_outliers.py
找到。让我们看看如何计算 IQR 并识别离群值:
def identify_outliers(column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
return outliers
这段代码定义了一个函数,用于通过 IQR 方法识别 DataFrame 中任何列的离群值。它计算 IQR,设定正常数据的上下限,然后过滤出那些列中的值落在这些边界之外的行。
然后,我们来识别并打印Age
(年龄)列中的离群值:
age_outliers = identify_outliers('Age')
print("Outliers in 'Age':")
print(age_outliers)
识别并打印Test_Score
(考试成绩)列中的离群值:
test_score_outliers = identify_outliers('Test_Score')
print("\nOutliers in 'Test_Score':")
print(test_score_outliers)
运行这段代码后,我们可以在打印语句中看到基于Age
列识别出的离群值/行:
Age Test_Score
76.0 10.0
90.0 5.0
如前所述,IQR(四分位距)的简便性以及其对离群值的稳健性使其在各种分析场景中非常受欢迎。然而,它也有一定的缺点。一个限制是信息的丢失,因为 IQR 仅考虑数据集的中央 50%,忽略了整个范围。此外,IQR 对样本大小的敏感性,尤其是在较小的数据集里,可能会影响其反映数据真实分布的准确性。
最后,我们将简要讨论如何利用领域知识来识别离群值。
领域知识
为了更好地理解领域知识在离群值检测中的应用,我们以考试成绩为例。假设数据集代表的是学生的考试成绩,并且根据教育标准,考试成绩应该落在 0 到 100 的范围内。任何超出此范围的成绩都可以被认为是离群值。通过利用教育领域的知识,我们可以设定这些边界来识别潜在的离群值。例如,如果某个成绩记录为 120,那么它很可能会被标记为离群值,因为它超出了最高分 100 的范围。同样,负数的成绩或低于 0 的成绩也会被视为离群值。以这种方式整合领域知识,使我们能够为离群值检测设定有意义的阈值,确保分析符合教育领域中的预期规范。
处理单变量离群值
处理单变量异常值是指识别、评估和管理那些显著偏离数据集典型模式或分布的个别变量数据点的过程。其目的是减少这些极端值对数据产品的影响。
处理单变量异常值有几种方法。我们将从删除开始,始终使用本章开头的示例进行操作。
删除异常值
删除异常值是指从数据集中移除那些被认为异常极端或偏离数据整体模式的数据点。删除异常值有其利弊。一方面,这是处理极端值的最简单方法;另一方面,它会导致样本量的减少,并可能丧失宝贵的信息。此外,如果异常值不是错误数据,而是反映数据的合理波动,删除它们可能会引入偏差。
回到我们的示例,在使用均值填充缺失数据并计算 IQR 之后,我们删除了超过异常值阈值的异常值。让我们来看一下执行这些步骤的代码;你也可以在仓库中找到它:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/8.handle_univariate_outliers_deletions.py
。
让我们计算 IQR 并利用它来设定正常数据范围的上下界限:
(IQR) Q1 = df['Test_Score'].quantile(0.25)
Q3 = df['Test_Score'].quantile(0.75)
IQR = Q3 - Q1
outlier_threshold = 1.5
让我们定义下限和上限异常值界限。任何超出此范围的值都将被标记为异常值:
lower_bound = Q1 - outlier_threshold * IQR
upper_bound = Q3 + outlier_threshold * IQR
最后一行过滤了 DataFrame(df
),仅保留Test_Score
值在计算出的下限和上限之间的行:
df_no_outliers = df[(df['Test_Score'] >= lower_bound) & (df['Test_Score'] <= upper_bound)].copy()
在以下图表中,我们可以看到删除异常值后的更新分布图:
图 8.10 – 删除异常值后的分布图
让我们看看删除异常值后的描述性统计数据:
Age Test_Score
count 22.000000 22.000000
mean 29.272727 71.203349
std 8.163839 17.794339
min 18.000000 20.000000
25% 22.250000 65.894737
50% 31.000000 71.000000
75% 33.937500 86.500000
max 45.000000 94.000000
删除异常值后观察到的变化如下所示:
-
平均年龄变化:删除异常值后,平均年龄从 33.75 略微下降至约 29.27。这一变化表明,删除的异常值是年龄较大的个体。
-
年龄标准差变化:年龄的标准差从 17.18 降至 8.16,表明删除异常值后年龄的分布略微变窄,可能是因为原数据中的异常值导致了较大的变异性。
-
最小和最大年龄值:最小年龄保持不变,仍为 18 岁,而最大年龄从 90 岁降至 45 岁,表明在处理异常值时,年龄较大的个体(潜在的异常值)被移除。
-
平均测试成绩变化:在删除异常值后,平均测试成绩从 65.89 轻微上升至 71.20,表明被删除的异常值是低分,拉低了原始的均值。
-
测试成绩的标准差变化:标准差从 24.76 降至 17.79,表明测试成绩的分布变得更为集中。
-
最低和最高测试成绩:最低测试成绩从 5.00 上升到 20.00,而最高测试成绩保持不变,为 94.00。这表明极低的分数在处理异常值时被移除。
删除异常值导致了均值和标准差的下降,同时平均测试成绩略有上升。虽然删除异常值可以提高数据质量,尤其是当异常值由于数据输入错误或测量不准确时,但它也会减少数据集的变异性。如果异常值代表了总体中的真实变异性,删除它们可能会扭曲数据的整体情况。因此,必须谨慎考虑异常值是否为真实数据点或错误数据。
注意
一些统计模型假设数据符合正态分布,因此可能对异常值非常敏感。删除异常值有助于满足某些模型的假设。因此,在删除之前,你需要更好地理解你正在解决的问题以及要使用的技术。
如果你不想完全删除数据中的异常值,还有其他方法可以处理它们。在接下来的部分,我们将讨论异常值的修剪和温莎化处理。
修剪
修剪是指从分布的两端删除一定比例的数据,然后计算均值。对于修剪,我们需要定义修剪比例,这个比例表示在计算修剪后的均值时,从分布的两端去除的数据比例。它用于排除一定比例的极端值(异常值)在均值计算中的影响。修剪比例的值介于 0 和 0.5 之间,满足以下条件:
-
0 表示不进行修剪(包括所有数据点)
-
0.1 表示从每个尾部修剪 10%的数据
-
0.2 表示从每个尾部修剪 20%的数据
-
0.5 表示从每个尾部修剪 50%的数据(排除最极端的值)
在我们的案例中,分析表明Age
列存在最显著的异常值。为此,我们决定通过排除Age
列中最上面和最下面的百分位数来修剪数据集。以下示例代码演示了这一修剪过程。我们仍在使用相同的数据集,因此这里跳过了 DataFrame 的创建。不过,你可以在以下链接查看完整代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/9.trimming.py
。
让我们看看下面的代码片段,它创建了一个新的数据框(df_trimmed
),只包括Age
(年龄)值位于第 10 百分位和第 90 百分位之间的行。这实际上去除了Age
列中最低的 10%和最高的 10%的值:
df_trimmed = df[(df['Age'] >= df['Age'].quantile(0.1)) & (df['Age'] <= df['Age'].quantile(0.9))]
现在让我们来计算每一列的修剪均值:
df_trimmed_mean = df_trimmed.mean()
在修剪数据后,最后一行计算了df_trimmed
数据框中每列的均值。修剪后计算的均值被称为修剪均值。它表示去除最极端的 20%(每侧 10%)后的中央 80%数据的平均值。
注意
请记住,修剪比例是平衡修剪均值的稳健性与排除数据量之间的一个方式。你可能需要尝试不同的比例,以找到适合你数据的平衡点。
让我们看看修剪后的更新分布:
图 8.11 – 在 10% 阈值下去除异常值后的分布图
让我们也来看看更新后的数据统计信息:
Age Test_Score
count 18.000000 18.000000
mean 30.222222 69.309942
std 6.757833 18.797436
min 20.000000 20.000000
25% 24.000000 60.723684
50% 32.875000 66.947368
75% 33.937500 84.750000
max 41.000000 94.000000
在原始数据集中,Age
列的均值为 33.75,标准差为 17.18,而修剪后的数据表现为更高的均值 30.22,且标准差大幅降低至 6.76。修剪数据中的最低年龄值从 18 增加到 20,表明去除了低值异常值。最高年龄值从 90 下降到 41,表明排除了高值异常值。
对于Test_Score
(测试分数)列,原始数据集中的均值为 65.89,标准差为 24.76。在修剪后的数据中,均值上升至 69.31,标准差下降至 18.80,表明测试分数的分布范围变窄。最低测试分数从 5 增加到 20,表明去除了低值异常值,而最高测试分数保持在 94 不变。
总体而言,去除异常值导致了数据的集中趋势(均值)和分布范围(标准差)发生变化,Age
(年龄)和Test_Score
(测试分数)均如此。这表明修剪后的数据集变得更加集中在中间值周围,极端值被移除。
记住!
虽然修剪有助于减少极端值的影响,但它也意味着丢弃一部分数据。这可能导致信息丢失,而修剪后的变量可能无法完全代表原始数据集。
在接下来的部分,我们将介绍一种稍微不同的处理异常值的方法,叫做温莎化。
温莎化
与直接去除极端值的修剪不同,winsorizing(温莎化)是通过用较不极端的值替代它们。极端值被替换为接近分布中心的值,通常是在指定的百分位数。温莎化在你希望保留数据集的大小并帮助保持数据分布的整体形态时非常有用。
回到我们的示例用例,看看代码。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/10.winsorizing.py
找到完整的代码:
winsorizing_fraction = 0.1
winsorizing_fraction
设置为0.1
,表示在数据分布的两端调整的数据比例。它以百分比的形式表示,值通常在 0 和 50%之间。确定 Winsor 化比例的过程涉及考虑你希望减少极端值的影响程度。一个常见的选择是将两端的某个百分比进行 Winsor 化,例如 5%或 10%。
这里还需要了解的一点是,Winsor 化过程是针对每一列单独且独立地进行的。记住:我们在这里以单变量的方式处理离群值:
df_winsorized = df.apply(lambda x: mstats.winsorize(x, limits=[winsorizing_fraction, winsorizing_fraction]))
limits=[winsorizing_fraction, winsorizing_fraction]
参数指定了从分布两端 Winsor 化的数据比例。这里从下端和上端各调整 10%。极端值(最低的 10%和最高的 10%)将被替换为指定范围内的最近值,从而减少它们对统计量的影响。
这里展示了 Winsor 化后的更新分布:
图 8.12 – 经 Winsor 化后的离群值分布图
让我们看看数据的更新统计信息:
Age Test_Score Age_Winsorized
count 24.000000 24.000000 24.000000
mean 33.750000 65.894737 30.666667
std 17.181575 24.761286 8.857773
min 18.000000 5.000000 19.000000
25% 22.750000 58.750000 22.750000
50% 32.875000 66.947368 32.875000
75% 35.750000 85.500000 35.750000
max 90.000000 94.000000 45.000000
Age
列的均值从 33.75 降至 30.67,表明由于极端高值被调整,数据分布向较低值偏移。标准差也从 17.18 显著降低至 8.86,说明数据集的变异性减少。最小值从 18 略微增加到 19,最大值从 90 降至 45,反映了极端值的限制。
至于Test_Score
,Winsor 化后均值保持在 65.89,标准差保持在 24.76,表明测试分数的变异性未受 Winsor 化过程的影响。最大值保持不变,依然为 94,显示上端极端值没有发生变化。
总体而言,对Age
列进行 Winsor 化后,数据的分布变得更加集中,标准差的减小也证明了这一点。Winsor 化成功地减少了极端值在Age
列中的影响,使数据更加集中于中间范围。对于Test_Score
列,Winsor 化并未对分布产生影响,可能是因为极端值已经在接受范围内。
接下来,我们将探讨如何通过数学变换来最小化离群值的影响。
数据变换
应用对数或平方根等数学变换是处理偏斜数据或稳定方差的常见技术。
提醒
偏度是分布不对称的度量。正偏度表示分布有右尾,而负偏度表示分布有左尾。
当数据右偏(正偏度)时,即大部分数据点集中在左侧,右侧有少数较大值时,应用对数变换会压缩较大值,使分布更对称,更接近正态分布。
类似于对数变换,平方根变换用于减少较大值的影响,并使分布更对称。当分布的右尾包含极端值时,特别有效。
另一点需要注意的是,当数据的方差随均值增加(异方差性)时,对数和平方根变换可以压缩较大值,减少极端值的影响,并稳定方差。
让我们回到我们的例子,并对数据集的两列进行对数变换。如往常一样,你可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/11.data_transformation.py
找到完整的代码。
让我们对 Age
和 Test_Score
应用对数变换:
df_log_transformed = df.copy()
df_log_transformed['Age'] = np.log1p(df_log_transformed['Age'])
df_log_transformed['Test_Score'] = np.log1p(df_log_transformed['Test_Score'])
np.log1p
是 NumPy 中的一个函数,用于计算 Age
和 Test_Score
列中每个值的 1 + x 的自然对数。log1p
函数用于处理数据集中的零值和负值,而不会出现错误,相比简单的对数函数 (np.log
) 更为实用。在处理包含零值或非常小数值的数据时特别有用。这种变换可以减小偏斜度,并使分布更接近正态分布,这对于各种假设数据正态分布的统计技术非常有用。
更多变换的实施
在代码中,你会发现对数据应用了对数和平方根变换。花些时间探索和理解这两种方法之间的差异。通过考虑每种变换对数据分布和方差的影响,评估哪种变换更适合你的数据。
更新后的分布在以下图表中展示,其中对 Age
列进行了对数变换,对 Test_Score
列进行了平方根变换:
图 8.13 – 对数和平方根变换后的分布图
让我们也来看一下数据的更新统计信息:
Age Test_Score
count 24.000000 24.000000
mean 3.462073 4.059624
std 0.398871 0.687214
min 2.944439 1.791759
25% 3.167414 4.090143
50% 3.522344 4.218613
75% 3.603530 4.460095
max 4.510860 4.553877
描述性统计显示了对Age
变量进行对数转换和对Test_Score
变量进行平方根转换的影响。在转换之前,原始数据集中的Age
呈右偏分布,均值为 33.75,标准差较大,为 17.18。Test_Score
的均值为 65.89,范围从 5 到 94,标准差为 24.76,表明测试成绩分布较广。
在应用了转换后,两个变量的分布明显发生了变化:
-
对
Age
进行对数转换后,值的分布被压缩,标准差从原始的 17.18 降至 0.40。转换后的值范围从 2.94 到 4.51,显示出极端值的压缩。 -
对于
Test_Score
,对数据进行对数转换后,值的分布变得更加均匀,标准差从 24.76 降低到 0.69。数据变得更加紧凑且对称,范围从 1.79 到 4.55。
这些转换对两个变量产生了明显的平滑效应,减少了偏斜度和变异性。这一点从标准差的减少和范围的缩小可以看出,使得数据更加对称,接近正态分布。
然而,需要注意的是,转换,特别是对数转换,会压缩数值的尺度,可能影响可解释性。虽然它们通过减少偏斜度和异方差性,有助于满足统计方法的假设,但转换后的数据可能比原始数据尺度更难以理解。尽管如此,这种转换在准备回归模型或其他假设数据呈正态分布的分析时,仍然非常有用。
注意
请记住,对数转换不适用于包含零或负值的数据,因为对数在这些值上是未定义的。
本章的这一部分最后,我们汇总了一个表格,概述了处理异常值时使用的各种方法。该表格突出了每种技术的最佳使用场景,并提供了它们各自的优缺点概览。
技术 | 何时使用 | 优点 | 缺点 |
---|---|---|---|
修剪 | 轻度异常值,保留整体数据结构 | 保留大部分数据集,保持数据完整性 | 减少样本量,可能影响代表性,修剪百分比的选择可能带有随意性 |
温莎化 | 中度异常值,保留整体数据 | 保持数据分布,减轻极端值的影响 | 改变数据值;可能扭曲分布;需要指定修剪的限度 |
删除数据 | 严重异常值 | 移除极端值的影响,简化分析 | 减少样本量,可能丧失信息;可能使结果偏向中心趋势 |
变换 | 偏斜或非正态分布 | 稳定方差,使数据更对称,适应传统统计技术 | 解释挑战,结果可能不太直观,变换方法的选择是主观的 |
表 8.2 – 单变量方法处理异常值的总结
在探讨了各种处理单变量异常值的技术后,包括从简单到复杂的方法,接下来的部分将深入探讨在处理含有异常值的数据时,一般更为偏好的不同统计量。
稳健统计
使用如中位数和中位数绝对偏差(MAD)等稳健的统计量而非均值和标准差,可以减少异常值的影响。
在处理包含异常值或偏斜分布的数据集时,选择稳健的统计量对于获取准确且具有代表性的总结至关重要。使用稳健的量度,如中位数和 MAD,在极端值可能影响传统量度(如均值和标准差)的场景中证明了其优势。中位数是排序后数据的中间值,它对异常值不那么敏感,提供了一个更可靠的集中趋势测量。此外,MAD 评估数据的分布,并且对异常值具有稳健性,从而进一步确保数据集变异性的更准确表示。
MAD
MAD 是一种衡量统计离散度的指标,用于量化数据集的离散程度或分布。它是通过计算每个数据点与数据集的中位数之间的绝对差的中位数来得出的。
该表总结了使用中位数和 MAD 与使用均值和标准差时的关键考虑因素、优缺点:
标准 | 中位数 和 MAD | 均值和 标准差 |
---|---|---|
何时使用 | 异常值的存在 | 正态或对称分布 |
偏斜分布 | 测量的精确性 | |
优点 | 对异常值的稳健性 | 对正态分布的效率 |
对偏斜数据的适用性 | 解释的简便性 | |
缺点 | 没有异常值时缺乏敏感性 | 对异常值敏感 |
在存在异常值的情况下不稳健 | ||
考虑因素 | 当需要稳定的集中趋势时很有用 | 适用于极端值最少或没有极端值的数据集 |
提供在正态分布中的精确度量 |
表 8.3 – 哪些统计方法在处理异常值时更有效
本章接下来的部分将讨论如何识别多变量异常值。
识别多变量异常值
多元离群值发生在一个观测值在多个变量的上下文中同时是极端的。这些离群值不能仅通过分析单个变量来检测;相反,它们需要考虑变量之间的相互作用。检测多元离群值涉及在更高维空间中评估数据点。在接下来的部分中,我们将概述不同的方法来识别多元离群值,并为每种方法提供代码示例。
马哈拉诺比斯距离
马哈拉诺比斯距离是一种统计量,用于识别多元数据中的离群值。它考虑了变量之间的相关性,并计算每个数据点与数据集均值在缩放空间中的距离。然后,将这个距离与一个阈值进行比较,以识别那些显著偏离多元均值的观测值。
对于这个示例,我们创建了一个新的数据集,包含一些多元学生数据,以便我们可以以最佳方式展示这一技术。完整代码可以在仓库中查看:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/12.mahalanobis_distance.py
。该过程的关键步骤如下:
-
让我们首先导入所需的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import chi2 from mpl_toolkits.mplot3d import Axes3D
-
让我们生成多元学生数据:
np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100)
我们从一个多元正态分布中生成了 100 个样本,指定均值向量为
[0, 0]
,协方差矩阵为[[1, 0.5], [0.5, 1]]
。 -
让我们引入离群值并创建数据框:
outliers = np.array([[8, 8], [9, 9]]) data = np.concatenate([data, outliers]) df = pd.DataFrame(data, columns=['X1', 'X2'])
-
以下函数根据均值和协方差矩阵的逆计算每个数据点的马哈拉诺比斯距离:
def mahalanobis_distance(x, mean, inv_cov_matrix): centered_data = x - mean mahalanobis_dist = np.sqrt(np.dot(centered_data, np.dot(inv_cov_matrix, centered_data))) return mahalanobis_dist
-
计算数据集的均值、协方差矩阵和协方差矩阵的逆:
mean = np.mean(df[['X1', 'X2']], axis=0) cov_matrix = np.cov(df[['X1', 'X2']], rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix)
-
为每个数据点计算马哈拉诺比斯距离,并将其作为新列添加到数据框中:
df['Mahalanobis_Distance'] = df.apply(lambda row: mahalanobis_distance(row[['X1', 'X2']], mean, inv_cov_matrix), axis=1)
-
设置离群值检测的显著性水平:
alpha = 0.01
显著性水平(
alpha
)表示在零假设为真的情况下拒绝它的概率,在本上下文中,它指的是错误地将数据点识别为离群值的概率。alpha
常见的选择值为0.01
,意味着错误地将正常数据点归类为离群值的概率为 1%。较低的alpha
值使得离群值检测更加保守,减少假阳性(正常点被标记为离群值)。相反,较高的alpha
值使检测更加宽松,可能会识别出更多的离群值,但增加了假阳性的机会。 -
接下来,我们设置卡方阈值:
chi2_threshold = chi2.ppf(1 - alpha, df=2) # df is the degrees of freedom, which is the number of features
卡方阈值是从卡方分布中得到的临界值,用于定义异常值检测的截止点。
chi2.ppf
函数计算卡方分布的百分位点函数(累积分布函数的反函数)。自由度等于马氏距离计算中使用的特征或变量的数量。在这种情况下,是2
(对于 X1 和 X2)。卡方阈值用于确定超过该值的马氏距离被认为过高,表示相应的数据点是异常值。例如,使用alpha = 0.01
时,表示你正在寻找一个阈值,超过该阈值的只有 1%的数据点,假设数据是正态分布的。 -
这一步涉及将每个数据点的马氏距离与卡方阈值进行比较,以确定它是否为异常值:
outliers = df[df['Mahalanobis_Distance'] > chi2_threshold] df_no_outliers = df[df['Mahalanobis_Distance'] <= chi2_threshold]
距离大于阈值的数据点被标记为异常值,并与其余数据分开。
-
现在让我们来可视化异常值:
fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection='3d') ax.scatter(df_no_outliers['X1'], df_no_outliers['X2'], df_no_outliers['Mahalanobis_Distance'], color='blue', label='Data Points') ax.scatter(outliers['X1'], outliers['X2'], outliers['Mahalanobis_Distance'], color='red', label='Outliers') ax.set_xlabel('X1') ax.set_ylabel('X2') ax.set_zlabel('Mahalanobis Distance') ax.set_title('Outlier Detection using Mahalanobis Distance') plt.legend() plt.show()
在下面的图表中,我们可以看到所有数据点在 3D 空间中的投影,并且可以看到标记为x的异常值:
图 8.14 – 使用马氏距离绘制的数据
注意
在你的笔记本电脑上运行可视化程序,以便能够看到这个空间并在 3D 视图中移动,挺酷的!
从 3D 图中可以看出,数据中的异常值非常容易识别。马氏距离在处理涉及多个变量的数据集时最为有效,因为它考虑了变量之间的均值和协方差,并能够识别在单个变量中可能无法显现的异常值。在变量具有不同单位或尺度的情况下,马氏距离可以规范化变量间的距离,从而提供更有意义的异常值度量。与单变量方法不同,马氏距离对变量之间的关系非常敏感。它捕捉每个数据点与数据分布中心的距离,同时考虑了变量之间的相关性。
在多变量部分的下一节中,我们将讨论聚类方法如何帮助我们检测异常值。
聚类技术
聚类方法,如 k-means 或层次聚类,可以用于将相似的数据点分组。那些不属于任何聚类或形成小聚类的数据点,可能会被视为多变量异常值。
一种常见的异常值检测方法是使用基于密度的空间聚类应用与噪声(DBSCAN)算法。DBSCAN 可以识别密集的数据点簇,并将异常值分类为噪声。DBSCAN 的优势在于它不需要事先指定聚类的数量,并且能够基于密度有效地识别异常值。它是一个相对简单但功能强大的异常值检测方法,尤其在聚类可能不完全分离或异常值形成孤立点的情况下表现良好。
让我们深入了解 DBSCAN 的代码。与往常一样,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/13.clustering.py
的代码库中找到完整的代码:
-
让我们导入所需的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler
-
让我们生成用于该方法的示例数据集。数据集由 100 个样本组成,来自一个多元正态分布,均值向量为
[0, 0]
,协方差矩阵为[[1, 0.5], [0.5, 1]]
。这将创建一个围绕原点的正态分布点簇,其中各特征之间存在一定的相关性:np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100) outliers = np.random.multivariate_normal(mean=[8, 8], cov=[[1, 0], [0, 1]], size=10) data_with_outliers = np.vstack([data, outliers])
-
让我们将数据转换为 DataFrame:
df = pd.DataFrame(data_with_outliers, columns=['Feature1', 'Feature2'])
-
通过去除均值并缩放到单位方差来标准化数据。
sklearn.preprocessing
中的StandardScaler
用于拟合和转换数据。标准化确保所有特征在距离计算中贡献相等,通过将它们缩放到均值为 0、标准差为 1 来实现。这对于基于距离的算法(如 DBSCAN)尤其重要:scaler = StandardScaler() data_scaled = scaler.fit_transform(df)
-
应用 DBSCAN 进行异常值检测。
eps=0.4
设置了被视为同一邻域的点之间的最大距离,min_samples=5
指定了形成密集区域所需的最小点数。DBSCAN 是一种聚类算法,可以识别不属于任何簇的异常值。DBSCAN 将标记为-1
的点视为异常值。eps
和min_samples
参数的选择会显著影响异常值的检测,这些值可能需要根据具体数据集进行调优:dbscan = DBSCAN(eps=0.4, min_samples=5) df['Outlier'] = dbscan.fit_predict(data_scaled)
在下图中,我们将所有数据点绘制在二维空间中,可以看到图表右侧的异常值:
图 8.15 – 基于 DBSCAN 的异常值检测聚类
在 DBSCAN 中有一个关键参数需要调整:eps
。eps
(epsilon)参数本质上定义了数据点周围的半径,所有位于该半径内的其他数据点都被视为该数据点的邻居。
在执行 DBSCAN 聚类时,算法首先选择一个数据点,并识别所有距离该点在eps
范围内的数据点。如果在这个距离内的数据点数量超过指定的阈值(min_samples
),则选中的数据点被视为核心点,所有在其 epsilon 邻域内的点将成为同一聚类的一部分。然后,算法通过递归地查找邻居的邻居,直到没有更多的点可以添加为止,从而扩展聚类。
eps
的选择取决于数据集的特定特征和所需的聚类粒度。它可能需要一些实验和领域知识来找到适合的eps
值。
使用 k-means 代替 DBSCAN 提供了另一种方法。K-means 是一种基于质心的聚类算法,需要预先指定聚类数量,因此必须有先验知识或进行探索性分析,以确定k的合适值。虽然它对异常值敏感,但 k-means 的简洁性和计算效率使其在某些场景中成为一个有吸引力的选择。当聚类之间分离良好并且具有相对均匀的结构时,k-means 可能特别适用。然而,必须注意,k-means 可能在处理不规则形状或重叠的聚类时表现不佳,并且在试图最小化平方距离和时,可能会受到异常值的影响。
发现多变量异常值后,我们需要决定如何处理这些异常值。这是下一部分的重点。
处理多变量异常值
处理多变量异常值涉及到解决在多个变量背景下显著偏离的数据点。在本章的这一部分,我们将提供不同方法来处理多变量异常值的解释和代码示例。
多变量修剪
该方法涉及基于多个变量的综合评估来限制极端值。例如,修剪的限制可以通过考虑马哈拉诺比斯距离来确定,马哈拉诺比斯距离考虑了变量之间的相关性。这种技术在处理跨多个变量存在异常值的数据集时尤其有用。其思路是在减少极端值影响的同时,保留数据的整体结构。
在这个例子中,我们将继续处理马哈拉诺比斯距离示例中的数据,在计算完马哈拉诺比斯距离后,我们将丢弃超过阈值的异常值。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter08/14.multivariate_trimming.py
的代码库中找到完整代码:
-
让我们从导入库开始:
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy.stats import chi2 from mpl_toolkits.mplot3d import Axes3D
-
让我们生成多变量学生数据。
np.random.seed(42) data = np.random.multivariate_normal(mean=[0, 0], cov=[[1, 0.5], [0.5, 1]], size=100) outliers = np.array([[8, 8], [9, 9]]) data = np.concatenate([data, outliers]) df = pd.DataFrame(data, columns=['X1', 'X2'])
-
定义计算马氏距离的函数,该距离衡量数据点与分布均值的距离,考虑特征之间的相关性:
def mahalanobis_distance(x, mean, inv_cov_matrix): centered_data = x - mean mahalanobis_dist = np.sqrt(np.dot(centered_data, np.dot(inv_cov_matrix, centered_data))) return mahalanobis_dist
-
为异常值检测准备数据:
df[['X1', 'X2']] = df[['X1', 'X2']].astype(float) mean = np.mean(df[['X1', 'X2']], axis=0) cov_matrix = np.cov(df[['X1', 'X2']], rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix)
-
计算每个数据点的马氏距离:
df['Mahalanobis_Distance'] = df.apply(lambda row: mahalanobis_distance(row[['X1', 'X2']], mean, inv_cov_matrix), axis=1)
-
设置异常值检测的阈值:
alpha = 0.1 chi2_threshold = chi2.ppf(1 - alpha, df=2)
-
过滤数据框,分离出异常值与其余数据。
outliers = df[df['Mahalanobis_Distance'] > chi2_threshold] df_no_outliers = df[df['Mahalanobis_Distance'] <= chi2_threshold]
在处理异常值之前,让我们先展示分布图。
图 8.16 – 包含多变量异常值的分布图
原始数据的描述性统计如下:
X1 X2
count 102.000000 102.000000
mean 0.248108 0.281463
std 1.478963 1.459212
min -1.852725 -1.915781
25% -0.554778 -0.512700
50% 0.108116 0.218681
75% 0.715866 0.715485
max 9.000000 9.000000
在删除被认为是多变量异常值的数据后,我们可以观察到以下分布的变化:
图 8.17 – 移除多变量异常值后的分布图
最后,让我们来看看更新后的描述性统计:
X1 X2 Mahalanobis_Distance
count 100.000000 100.000000 100.000000
mean 0.083070 0.117093 1.005581
std 0.907373 0.880592 0.547995
min -1.852725 -1.915781 0.170231
25% -0.574554 -0.526337 0.534075
50% 0.088743 0.200745 0.874940
75% 0.699309 0.707639 1.391190
max 1.857815 2.679717 2.717075
在修剪掉异常值之后,让我们讨论数据中观察到的变化:
-
移除异常值后,观察的数量从 102 降至 100,因此我们丢弃了两条记录。
-
在
X1
列中,均值从 0.248 降至 0.083,标准差从 1.479 降至 0.907。 -
在
X2
列中,均值从 0.281 降至 0.117,标准差从 1.459 降至 0.881。 -
X1
和X2
的最大值分别被限制在 1.857815 和 2.679717,表明极端异常值已被移除。
总的来说,移除异常值后,数据集的变异性减小,尤其是在均值和标准差方面。极端值可能对分析产生偏差的风险已被减轻。
让我们总结本章的关键要点。
总结
在本章中,我们深入探讨了缺失值和异常值的处理。我们理解了缺失值如何扭曲我们的分析,并学习了从简单的均值插补到先进的基于机器学习的插补技术等多种插补方法。同样,我们认识到异常值可能会偏移我们的结果,并深入研究了在单变量和多变量背景下检测和管理异常值的方法。通过结合理论和实践示例,我们对确保数据质量和可靠性的考虑、挑战及策略有了更深入的理解。
拥有这些见解后,我们现在可以进入下一章,讨论特征的缩放、归一化和标准化。
第九章:归一化与标准化
特征缩放、归一化和标准化是机器学习中的重要预处理步骤,能够帮助确保机器学习模型能够有效地从数据中学习。这些技术解决了与数值稳定性、算法收敛性、模型性能等相关的问题,最终有助于在数据分析和机器学习任务中获得更好、更可靠的结果。
在本章中,我们将深入探讨以下主题:
-
将特征缩放到一个范围
-
Z-score 缩放
-
鲁棒缩放
技术要求
你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter09
找到本章的所有代码。
不同的代码文件遵循章节中不同部分的名称。
将特征缩放到一个范围
特征缩放是机器学习中的一种预处理技术,它对数据集的独立变量或特征进行范围重缩放。它的目的是通过将所有特征调整到一个共同的尺度上,确保所有特征在模型训练过程中对模型的贡献相同。特征缩放对于那些对输入特征尺度敏感的算法尤为重要,如 k 近邻算法和基于梯度下降的优化算法。
注意
在进行特征缩放时,我们实际上是在改变数据分布的范围。
让我们通过一个例子来帮助理解特征缩放的概念。假设你正在进行一个机器学习项目,目的是根据房屋的各种特征来预测房价,例如以下几个特征:
-
建筑面积(平方英尺)
-
到最近学校的距离(英里)
-
到最近公共交通站点的距离(英里)
现在,让我们来讨论一下在这个背景下特征缩放为什么如此重要。建筑面积这一特征的范围可能从几百平方英尺到几千平方英尺不等。而到最近学校的距离和到最近公共交通站点的距离可能从几分之一英里到几英里不等。如果不对这些特征进行缩放,算法可能会给较大值过高的权重,从而使得建筑面积在预测房价中占主导地位。像到最近学校的距离这样的特征可能会被不公平地忽略。
对于本章的所有部分,我们将使用上述示例来展示不同的缩放方法。让我们先来看一下这个示例的数据创建过程;代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter09/min_max_scaling.py
找到:
-
让我们从导入所需的库开始:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler
-
接下来,我们将创建一个与房价相关的特征数据集:
np.random.seed(42) num_samples = 100
我们将创建以下影响房价的特征:
-
平方英尺面积:
square_footage = np.random.uniform(500, 5000, num_samples)
-
到最近学校的距离(以英里为单位):
distance_to_school = np.random.uniform(0.1, 5, num_samples)
-
通勤到工作的距离(以英里为单位):
commute_distance = np.random.exponential(5, num_samples)
-
交通密度(偏态特征):
traffic_density = np.random.exponential(2, num_samples)
-
-
然后,我们创建一个包含所有特征的 DataFrame:
data = pd.DataFrame({ 'Square_Footage': square_footage, 'Distance_to_School': distance_to_school, 'Commute_Distance': commute_distance, 'Traffic_Density': traffic_density })
-
最后,我们绘制原始分布:
plt.figure(figsize=(12, 8))
你可以在这里看到数据的原始分布:
图 9.1 – 房价预测用例的分布
-
让我们显示数据集的统计信息:
print("Original Dataset Statistics:") print(data.describe())
这将打印以下输出:
图 9.2 – 原始数据集统计信息
现在让我们讨论最常见的缩放方法之一——min-max 缩放。
Min-max 缩放
Min-max 缩放,也称为归一化,将变量的值缩放到一个特定的范围,通常在 0 和 1 之间。Min-max 缩放在你想确保变量中的所有值都落在标准化范围内,使它们可以直接比较时非常有用。当变量的分布不假设为正态分布时,它通常被应用。
让我们看一下计算 min-max 缩放的公式:
X _ 缩放 = (X − X _ min) / (X _ max − X _ min)
从公式中可以看出,min-max 缩放保持了值的相对顺序,但将它们压缩到一个特定的范围。需要注意的是,这不是处理异常值的方法,如果数据中存在异常值,这些极端值可能会不成比例地影响缩放。因此,最好先处理异常值,然后再进行特征的缩放。
当满足以下条件时,缩放到特定范围是合适的:
-
你已经知道数据的大致上下限
-
你的数据在这个范围内遵循相对均匀或钟形分布
-
你选择的机器学习算法或模型通过将特征限制在特定范围内,通常是
[0, 1]
或任何其他期望的范围,会受益。
这个场景的经典例子是年龄。年龄值通常从 0 到 90,整个范围内有大量个体。然而,不太建议对收入进行缩放,因为高收入个体的数量有限。如果你对收入进行线性缩放,缩放的上限将变得异常高,大多数数据点将集中在一个狭窄的范围内,导致信息丢失和失真。
让我们看一下我们之前讨论的房价预测用例的代码,了解 min-max 缩放器如何转换数据:
-
首先,我们使用
MinMaxScaler()
来缩放数据:scaler = MinMaxScaler() data_scaled = pd.DataFrame(scaler.fit_transform(data), columns=data.columns)
-
我们可以使用以下代码显示缩放后的数据集统计信息:
print("\nDataset Statistics After Scaling:") print(data_scaled.describe())
-
让我们绘制并观察缩放后的分布:
plt.figure(figsize=(12, 8))
让我们看看标准化后修改的数据分布:
图 9.3 – 经过最小最大标准化后的房价预测用例分布
我们应用了最小最大标准化,将每一列转化为标准化的 0 到 1 之间的范围。由于最小最大标准化保持了数据点之间的相对距离,因此原始特征分布的形状在标准化后保持不变。标准化对数据起到了归一化的作用,将所有特征带到了一个共同的尺度。这在特征具有不同单位或范围时尤为重要,可以防止某个特征支配其他特征。
注意
如果你的数据集是稀疏的(包含许多零值),则最小最大标准化可能不适用,因为它可能导致信息丢失。可以考虑使用替代方法,如MaxAbsScaler或鲁棒缩放器。
在接下来的部分,我们将讨论 Z 分数标准化。
Z 分数标准化
Z 分数标准化,也称为标准化,适用于当你想将数据转化为均值为 0,标准差为 1 的形式时。Z 分数标准化在统计分析和机器学习中被广泛应用,尤其是在使用 k-means 聚类或主成分分析(PCA)等算法时。
这是 Z 分数的公式:
X _ scaled =(X − mean(X)) / std(X)
让我们继续使用房价预测用例来展示 Z 分数标准化。代码可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter09/zscaler.py
找到:
-
我们首先执行 Z 分数标准化:
data_zscore = (data - data.mean()) / data.std()
-
然后,我们打印数据集统计信息:
print("\nDataset Statistics after Z-score Scaling:") print(data_zscore.describe())
-
最后,我们可视化分布:
data_zscore.hist(figsize=(12, 10), bins=20, color='green', alpha=0.7) plt.suptitle('Data Distributions after Z-score Scaling') plt.show()
让我们看看标准化后修改的数据分布:
图 9.4 – 经过 Z 标准化后的房价预测用例分布
标准化后的每个特征的均值非常接近 0,这是 Z 分数标准化的预期结果。数据现在已围绕 0 进行集中。每个特征的标准差大约为 1,使得不同特征之间的尺度具有可比性。最小值和最大值已被转换,但保持了数据的相对分布。
何时使用 Z 分数标准化
让我们还讨论一些关于何时使用 Z 分数标准化的注意事项:
-
Z-score 缩放假设数据大致呈正态分布,或者至少围绕中心均值呈对称分布。如果数据高度偏斜或具有非标准分布,标准化可能在使数据更符合高斯分布方面效果不佳。如你所见,
Commute_Distance
和Traffic_Density
特征呈偏斜分布,且由于数据未围绕均值集中,z-score 缩放效果并不理想。 -
这种方法最适用于数值特征,而非分类或有序特征。确保你要标准化的数据是定量性质的。
-
极端异常值可能对均值和标准差产生重大影响,而这些是 z-score 缩放中使用的统计量。因此,在标准化之前处理异常值非常重要,因为它们可能扭曲缩放效果。
-
Z-score 缩放假设变量之间存在线性关系。如果底层关系是非线性的,其他缩放方法或变换可能更为合适。
-
Z-score 缩放假设变量是独立的,或至少不高度相关。如果变量之间高度相关,标准化可能不会提供额外的好处,且应单独考虑相关性结构。
-
Z-score 缩放会改变数据的原始单位,这可能会影响结果的可解释性。考虑在分析中是否需要保持原始单位。
对于小型数据集,z-score 缩放的影响可能更为显著。对非常小的数据集应用标准化时要小心,因为它可能会过度强调异常值的影响。
稳健缩放
稳健缩放,也叫稳健标准化,是一种特征缩放方法,特别适用于处理包含异常值的数据集。与可能对异常值敏感的最小-最大缩放和 z-score 缩放不同,稳健缩放旨在在存在极端值时保持稳健性。当你希望在最小化极端值影响的同时对特征进行规范化或标准化时,它尤为有用。稳健缩放也适用于那些特征不遵循正态分布、可能具有偏斜或重尾的数据集。
下面是稳健缩放的公式:
X _ scaled = (X − median) / IQR
在缩放过程中,通过减去中位数并除以四分位距(IQR)来规范化数据,使其围绕中位数进行中心化,并根据 IQR 所表示的分布范围进行缩放。这种规范化有助于减轻极端值的影响,使得缩放后的值更加代表整体分布。
正如我们在上一章中讨论的那样,中位数是当数据按顺序排列时位于中间的值,使得鲁棒缩放比其他依赖均值的缩放方法对极端值或异常值的敏感度较低。此外,四分位距(IQR)表示第一四分位数(Q1)和第三四分位数(Q3)之间的范围,亦对异常值具有鲁棒性。与完整范围或标准差不同,IQR 侧重于数据的中间 50%,因此不容易受到极端值的影响。
下面是如何使用 Python 代码应用鲁棒缩放的示例:
robust_scaler = RobustScaler()
data_scaled = robust_scaler.fit_transform(data)
让我们来看一下缩放后修改的数据分布:
图 9.5 – 鲁棒缩放后房价预测案例分布
经过鲁棒缩放处理后,我们可以观察到数据的中心趋势发生了变化,每个特征的均值现在更接近零。这是因为鲁棒缩放过程减去了中位数。至于数据的分布,它在除以四分位距(IQR)后发生了变化。每个特征的变异性现在以更一致的方式呈现,且对异常值更为稳健。每个特征的值范围现在被压缩,特别是对于那些初始范围较大的特征,防止了极端值特征的主导作用。
为了总结本章内容,我们制作了一个总结表格,展示了迄今为止讨论的所有技术,包括它们的使用时机、优缺点等信息。
方法比较
本图表提供了在不同数据情境下,适合使用哪种缩放技术的指导。
缩放方法 | 何时使用 | 优点 | 缺点 |
---|---|---|---|
最小-最大缩放 | 特征具有明确且已知的范围假设数据服从正态分布数据不包含异常值 | 简单易懂保留相对关系节省内存 | 对异常值敏感 |
Z-score 缩放 | 数据服从正态分布对范围没有强假设处理异常值不是重点 | 将数据标准化为零均值,标准差为 1 | 对异常值敏感可能不适合偏态数据 |
鲁棒缩放 | 数据包含异常值偏态分布或非正态数据平衡特征贡献对不同特征方差的弹性 | 对异常值较不敏感保留中心趋势和分布 | 计算开销更大 |
表 9.1 – 比较不同的缩放技术
计算复杂度
最小-最大缩放方法通常更节省内存,尤其是在处理大型数据集时。这是因为最小-最大缩放仅仅基于每个特征的最小值和最大值进行缩放,计算相对简单。
另一方面,z-score 标准化和稳健标准化都需要额外的计算,如均值、标准差(用于 z-score 标准化)、中位数和四分位间距(用于稳健标准化),这可能会导致更多的内存使用。尤其在处理大数据集时,z-score 标准化和稳健标准化的计算复杂性和内存需求可能会变得更加明显。
最后,让我们总结一下本章的学习内容,并为下一章获得启发。
总结
本章中,我们探讨了三种常见的数值特征标准化方法:最小-最大标准化、z-score 标准化和稳健标准化。最小-最大标准化将数据转换到一个特定的范围,使其适用于对特征大小敏感的算法。z-score 标准化将数据标准化为零均值和单位方差,提供一个标准化的分布。稳健标准化则通过使用中位数和四分位间距,能够抵抗离群值,适用于具有偏态分布或离群值的数据集。我们还讨论了在选择最适合你使用场景的方法时需要考虑的不同因素。
接下来,我们将在下一章将重点转向处理分类特征。
第十章:处理分类特征
处理分类特征涉及表示和处理那些本质上不是数值的信息。分类特征是可以取有限且固定数量值或类别的属性,它们通常定义数据集中的不同类别或组,例如产品类型、书籍类型或客户群体。有效地管理分类数据至关重要,因为大多数机器学习(ML)算法要求输入为数值。
在本章中,我们将涵盖以下主题:
-
标签编码
-
一热编码
-
目标编码(均值编码)
-
频率编码
-
二进制编码
技术要求
本章的完整代码可以在以下 GitHub 仓库中找到:
github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter10
让我们安装本章将使用的必要库:
pip install scikit-learn==1.5.0
pip install matplotlib==3.9.0
pip install seaborn==0.13.2
pip install category_encoders==2.6.3
标签编码
标签编码是一种处理分类数据的技术,通过将每个类别转换为唯一的整数。它适用于具有顺序关系的分类特征,即类别之间有明确的排名或顺序。
例如,当处理“高中”、“学士”、“硕士”和“博士”等教育水平时,可以使用标签编码,因为这些教育水平有一个从最低到最高的明确顺序。
用例 —— 员工绩效分析
人力资源(HR)部门希望分析员工绩效数据,以了解员工评分与薪资、工作年限和部门等其他因素之间的关系。他们计划使用机器学习根据这些因素预测员工评分。
数据
让我们快速浏览一下用于绩效分析的数据:
-
Employee Rating
:具有Poor
、Satisfactory
、Good
和Excellent
值的分类特征 -
Salary
:表示员工薪资的数值特征 -
Years of Experience
:表示员工工作年限的数值特征 -
Department
:表示员工所在部门的分类特征
让我们先看一下编码前的原始数据框:
Employee Rating Salary Years of Experience Department
0 Poor 35000 2 HR
1 Good 50000 5 IT
2 Satisfactory 42000 3 Finance
3 Excellent 60000 8 IT
4 Good 52000 6 Marketing
在理解了数据之后,我们可以进入用例的目标。
用例目标
该用例的目标是使用标签编码对Employee Rating
特征进行编码,以便准备数据进行机器学习分析。让我们看看如何使用 scikit-learn 来完成这项工作,完整代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/1a.label_encoding.py
找到:
-
让我们导入所需的库:
import pandas as pd from sklearn.preprocessing import LabelEncoder
-
让我们创建一个示例数据集并将其转化为 DataFrame:
data = { 'Employee Rating': ['Poor', 'Good', 'Satisfactory', 'Excellent', 'Good'], 'Salary': [35000, 50000, 42000, 60000, 52000], 'Years of Experience': [2, 5, 3, 8, 6], 'Department': ['HR', 'IT', 'Finance', 'IT', 'Marketing'] } df = pd.DataFrame(data)
-
初始化
LabelEncoder
类:label_encoder = LabelEncoder()
-
对
员工评级
列应用标签编码:df['Employee Rating (Encoded)'] = label_encoder.fit_transform(df['Employee Rating'])
让我们看看我们创建的编码输出。
编码后的输出
在这个用例中,应用标签编码对员工评级
特征进行转换,将其转换为数字值,同时保留序数关系。下表显示了编码操作的输出结果。
员工评级 | 薪资 | 工作经验年限 | 部门 | 员工评级(编码后) | |
---|---|---|---|---|---|
1 | 差 | 35000 | 2 | 人力资源 | 2 |
2 | 好 | 50000 | 5 | 信息技术 | 1 |
3 | 满意 | 42000 | 3 | 财务 | 3 |
4 | 优秀 | 60000 | 8 | 信息技术 | 0 |
5 | 好 | 52000 | 6 | 市场营销 | 1 |
表 10.1 – 标签编码后的输出数据集
如你所见,已添加了一个员工评级(编码后)
特征,所有项目现在都变成了数字。让我们看一下编码列的分布图:
图 10.1 – 编码前后的分布
如我们所见,编码前后的分布没有变化。标签编码将类别标签转换为数值,同时保留原始数据分布。它只是为每个类别分配了唯一的整数值,并未改变其频率。然而,在视觉上,x 轴上的标签将从类别值变为数字值,但每个标签的计数(或频率)将保持不变。
注意
如果数据被打乱或在不同的编码器运行之间类别的顺序发生变化,编码后的值可能会不同。这是因为将整数分配给类别可能依赖于它们出现的顺序。此外,如果每次都初始化一个新的标签编码器实例,类别与整数之间的映射可能也会发生变化。为了确保结果一致,应该在第一次拟合编码器后使用它来进行数据转换。
编码后的值可以作为机器学习模型的输入特征,用于根据薪资、工作经验和部门预测员工评级。现在,让我们讨论在使用标签编码器编码特征时需要注意的一些事项。
标签编码的注意事项
在进行标签编码时,尤其是在处理大型数据集时,有几个重要的注意事项。确保类别特征具有有意义的顺序。如果类别之间没有自然的顺序,标签编码可能不适用。标签编码将整数值分配给类别,基于字母顺序。如果类别没有固有的顺序,可能会引发问题,模型可能会将数字值视为有序。例如,Poor
、Good
和Excellent
可能会被编码为2
、1
和0
,但Poor
并不比Good
大。正如前面提到的用例中所发生的那样。为了确保标签编码反映正确的顺序(即Poor
< Satisfactory
< Good
< Excellent
),我们可以通过手动设置顺序并指定所需的映射来解决这个问题,完整的代码可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/1b.label_encoding_forced.py
找到:
-
定义具有前缀的类别的正确顺序:
ordered_categories = { 'Poor': '1.Poor', 'Satisfactory': '2.Satisfactory', 'Good': '3.Good', 'Excellent': '4.Excellent' }
-
将
Employee Rating
列映射到带前缀的类别:df['Employee Rating Ordered'] = df['Employee Rating'].map(ordered_categories)
生成的 DataFrame 如下所示:
Employee Rating Ordered Employee Rating (Encoded) 0 Poor 0 1 Good 2 2 Satisfactory 1 3 Excellent 3 4 Good 2
在编码时,始终保持一致性,尤其是在训练集和测试集之间。编码器应该在训练数据上拟合,并用于转换训练集和测试集。这可以防止在测试集中出现未见过的类别,从而导致错误或编码不正确。按照以下步骤作为最佳实践:
-
对
Employee
Rating
列应用标签编码:df['Employee Rating (Encoded)'] = label_encoder.fit_transform(df['Employee Rating'])
-
保存编码器:
joblib.dump(label_encoder, 'label_encoder.pkl')
-
加载编码器(在另一个脚本或会话中):
loaded_encoder = joblib.load('label_encoder.pkl')
-
转换新数据:
df['Employee Rating (Encoded)'] = loaded_encoder.transform(df['Employee Rating'])
最后一项要提到的重点是,在处理大型数据集时,标签编码通常比独热编码更节省内存,后者可能会创建许多二进制列。
虽然标签编码是一种将类别数据转换为数值形式的简单方法,但它可能会无意中在类别之间引入不存在的顺序关系。为了避免这个问题,并确保每个类别被独立处理,独热编码通常是更合适的方法。
独热编码
独热编码是一种将类别数据转换为二进制矩阵(1 和 0)的技术。每个类别都被转换为一个新列,并且在对应类别的列中放置 1,而所有其他列则放置 0。该方法在处理没有类别间顺序关系的类别数据时特别有用。
何时使用独热编码
一热编码适用于缺乏自然顺序或类别排名的类别数据。以下是一些适用的场景:
-
名义类别数据:处理名义数据时,类别是独立的,并且没有固有的顺序。
-
不处理序列数据的算法:一些机器学习算法(例如,决策树和随机森林)并非专门设计来正确处理序列数据。一热编码确保每个类别都被视为独立的实体。
-
防止误解:为了防止模型假设不存在的序列关系,采用一热编码(one-hot encoding)将类别数据表示为二进制值。
接下来,让我们看一下可以使用一热编码的用例。
用例 – 客户流失预测
一家电信公司正在经历较高的客户流失率,想要构建一个机器学习模型,预测哪些客户可能会离开其服务。他们收集了有关客户人口统计、合同详情和使用的服务的数据。
数据
让我们快速查看一下可用于分析的数据:
-
合同类型
:具有月度
、一年
和两年
等值的类别特征 -
互联网服务
:具有DSL
、光纤
和无互联网
服务等值的类别特征 -
支付方式
:具有电子支票
、邮寄支票
、银行转账
和信用卡
等值的类别特征
让我们看一下用于此用例的示例数据:
Customer ID Contract Type Internet Service Payment Method
0 1 Month-to-Month DSL Electronic Check
1 2 One Year Fiber Optic Mailed Check
2 3 Month-to-Month DSL Bank Transfer
3 4 Two Year Fiber Optic Credit Card
在了解了数据后,我们可以进入用例的目标部分。
用例目标
用例的目标是使用一热编码对类别特征进行编码,为机器学习分析准备数据。此示例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/2.one_hot_encoding.py
。
请按以下步骤操作:
-
初始化
OneHotEncoder
类:one_hot_encoder = OneHotEncoder(sparse_output=False, drop='first')
-
拟合并转换类别列:
encoded_columns = one_hot_encoder.fit_transform(df[['Contract Type', 'Internet Service', 'Payment Method']])
-
创建一个包含一热编码列的新 DataFrame:
encoded_df = pd.DataFrame(encoded_columns, columns=one_hot_encoder.get_feature_names_out(['Contract Type', 'Internet Service', 'Payment Method']))
-
将一热编码后的 DataFrame 与原始 DataFrame 进行连接:
df_encoded = pd.concat([df, encoded_df], axis=1)
-
删除原始类别列,因为它们现在已经编码:
df_encoded = df_encoded.drop(['Contract Type', 'Internet Service', 'Payment Method'], axis=1)
让我们看一下我们创建的编码输出。
编码输出
在这个用例中,我们正在为客户流失预测模型准备客户数据。类别特征如合同类型
、互联网服务
和支付方式
被一热编码,转换为适合机器学习的二进制表示。这些编码后的特征可以用来训练预测模型,帮助电信公司识别有流失风险的客户,并采取主动措施留住他们。
让我们通过一些图表看看应用编码时特征分布的变化。首先来看一下编码前的原始分布:
图 10.2 – 独热编码前的分布
编码后,每个类别变量的值会被转化为一个独特的列,展示二进制值(0 或 1),反映该类别在数据集每一行中的存在情况。让我们看看Contract
Type
列的分布图:
图 10.3 – 独热编码后合同类型特征的分布
注意
可视化原始类别数据有助于理解数据分布并识别任何不平衡情况。可视化编码后的列可以确保转换已正确应用。每个二进制列应仅包含 0 或 1 的值。
现在让我们讨论一些在使用独热编码器编码特征时需要注意的事项。
独热编码的注意事项
在进行独热编码时,特别是在大数据集上,有几个重要的注意事项需要牢记:
-
独热编码会显著增加数据集的维度,尤其是在类别较多的情况下。这可能导致“维度灾难”,对于某些算法来说可能是个问题。
-
共线性:由于每个类别都表示为一个单独的二进制列,这些列之间可能会存在共线性。这意味着某些列可能高度相关,这可能会影响线性模型的性能。
-
处理缺失值:在应用独热编码之前,决定如何处理类别特征中的缺失值。你可以选择为缺失值创建一个单独的列,或者使用插补技术。
-
在大数据集上进行独热编码可能会很具挑战性,因为特征数量的增加和潜在的高内存使用。若数据集过大无法放入内存,可将数据分批处理。
从独热编码转向目标编码,特别是在处理高基数类别特征时,可以特别有益。让我们更详细地探讨目标编码。
目标编码(均值编码)
目标编码,也称为均值编码,是一种通过将每个类别替换为该类别对应的目标变量的均值(或其他相关聚合函数)来编码类别特征的方法。此方法对于处理高基数类别特征的分类任务特别有用,而使用独热编码会导致维度的大幅增加。
更具体地说,目标编码将类别值替换为每个类别的目标变量的均值(或其他聚合度量)。它利用类别特征和目标变量之间的关系来编码信息。
什么时候使用目标编码
当你的特征是类别型并且有很多独特的类别时,使用独热编码可能会导致数据集的维度过高。在这种情况下,目标编码可以是一个有效的替代方案。
如果类别特征与目标变量之间存在强关系,目标编码能够捕捉到这种关系,并可能提高预测能力。
当你有内存限制并且需要降低数据集的维度时,你也可以使用目标编码,因为目标编码不会创建额外的列。
用例 – 零售商店的销售预测
一家拥有多个商店的零售连锁店希望建立一个机器学习模型,以预测每个商店的日销售额。他们收集了关于多个特征的数据,其中包括具有高基数的 Store Type
特征。为了避免使用独热编码(这会导致特征数量过多),零售连锁决定使用目标编码来编码 Store
Type
特征。
数据
让我们快速查看一下可用于分析的数据:
-
商店类型
: 商店的类型(具有Type A
、Type B
、Type C
和Type D
值的类别变量) -
员工数量
: 商店的员工数量(整数变量) -
广告预算
: 商店为广告分配的预算(以美元为单位的连续变量) -
日销售额
: 商店一天内的销售额(以美元为单位的目标变量)
让我们看看这个用例的数据样本:
Store Type Number of Employees Advertising Budget Daily Sales
0 Type C 21 23117.964192 16195.682148
1 Type D 13 9017.567238 851.127834
2 Type A 37 39945.667889 19274.801963
3 Type C 24 34990.429063 14670.084345
4 Type C 17 11817.711027 6442.646360
理解了数据之后,我们可以继续进行该用例的目标。
用例目标
该用例的目标是使用目标编码来编码类别特征,以准备数据进行机器学习建模。让我们看看如何使用 scikit-learn 来完成这一操作。这个示例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/3.target_encoding.py
。
确保你已经安装并导入了在章节开头的 技术要求 部分中提到的库。完成这些后,我们开始吧:
-
让我们创建一个样本大小为 1000 的合成数据集:
np.random.seed(42) n_samples = 1000
-
生成一些随机数据:
data = { 'Store Type': np.random.choice(['Type A', 'Type B', 'Type C', 'Type D'], size=n_samples), 'Number of Employees': np.random.randint(5, 50, size=n_samples), 'Advertising Budget': np.random.uniform(1000, 50000, size=n_samples), 'Daily Sales': np.random.uniform(500, 20000, size=n_samples) }
-
将数据放入 DataFrame:
df = pd.DataFrame(data)
-
定义目标变量和特征:
X = df.drop(columns=['Daily Sales']) # Features y = df['Daily Sales'] # Target variable
-
将数据拆分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
-
初始化一个
TargetEncoder
类:target_encoder = TargetEncoder(cols=['Store Type'])
-
在训练数据上进行拟合和转换:
X_train_encoded = target_encoder.fit_transform(X_train, y_train)
注意
在 GitHub 提供的本节代码中,我们使用数据和编码特征来训练随机森林回归模型并计算验证指标。如果你有兴趣,可以在这里查看代码文件:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/3.target_encoding.py
。
这种编码技术有助于捕捉不同商店类型与每日销售之间的关系,因此,让我们来看一下编码后的输出。
编码后的输出
让我们来看一下编码后的数据:
Store Type Number of Employees Advertising Budget
29 10025.134200 37 43562.535230
535 10190.055174 12 1940.421564
695 10025.134200 14 47945.600526
557 10190.055174 23 19418.525972
836 10560.489044 27 35683.919764
让我们重点关注现在已被编码为数值的 Store Type
列。我们可以通过以下图表更详细地查看编码前后的差异:
图 10.4 – 编码前后商店类型的分布
在这种情况下,目标编码具有优势,因为它能有效地对类别特征进行编码,使其适用于回归任务(例如销售预测),同时避免了与独热编码相关的维度问题。
现在让我们讨论一些在使用目标编码器编码特征时需要注意的事项。
目标编码的注意事项
在对大数据集执行目标编码时,有几个重要事项需要注意:
-
过拟合:如果目标编码没有谨慎应用,或者某些类别只有少量样本,可能会导致过拟合。为了缓解这种情况,通常会使用平滑或添加正则化项等技术。
-
平滑(正则化):平滑涉及将每个类别的目标变量的均值与全局均值进行混合。这可以减少训练数据中极端值或噪声的影响。平滑目标编码的公式通常如下所示:
平滑均值 = (n * 类别均值 + m * 全局均值) / (n + m)
在这里,我们有以下内容:
-
n 是类别中的观察值数量。
-
m 是一个超参数,用于控制平滑的强度。
调整 m 的值可以控制正则化的水平。较小的 m 值给予类别实际均值更多的权重,而较大的 m 值则给予全局均值更多的权重。
-
交叉验证:在交叉验证的每个折叠内执行目标编码。这有助于确保编码基于一个独立于被预测数据的部分数据。交叉验证可以为每个类别提供更可靠的目标变量分布估计。
-
留一法编码:在这种方法中,你计算排除当前观察的类别的目标变量的均值。它可能更抗过拟合,因为它考虑了类别的效应,但不包括正在编码的实例的目标值。
-
添加噪声:向编码值中引入少量随机噪声有助于减少过拟合。这通常被称为贝叶斯目标编码。
-
要注意数据泄露问题。在训练数据集上计算均值至关重要,并将相同编码应用于验证和测试数据集。
-
仅在训练数据上计算编码统计信息:仅基于训练数据集计算编码统计信息(例如均值)。这确保模型在无偏信息上训练。
-
应用相同的编码到所有数据集:一旦在训练数据上计算了编码统计信息,预处理验证和测试数据集时应使用相同的编码。不要单独为这些数据集重新计算统计信息。
-
虽然目标编码可以提高模型性能,但可能会降低模型的可解释性,因为丢失了原始的分类值。
在探索目标编码之后,处理高基数分类特征的另一种有效技术是频率编码。频率编码用数据集中每个类别的频率或计数替换每个类别,这有助于捕捉每个类别的固有重要性并维持数据的整体分布。让我们深入了解频率编码及其在处理分类变量中的优势。
频率编码
频率编码,也称为计数编码,是一种通过在数据集中用每个类别的频率或计数替换每个类别的技术。在这种编码方法中,类别出现的频率越高,其编码值就越高。在某些情况下,频率编码可以是一种有价值的工具,因为类别出现的频率携带了有价值的信息。
何时使用频率编码
可以考虑在以下情况下使用频率编码:
-
信息频率:类别的频率或计数具有信息量,与目标变量直接或间接相关。例如,在客户流失预测问题中,客户购买产品的频率可能与其流失的可能性相关。
-
效率:您需要一种高效的编码方法,相比独热编码,它需要较少的计算资源和内存。
这种编码方法通常与基于树的模型(如决策树、随机森林和梯度提升树)配合良好,因为这些模型能有效捕捉编码频率与目标变量之间的关系。
用例 - 客户产品偏好分析
一家零售公司希望基于顾客的购买历史分析顾客的产品偏好。他们拥有一个包含顾客购买信息的数据集,其中包括他们最常购买的产品类别。
数据
在这个例子中,我们将对产品类别
特征使用频率编码,以确定顾客最常购买的产品类别。这种编码方法可以帮助零售公司分析顾客偏好,并了解如何根据热门产品类别优化产品推荐或营销策略。
让我们来看一下样本数据集:
Customer ID Product Category Total Purchases
0 1 Electronics 5
1 2 Clothing 2
2 3 Electronics 3
3 4 Books 8
4 5 Books 7
5 6 Clothing 4
在理解了数据之后,我们可以进入用例的目标部分。
用例的目标
该用例的目标是使用频率编码对类别特征进行编码,以便为机器学习建模准备数据。让我们看看如何使用 scikit-learn 实现这一目标:
-
让我们创建一个样本数据集:
data = { 'Customer ID': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'Product Category': ['Electronics', 'Clothing', 'Electronics', 'Books', 'Books', 'Clothing', 'Electronics', 'Books', 'Clothing', 'Books'], 'Total Purchases': [5, 2, 3, 8, 7, 4, 2, 5, 1, 6] } df = pd.DataFrame(data)
-
定义特征:
X = df[['Customer ID', 'Product Category', 'Total Purchases']]
-
将数据拆分为训练集和测试集:
X_train, X_test = train_test_split(X, test_size=0.2, random_state=42)
-
初始化一个
CountEncoder
类,用于产品类别
:count_encoder = CountEncoder(cols=['Product Category'])
-
拟合并转换训练数据:
X_train_encoded = count_encoder.fit_transform(X_train)
该公司希望使用频率编码对这个类别特征进行编码,以了解哪些产品类别是顾客最常购买的。让我们来看一下编码后的数据。
编码后的输出
让我们看一下编码后的数据:
Customer ID Product Category Total Purchases
5 6 1 4
0 1 3 5
7 8 4 5
2 3 3 3
9 10 4 6
让我们重点关注产品类别
特征,它现在根据频率被编码成数值。我们可以通过以下图表更详细地查看编码前后的差异:
图 10.5 – 编码前后产品类别的分布
第一个子图展示了编码前训练集中产品类别
的分布。第二个子图展示了编码后训练集中编码的产品类别
特征的分布。正如我们所看到的,产品类别
列中的每个类别都被该类别在训练集中的频率计数所替代。
注意
频率编码保留了数据集中每个类别的出现频率信息。
现在让我们讨论一些在使用频率编码器进行特征编码时需要注意的事项。
频率编码的注意事项
在执行频率编码时,有几个重要的注意事项需要记住:
-
频率编码可能会导致过拟合,尤其是在数据集较小或某些类别观察样本很少的情况下。这是因为模型可能会过度依赖频率计数,而这些计数在新数据上可能无法很好地泛化。
-
当两个或多个类别具有相同的频率时,它们将得到相同的编码值。如果这些类别对目标变量有不同的影响,这可能会成为一个限制。
-
频率编码通常不适用于线性模型,因为它不会在编码值和目标变量之间创建线性关系。如果你使用的是对特征缩放敏感的线性模型,可能需要对编码值进行归一化处理,使它们具有相似的尺度。
总的来说,频率编码实现简单,不像独热编码那样扩展特征空间,因此在处理高基数特征时非常高效,不会创建过多的新列。
虽然频率编码在处理高基数特征时提供了简便和高效的方法,但另一种有效的技术是二进制编码。二进制编码将类别表示为二进制数字,提供比独热编码更紧凑的表示方式,并且保留了有序关系。让我们探讨一下二进制编码如何进一步增强类别变量的处理。
二进制编码
二进制编码是一种通过将每个类别转换为二进制代码来编码类别特征的技术。每个独特的类别都由一个独特的二进制模式表示,其中模式中的每个数字(0 或 1)对应于该类别的存在或缺失。二进制编码在处理高基数类别特征的同时减少维度,非常有用。
何时使用二进制编码
在以下情况下,可以考虑使用二进制编码:
-
降维:你希望在减少数据集维度的同时,仍然能够保留类别特征中的信息。在这种情况下,二进制编码特别有用。
-
高效性:你需要一种高效的编码方法,能够以紧凑的方式表示类别数据,并且易于被机器学习算法处理。
我们来看一下一个使用场景。
使用场景 —— 客户订阅预测
一个订阅服务提供商希望根据各种特征预测客户是否会订阅高级计划,其中包括具有高基数的国家
特征。二进制编码将被用来高效地编码国家
特征。
数据
我们来看一下样本数据集:
-
国家
:这个类别特征表示客户所在的国家。它有助于了解地理位置是否会影响订阅状态。 -
年龄
:这个数值特征表示客户的年龄。年龄在确定客户是否订阅某项服务的可能性中可能是一个重要因素。 -
收入
:这个数值特征表示客户的年收入。收入可以反映客户是否有经济能力订阅某项服务。 -
订阅
:这个二进制目标变量表示客户是否订阅了服务。我们希望通过其他特征来预测这个目标变量。
我们来看一下该使用场景的数据样本:
Country Age Income Subscription
0 USA 25 50000 1
1 Canada 30 60000 0
2 USA 35 70000 1
3 Canada 40 80000 0
4 Mexico 45 90000 1
国家
的分布可以在以下图表中看到:
图 10.6 – 编码前后国家分布
用例目标
本分析的目标是根据客户的国家、年龄和收入预测订阅状态。我们对Country
特征使用二进制编码,将其从分类变量转换为可以在机器学习算法中使用的数值格式。此用例的代码可以在这里找到:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter10/5.binary_encoding.py
。
请按照以下步骤操作:
-
让我们创建一个示例数据集:
data = { 'Country': ['USA', 'Canada', 'USA', 'Canada', 'Mexico', 'USA', 'Mexico', 'Canada'], 'Age': [25, 30, 35, 40, 45, 50, 55, 60], 'Income': [50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000], 'Subscription': [1, 0, 1, 0, 1, 0, 1, 0] } df = pd.DataFrame(data)
-
对
Country
特征应用二进制编码:encoder = BinaryEncoder(cols=['Country']) df_encoded = encoder.fit_transform(df)
-
显示编码后的数据框:
print(df_encoded)
让我们来看一下编码后的数据。
编码输出
在这个例子中,应用了二进制编码到Country
特征,正如我们在以下输出中看到的:
Country_0 Country_1 Age Income Subscription
0 0 1 25 50000 1
1 1 0 30 60000 0
2 0 1 35 70000 1
3 1 0 40 80000 0
4 1 1 45 90000 1
5 0 1 50 100000 0
6 1 1 55 110000 1
7 1 0 60 120000 0
正如我们从编码输出中看到的,二进制数字被拆分成了单独的列。让我们也来看一下编码后分布的变化:
图 10.7 – 国家编码特征分布
现在让我们讨论在使用二进制编码器进行特征编码时需要注意的一些事项。
二进制编码的注意事项
在执行二进制编码时,需要考虑几个重要事项:
-
二进制编码没有提供直接的可解释性。与每个二进制特征对应一个特定类别的独热编码不同,编码后的二进制模式可能没有明确的意义。
-
对于具有非常高基数的类别,二进制表示可能变得复杂,因为二进制数字的数量会随着类别数量的增加而对数增加。
-
一些机器学习算法,特别是线性模型,可能不适用于二进制编码特征。需要仔细评估算法的兼容性。
现在我们已经探讨了不同编码方法的细节,让我们转向总结它们的主要区别以及在机器学习工作流中的实际应用考虑事项。
总结
在本章中,我们探讨了用于编码分类变量的各种技术,这些技术对于机器学习任务至关重要。标签编码为每个类别分配唯一的整数,方法简单明了,但可能会不自觉地赋予没有顺序关系的类别以顺序性。独热编码将每个类别转换为二进制特征,保持了类别的独立性,但可能会导致高维数据集。二进制编码将分类值压缩成二进制表示,平衡了可解释性和效率,尤其适用于高基数数据集。频率编码通过用类别的出现频率替换类别,捕捉了关于分布模式的有价值信息。目标编码将目标变量的统计信息融入到分类编码中,提高了预测能力,但需要谨慎处理以避免数据泄漏。
让我们在下表中总结我们的学习:
编码方法 | 高基数 | 保留顺序信息 | 冲突 | 可解释性 | 适用于 | 不适用于 |
---|---|---|---|---|---|---|
标签编码 | 好 | 是 | 否 | 中等 | 基于树的模型 | 线性模型 |
独热编码 | 差 | 否 | 否 | 高 | 线性模型,神经 网络(NNs) | 高基数特征 |
目标编码 | 好 | 否 | 可能 | 低 | 大多数算法 | 小数据集(存在过拟合风险) |
频率编码 | 好 | 否 | 可能 | 中等 | 基于树的模型 | 线性模型 |
二进制编码 | 好 | 部分 | 可能 | 低 | 基于树的模型 | 线性模型 |
表 10.2 – 所有编码技术的比较
每种方法根据数据集的特点和建模任务的具体要求提供不同的优势。在下一章中,我们将重点讨论分析时间序列数据时需要考虑的问题和方法。时间序列数据引入了时间依赖性,要求使用专门的特征工程技术,正如我们在下一章中将要展开的内容。
第十一章:消耗时间序列数据
在本章关于时间序列分析的内容中,我们将探索时间序列的基本概念、方法论以及在不同行业中的实际应用。时间序列分析涉及研究随时间收集的数据点,以识别模式和趋势并进行预测。
在本章中,我们将深入探讨以下主题:
-
理解时间序列数据的组成部分
-
时间序列数据的类型
-
识别时间序列数据中的缺失值
-
处理时间序列数据中的缺失值
-
分析时间序列数据
-
处理离群值
-
使用时间序列数据进行特征工程
-
在不同行业应用时间序列技术
技术要求
本章的完整代码可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/tree/main/chapter11
。
运行以下代码以安装我们在本章中将使用的所有必要库:
pip install pandas
pip install numpy
pip install matplotlib
pip install statsmodels
理解时间序列数据的组成部分
时间序列数据是指一系列在时间上收集和记录的观察值或测量值。与非顺序数据不同,后者的观察值是在单一时间点采集的,时间序列数据则是在多个时间点按顺序捕捉信息。时间序列中的每个数据点都与特定的时间戳相关联,从而形成一个时间结构,允许分析随时间变化的趋势、模式和依赖关系。接下来,我们将讨论时间序列数据的不同组成部分,从趋势开始。
趋势
趋势组件表示数据中的长期变化或方向。它反映了一个持续较长时间的整体模式,指示值是普遍增加、减少,还是相对保持恒定。
趋势具有以下特征:
-
上升趋势:数值随时间系统性增加
-
下降趋势:数值随时间系统性减少
-
平稳趋势:数值在时间上保持相对恒定
确定趋势对做出关于所观察现象长期行为的明智决策至关重要。它提供了整体方向的洞察,并且对预测未来趋势具有重要价值。在接下来的部分中,我们将呈现一个灵感来自数据世界的用例,重点关注趋势组件。
分析长期销售趋势
在这个案例中,我们旨在分析十年来的销售趋势,以了解 2010 到 2020 年间企业的增长模式。你可以在本书的 GitHub 仓库中找到此示例的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/trend.py
。让我们开始吧:
-
我们将首先生成一个日期范围,并为每个月生成相应的销售数据,覆盖 10 年:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') sales_data = pd.Series(range(1, len(date_rng) + 1), index=date_rng)
-
接下来,我们必须绘制数据以可视化上升趋势:
plt.figure(figsize=(10, 5)) plt.plot(sales_data, label='Sales Data') plt.title('Time Series Data with Trend') plt.xlabel('Time') plt.ylabel('Sales') plt.legend() plt.show()
这将产生以下图表:
图 11.1 – 具有上升趋势的月度销售数据
图 11.1 显示了十年来销售额的持续上升趋势。这表明业务一直在稳步增长。
在我们的初步分析中,我们集中在理解销售数据中十年间的总体上升趋势。这为我们提供了有关企业长期增长的宝贵洞察。
通常,企业会经历在特定时间段内定期出现的波动,如月份或季度。这被称为季节性。识别这些季节性模式和理解整体趋势同样重要,因为它可以帮助企业预测并为高需求或低需求时期做好准备。为了说明这一点,我们将扩展我们的分析,加入销售数据中的季节性因素。
季节性
季节性 是指在时间序列中定期出现的重复且可预测的模式。这些模式通常对应于特定的时间段,如天、月或季节,并且可能受外部因素如天气、假期或文化事件的影响。
与长期趋势不同,季节性跨越较短的时间框架,对数据产生短期影响。季节性的这种周期性特征使得企业能够预测并规划需求波动,从而优化其运营和战略。
重要提示
了解季节性有助于识别重复出现的模式,并预测某些行为或事件可能发生的时间。这些信息对于准确的预测和规划至关重要。
在接下来的部分,我们将扩展之前提出的销售案例,同时关注季节性因素。
分析带有季节性的长期销售趋势
在这个用例的这一部分,我们旨在分析包括季节性变化的十年销售趋势。你可以在这里找到完整的代码示例:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/seasonality.py
。让我们开始吧:
-
我们将从为每个月生成一个日期范围,并相应地生成销售数据开始,覆盖 10 年:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') seasonal_data = pd.Series([10, 12, 15, 22, 30, 35, 40, 38, 30, 22, 15, 12] * 11, index=date_rng)
-
然后,我们必须绘制数据,以可视化季节性成分:
图 11.2 – 带有季节性因素的月度销售数据
图 11.2 显示了每 12 个月重复的模式,表明销售中存在明显的季节性。销售在年中达到高峰,并在年末和年初下降,暗示着夏季的销售较高,冬季的销售较低。这一模式在多年中的一致性有助于预测未来的销售周期。了解这些季节性趋势对库存管理、营销活动和在销售高峰期的资源分配非常有价值,帮助企业相应优化策略。
虽然识别趋势和季节性提供了对销售模式的宝贵见解,但现实世界的数据通常还包含另一个关键成分:噪声。在接下来的部分,我们将深入探讨噪声,并扩展销售用例,以探索噪声如何影响销售。
噪声
噪声,也称为残差或误差,代表时间序列数据中无法归因于趋势或季节性的随机波动或不规则性。它反映了数据中的变化性,这些变化性无法通过基础模式来解释。
重要说明
虽然噪声通常被认为是不需要的,但它是任何现实世界数据的自然组成部分。识别并隔离噪声对于构建准确的模型和理解时间序列中固有的不确定性至关重要。
在接下来的部分,我们将扩展前面介绍的销售用例,并重点关注噪声。
分析带噪声的销售数据
在这个用例中,我们旨在分析包含噪声的销售数据,除了趋势和季节性因素外。这将帮助我们理解随机波动如何影响我们识别潜在模式的能力。要跟随这个示例,请查看以下代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/1.decomposing_time_series/noise.py
。让我们开始吧:
-
让我们导入所需的库:
import pandas as pd import matplotlib.pyplot as plt
-
我们将从生成一个覆盖 10 年的每月日期范围开始:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M')
-
然后,我们必须创建带噪声的销售数据:
np.random.seed(42) noise_data = pd.Series(np.random.normal(0, 2, len(date_rng)), index=date_rng)
-
现在,我们必须绘制数据以可视化噪声:
图 11.3 – 带噪声的每月销售数据
图 11.3 显示了随机、不可预测的变化,这些变化没有遵循任何特定模式。这些波动发生在短时间内,导致数据的不稳定,使得更难看出任何模式。
现在我们可以识别不同的时间序列组件,让我们来看看不同类型的时间序列。
时间序列数据的类型
在本节中,我们将简要回顾时间序列数据的类型——单变量和多变量——同时阐明它们的区别,并展示它们的应用。
单变量时间序列数据
单变量时间序列数据由单个变量或观察值在时间上记录而成。它是一个一维的按时间顺序排列的序列,相较于多变量时间序列数据,它更易于分析。
考虑一个单变量时间序列,表示一个城市多年来每月的平均温度。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/2.types/univariate.py
。
让我们生成我们的单变量时间序列数据:
-
首先,我们将创建我们需要的数据范围,在这个例子中是从
2010-01-01
到2020-12-31
:date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M')
-
然后,我们必须通过使用正态分布(也称为高斯分布)添加噪声,来创建温度的相应值:
temperature_data = pd.Series(np.random.normal(20, 5, len(date_rng)), index=date_rng)
让我们理解值参数:
-
20
:这是5
:这是正态分布的标准差。噪声值通常会围绕均值波动约±5 个单位。较大的标准差意味着噪声会更分散,而较小的标准差意味着噪声值更接近均值。 -
我们之前创建的日期范围被作为索引传递给数据框。
-
-
现在,让我们绘制单变量时间序列数据:
plt.figure(figsize=(10, 5)) plt.plot(temperature_data, label='Temperature Data') plt.title('Univariate Time Series Data') plt.xlabel('Time') plt.ylabel('Temperature (°C)') plt.legend() plt.show()
这将输出以下图表:
图 11.4 – 单变量温度数据
在这个例子中,单变量时间序列代表了每月的平均温度。由于数据是随机生成的,均值为 20°C,并有一定的波动(标准差为 5°C),因此图表将表现出围绕该平均温度的随机波动。
理解单变量时间序列数据的复杂性为深入研究多变量时间序列分析打下了坚实的基础。与单变量数据只观察单一变量随时间的变化不同,多变量时间序列数据涉及同时监测多个相互关联的变量。
多元时间序列数据
多元时间序列数据涉及多个变量或观察值,这些变量或观察值是随着时间记录的。每个变量都是一个按时间顺序排列的序列,并且这些变量可能是相互依赖的,从而捕捉到更复杂的关系。
考虑一个多元时间序列,表示一个城市在多年中的月平均温度和月降水量。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/2.types/multivariate.py
找到这个示例的代码。让我们开始吧:
-
让我们为这个示例添加所需的库:
import pandas as pd import matplotlib.pyplot as plt import numpy as np
-
现在,让我们通过使用之前创建的温度数据并在同一个 DataFrame 中添加一条新的时间序列数据(表示降水量数据,具有不同的均值和标准差值),来生成一个多元时间序列数据示例:
date_rng = pd.date_range(start='2010-01-01', end='2020-12-31', freq='M') temperature_data = pd.Series(np.random.normal(20, 5, len(date_rng)), index=date_rng) rainfall_data = pd.Series(np.random.normal(50, 20, len(date_rng)), index=date_rng)
-
将所有时间序列合并到同一个 DataFrame 中,确保包含温度和降水量数据:
multivariate_data = pd.DataFrame({'Temperature': temperature_data, 'Rainfall': rainfall_data}) print(multivariate_data.head())
-
合并后的时间序列 DataFrame 如下所示:
Temperature Rainfall 2010-01-31 19.132623 56.621393 2010-02-28 18.551274 51.249927 2010-03-31 24.502358 65.679049 2010-04-30 27.069077 73.044307 2010-05-31 21.176376 41.317497
-
最后,让我们绘制多元时间序列数据:
图 11.5 – 多元数据
在这个示例中,多元时间序列包括温度和降水量数据,提供了一个更全面的环境条件视角。
总体而言,单变量数据较易处理,而多元数据使我们能够捕捉到随时间变化的变量之间更复杂的关系和依赖性。多元分析在解决经济学、金融、环境科学和医疗健康等各个领域的现实挑战时至关重要,在这些领域中,理解变量之间的多方面关系至关重要。
现在我们对时间序列数据有了较强的理解,可以探索有效清理和管理这种数据的方法。
识别时间序列数据中的缺失值
识别时间序列数据中的缺失值有点类似于识别其他类型数据中的缺失值,但由于时间序列的时间性特征,存在一些特定的注意事项。由于我们在第八章《检测和处理缺失值和异常值》中讨论过其中的一些技术,让我们在这里总结它们,并重点说明这些技术在分析时间序列数据时的具体应用,使用股票市场分析作为示例。
假设我们有某公司多年来每日的股价数据(开盘价、最高价、最低价和收盘价)。我们的目标是识别这些时间序列中的缺失数据,以确保数据集的完整性。你可以在这里找到该示例的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/1.identify_missing_values.py
。
让我们从生成数据开始:
-
首先,我们将生成从 2020 年 1 月 1 日到 2023 年 12 月 31 日的工作日日期范围。这里,
freq='B'
用于生成仅包括工作日(即排除周末)的日期范围:date_range = pd.date_range(start='2020-01-01', end='2023-12-31', freq='B') # Business days
-
接下来,我们必须为日期范围生成随机股价,长度为n:
n = len(date_range) data = { 'open': np.random.uniform(100, 200, n), 'high': np.random.uniform(200, 300, n), 'low': np.random.uniform(50, 100, n), 'close': np.random.uniform(100, 200, n) }
-
接下来,我们必须通过传递在上一步创建的所有单独数据点来创建一个 DataFrame:
df = pd.DataFrame(data, index=date_range)
-
现在,让我们引入随机的 NaN 值,以模拟数据中的一些缺失值:
nan_indices = np.random.choice(n, size=100, replace=False) df.iloc[nan_indices] = np.nan
-
然后,随机丢弃一些日期,以模拟缺失的时间戳:
missing_dates = np.random.choice(date_range, size=50, replace=False)
-
最后,显示 DataFrame 的前几行:
open high low close 2020-01-01 137.454012 262.589138 55.273685 183.849183 2020-01-02 195.071431 288.597775 82.839005 180.509032 2020-01-03 173.199394 261.586319 91.105158 182.298381 2020-01-06 159.865848 223.295947 69.021000 193.271051 2020-01-07 NaN NaN NaN NaN
这里需要注意的关键点是,我们有两种缺失数据:
-
完整的行缺失,因此没有完整的日期索引可用
-
当前日期的某些列中的部分观测值缺失
我们将在这里主要处理第一种情况,因为第二种情况在*第八章**《检测和处理缺失值与异常值》中已经讲过了。让我们从简单而有效的isnull()
方法开始。
检查 NaN 或空值
与常规数据集不同,时间序列数据点按时间顺序排列。缺失的值可能会破坏数据的连续性,影响趋势和季节模式的分析。我们可以使用isnull()
方法来识别缺失的时间戳。这里,我们要查找数据集中缺失的完整行:
-
要检查时间序列 DataFrame 中哪些日期缺失,我们需要创建一个完整的日期范围(没有缺失值),并且该日期范围的频率与当前 DataFrame 索引的频率一致,然后将其与当前 DataFrame 中的日期范围进行对比。这里,我们正在为工作日创建一个完整的日期范围:
complete_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B')
-
为了快速查看缺失的索引点,必须将 DataFrame 重新索引到这个完整的日期范围,以便识别任何缺失的时间戳:
df_reindexed = df.reindex(complete_index)
-
现在,我们可以使用
isnull()
方法来识别任何缺失的时间戳:missing_timestamps = df_reindexed[df_reindexed.isnull().any(axis=1)]
在这里,我们可以看到数据中有一些缺失的时间戳:
print(f"\nPercentage of Missing Timestamps: {missing_timestamps_percentage:.2f}%")
Percentage of Missing Timestamps: 14.09%
到目前为止的分析告诉我们,我们的数据集中缺失了完整的日期。现在,让我们添加一些可视化图表,帮助我们更好地看到数据中的空白。
注意
如在第八章**,检测与处理缺失值和异常值中所述,你可以使用isnull()
方法查看每列中缺失的数量——例如,missing_values =
df.isnull().sum()
。
目视检查
可视化数据有助于我们识别缺失值及缺失模式。图表能够揭示数据中的缺口,而这些缺口在表格检查中可能不易察觉。
继续前一部分的例子,让我们绘制时间序列数据并在图表上标出任何缺失值:
-
绘制闭盘价格图:
plt.figure(figsize=(14, 7)) plt.plot(df.index, df['close'], linestyle='-', label='Closing Price', color='blue')
-
用垂直线标记缺失的时间戳:
for date in missing_dates: plt.axvline(x=date, color='red', linestyle='--', linewidth=1) plt.title('Daily Closing Prices with Missing Timestamps and NaN Values Highlighted') plt.xlabel('Date') plt.ylabel('Closing Price') plt.legend() plt.grid(True) plt.show()
这将生成以下图表:
图 11.6 – 日闭盘价及缺失时间戳高亮显示
在图 11.6中,闭盘价格用蓝色标记显示,而缺失的时间戳用虚线高亮,便于识别数据中的缺口。现在,让我们探讨最后一种方法,称为滞后分析。在这种方法中,我们创建一个滞后的序列版本,并与原始数据进行比较,以检测不一致之处。
注意
在第三章,数据剖析 – 理解数据结构、质量和分布中,我们演示了多种数据剖析方法。你可以通过使用内建的缺口分析功能,将类似的方法应用于时间序列数据。只需在创建报告时传递tsmode=True
即可——例如,profile =
ProfileReport(df, tsmode=True)
。
随着我们深入,探索有效的时间序列缺失数据处理策略变得至关重要。
处理时间序列数据中的缺失值
缺失值是时间序列数据中常见的问题,可能由于多种原因产生,比如传感器故障、数据传输问题,或只是记录观察值的缺失。如我们所讨论的,通常会出现两种主要情况:
-
某些特征中的空值:想象一下股票市场分析,其中收集了每日交易数据。虽然所有交易日都已记录,但某些日子的成交量可能由于报告错误而缺失。这种情况带来了一个挑战:如何在确保分析保持稳健的同时,保持数据集的完整性?
-
完整行缺失:相反,考虑一个天气监测系统,它记录每日气温。如果某些完整天的数据缺失——可能是由于传感器故障——这就构成了一个重大问题。缺失的时间戳意味着你不能简单地填充数据;这些天的数据缺失会打乱整个时间序列。
在下一部分中,我们将重点解决第一种情况,考虑某些特征中缺失值的存在。完成这一步后,我们可以调整方法来处理第二种情况。
删除缺失数据
删除缺失数据是一个直接的方法,但应该谨慎操作,并考虑其对整体数据集的影响。以下是一些可能适合删除数据的场景:
-
如果缺失值占数据集的比例很小(例如,少于 5%),删除它们可能是可行的。如果数据丢失不会显著影响分析结果或从数据集得出的结论,这种方法效果较好。例如,在一个包含 10,000 个时间点的数据集中,如果有 50 个时间点缺失,删除这 50 个数据点(占数据的 0.5%)可能不会显著影响整体分析。
-
如果插补缺失值会引入过多的不确定性,特别是当这些值非常重要且无法准确估计时。这种情况通常出现在缺失值是高度不可预测的数据时,插补结果不可靠。
-
如果缺失值完全是随机发生的,并且没有遵循任何系统的模式。例如,传感器数据中偶尔出现的随机故障导致缺失读数,但这些故障没有任何潜在的规律。
让我们重新审视股票市场的使用案例,看看如何删除 null 值,并观察这对数据集的影响。
删除缺失数据在股票市场使用案例中的应用
在我们的股票价格数据场景中,我们将添加一些 NaN 值,并评估删除这些值的影响。你可以在这里找到完整的代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/2.remove_missing_values.py
。让我们开始吧:
-
继续使用上一节的示例,我们将创建具有不同特征的股票数据。然后,我们将从特定特征(例如,
close
和open
)中随机选择一些索引,以便将该索引的每个特征的值映射为 NaN 值:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False)
-
然后,我们将之前随机选择的索引映射为 NaN 值:
df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan
-
让我们检查数据中有多少 NaN 或 null 值:
missing_values = df.isnull().sum() Percentage of Missing Values in Each Column: open 4.793864 high 0.000000 low 0.000000 close 4.793864
正如预期的那样,
open
和close
特征中引入了一些 null 值。在删除数据集中的任何行之前,让我们先检查一下包含 null 值的数据行数:
print(f"\nNumber of rows before dropping NaN values: {len(df)}") Number of rows before dropping NaN values: 1043
-
在这个阶段,我们将删除在
close
或low
列中包含 NaN 值的任何行:df_cleaned = df.dropna() print(f"\nNumber of rows after dropping NaN values: {len(df_cleaned)}") ---- Number of rows after dropping NaN values: 945
-
让我们绘制时间序列数据:
图 11.7 – 删除/标记缺失数据后的每日收盘价格
如图 11.7所示,原始的收盘价已被绘制,因缺失值被丢弃的点通过红色“x”标记突出显示。请记住,即使是选择性丢弃,删除行也可能会导致有用信息的丧失,因为它减少了样本大小,从而可能降低分析的统计功效并影响结果的普适性。
在需要保留每个时间戳,但需要解决特征中的缺失值的场景中,前向和后向填充提供了实用的解决方案。这些方法允许我们保持时间序列数据的时间顺序完整性,同时根据相邻的观测值高效地填补缺失值。让我们探索一下前向和后向填充如何有效地处理时间序列分析中的缺失数据。
前向填充和后向填充
Forward fill(ffill)和backward fill(bfill)是通过将最后已知值向前传播或将下一个已知值向后传播来填补缺失值的两种方法,分别用于时间序列中的缺失数据。
在处理时间序列反向填充时,选择ffill和bfill之间的方式取决于多个因素和使用场景。以下是何时使用每种方法的概述,以及做出这些决策时的思考过程:
-
Ffill:前向填充,也称为最后观测值向前填充(LOCF),是将最后已知值向前传播以填补缺失的数据点。
这是你应该使用它的情况:
-
当你认为最近已知的值是预测未来缺失值的最佳依据时
-
在金融时间序列中,将最后已知价格向前传播通常是一个合理的假设
-
在处理缓慢变化的变量时,如果假设其持续性较好
-
在你希望保持最近状态,直到新的信息变得可用时
如果你仍然不确定,或者在思考该使用哪种方法时,回答以下三个问题中的至少两个“是”将帮助你做出正确的决策:
-
该变量是否可能在短时间内保持相对稳定?
-
使用最后已知值作为缺失数据的合理假设吗?
-
是不是更重要的是反映最近已知的状态,而不是潜在的未来变化?
-
-
Bfill:与此相反,后向填充是将下一个已知值向后传播以填补缺失的数据点。
这是你应该使用它的情况:
-
当你对未来的值比过去的值更有信心时
-
在你希望将已知的结果追溯性地应用于之前缺失的时间段时
-
当你处理滞后效应时,未来的事件会影响过去的缺失数据
-
在你希望将数据与下一个已知状态对齐,而不是与之前的状态对齐时
如果你仍然不确定,或者在思考该使用哪种方法时,回答以下问题中的“是”将帮助你做出正确的决策:
-
下一个已知值是否更有可能代表缺失数据而不是前一个已知值?
-
您是否处理一种情况,即未来信息应该通知过去的缺失值?
-
是否与下一个已知状态对齐能为您的分析提供更有意义的见解?
-
在实践中,选择 ffill 和 bfill 通常需要结合领域专业知识、对数据生成过程的理解以及考虑特定分析目标。同时,值得尝试两种方法并比较结果,看哪一种为您的特定用例提供更有意义和准确的见解。
使用 ffill 和 bfill 处理时间序列数据时,始终有一些重要的考虑因素。让我们扩展一下:
-
顺序性质:时间序列数据的顺序性质对于 ffill 和 bfill 方法确实至关重要。这两种方法都依赖于相邻数据点相关的假设,这是时间序列分析的基础。
-
Ffill 和上升趋势:Ffill 可适用于上升趋势,因为它向前延续最后已知的值,可能在上升趋势中低估真实值。然而,在强烈上升趋势中可能会导致“阶梯”效应,可能低估增长率。
-
Bfill 和下降趋势:Bfill 可适用于下降趋势,因为它会拉回未来更低的值,可能在下降趋势中高估真实值。在强烈下降趋势中可能会产生类似的“阶梯”效应,可能会夸大下降率。
-
在选择 ffill 和 bfill 时,应考虑趋势的方向以及缺失数据期间的强度和长度。对于微妙的趋势,任一方法都可能适用,选择可能更多地取决于其他因素,如数据的性质或具体的分析目标。
-
如果缺失值与周围数据点不一致,这两种方法确实可能传播错误。对于长时间的缺失数据,填充值可能会显著偏离真实的基础模式。
-
处理异常值:如果异常值在一段缺失数据之前或之后,ffill 或 bfill 可能会传播这种异常值,扭曲系列。
-
数据连续性的假设:这两种方法都假设缺失的数据可以通过相邻的已知值合理地逼近,但这并不总是正确的。对于可能突然改变或存在不连续性的变量,这些方法可能不适用。
让我们重新看一下股票价格的例子,并看看如何填补空值列。
在股市使用案例中填充空值
在这个示例中,我们将不关注缺失的索引,只关注一些特征中缺失的数据。让我们深入研究代码——和往常一样,你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/3.back_forward_fill.py
找到完整的端到端代码:
-
这段代码将随机缺失值引入 DataFrame(
df
)的close
和open
列。它首先使用np.random.choice
从 DataFrame 的索引中随机选择 50 个索引。选中的索引存储在两个变量nan_indices_close
和nan_indices_open
中,这些变量对应于缺失值将被插入的行:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False)
-
以下代码使用
.loc
访问器在nan_indices_close
指定的索引处将NaN
赋值给close
列,类似地,在nan_indices_open
指定的索引处将NaN
赋值给open
列。实际上,这将在这两列中创建 50 个随机的缺失值,这对于模拟真实世界数据场景或测试数据处理技术非常有用:df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan
-
使用 ffill 和 bfill 填充 NaN 值:
df['close_ffill'] = df['close'].ffill() # Forward Fill df['close_bfill'] = df['close'].bfill() # Backward Fill
-
让我们来看一下结果:
print(df[['open', 'close', 'close_ffill', 'close_bfill']].head(20)) # Show first 20 rows
这将显示以下输出:
open close close_ffill close_bfill 2020-01-01 137.454012 183.849183 183.849183 183.849183 2020-01-02 195.071431 180.509032 180.509032 180.509032 2020-01-03 173.199394 182.298381 182.298381 182.298381 2020-01-06 159.865848 193.271051 193.271051 193.271051 2020-01-07 115.601864 NaN 193.271051 120.028202 2020-01-08 115.599452 120.028202 120.028202 120.028202 2020-01-09 105.808361 161.678361 161.678361 161.678361 2020-01-10 186.617615 174.288149 174.288149 174.288149 2020-01-13 160.111501 173.791739 173.791739 173.791739 2020-01-14 170.807258 152.144902 152.144902 152.144902 2020-01-15 102.058449 NaN 152.144902 137.111294 2020-01-16 196.990985 137.111294 137.111294 137.111294
正如我们所见,在2020-01-07
和2020-01-15
,close
列中有缺失值(NaN)。这表示这两个日期的收盘价没有被记录或无法获取。
如我们所学,ffill 方法(close_ffill
)通过最后一个有效观测值填充缺失值:
-
对于
2020-01-07
,收盘价使用来自2020-01-06
的最后一个已知值(193.27
)进行填充。 -
对于
2020-01-15
,缺失值使用来自2020-01-14
的最后一个有效价格(152.14
)进行填充。
另一方面,bfill 方法(close_bfill
)通过下一个有效观测值填充缺失值:
-
对于
2020-01-07
,由于没有立即记录下一个有效价格,它使用2020-01-08
的收盘价(120.03
)进行填充。 -
对于
2020-01-15
,该值使用来自2020-01-16
的下一个已知价格进行填充。
让我们仔细看看在执行不同填充方法后,数据发生了什么变化:
-
在
2020-01-07
,ffill 方法相比 bfill 方法高估了缺失值,bfill 则与下一个已知值更为接近。 -
在
2020-01-15
,ffill 和 bfill 提供了不同的估算结果,ffill 可能高估了该值,而 bfill 则较为准确。
一般建议,我们需要调查缺失值的模式。如果缺失值是随机且稀疏的,任一方法可能都合适。然而,如果存在系统性的模式,可能需要更复杂的插值方法,例如插值。插值允许我们通过利用数据集中的现有值来估算缺失的数据点,提供了一种更为细致的方式,能够捕捉随时间变化的趋势和模式。接下来我们将更详细地讨论这一点。
插值
插值是一种通过根据周围数据点填补缺口来估算缺失值的方法。与前向填充(ffill)和后向填充(bfill)不同,后者是复制现有值,插值使用数学技术来估算缺失值。插值有不同的技术和应用。所以,让我们看一下可用的选项及其考虑因素:
-
线性插值:线性插值通过一条直线连接两个相邻的已知数据点,并沿此线估算缺失值。它是最简单的插值形式,假设数据点之间存在线性关系。它适用于数据点之间的变化预计为线性或近似线性的情况。常用于金融数据、温度读数和其他预期逐渐变化的环境数据中。
-
多项式插值:多项式插值将一个多项式函数拟合到已知的数据点,并使用这个函数来估计缺失值。更高阶的多项式可以捕捉数据点之间更复杂的关系。它适用于具有非线性趋势的数据集,通常用于科学和工程应用中,其中数据遵循多项式趋势。
-
样条插值:样条插值使用分段多项式,通常是三次样条,来拟合数据点,确保数据点的平滑性,并通过数据提供平滑的曲线。它适用于需要数据点之间平滑过渡的数据集,常用于计算机图形学、信号处理和环境数据中。
让我们在我们的用例中使用插值。
股票市场用例中的插值
考虑到之前提到的同一时间序列数据集,其中存在缺失值。在这种情况下,我们希望使用不同的插值方法来填补这些缺失值。你可以在本书的 GitHub 仓库中找到完整的代码示例:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/3.missing_values/4.interpolation.py
。让我们开始吧:
-
以下代码将随机缺失值引入我们的数据框(
df
)中的close
和open
列,就像在上一节中所做的那样:nan_indices_close = np.random.choice(df.index, size=50, replace=False) nan_indices_open = np.random.choice(df.index, size=50, replace=False) df.loc[nan_indices_close, 'close'] = np.nan df.loc[nan_indices_open, 'open'] = np.nan
-
以下代码行用于通过线性插值填充我们的 DataFrame(
df
)中close
列的缺失值。该代码特别使用线性插值,其中通过在缺失值前后最近的已知数据点之间画一条直线来估算缺失值:df['close_linear'] = df['close'].interpolate(method='linear')
-
我们可以通过将方法参数更改为
method='polynomial'
来使用多项式插值填充缺失值。这指定插值应使用order=3
的多项式函数。order
参数表示要使用的多项式的次数。在这种情况下,使用三次多项式(三次方),意味着估算缺失值的函数将是一个曲线,可能比简单的直线(如线性插值)提供更好的拟合,以适应更复杂的数据趋势:df['close_poly'] = df['close'].interpolate(method='polynomial', order=3)
-
我们可以通过将方法更改为
method='spline'
来使用样条插值填充缺失值。这指定插值应使用样条插值,这是一种分段的多项式函数,确保数据点处的平滑性。order=3
参数表示每段样条使用的多项式的次数。在这种情况下,使用三次样条(第三次多项式),意味着插值将涉及拟合三次多项式到数据的各个段落:df['close_spline'] = df['close'].interpolate(method='spline', order=3)
-
现在,让我们绘制插值后的数据:
图 11.8 – 日闭盘价插值
在图 11.8中,我们可以看到不同插值方法下数据的变化。为了更好地理解这些差异,让我们看看插值后的实际数据,如图 11.9所示:
图 11.9 – 日闭盘价插值表
让我们比较不同的插值方法并得出一些结论:
在 2020-01-07,我们有以下数据:
-
线性插值:156.649626
-
多项式插值:142.704592
-
样条插值:143.173016
在 2020-01-15,我们有以下数据:
-
线性插值:144.628098
-
多项式插值:127.403857
-
样条插值:128.666028
根据这些数据,线性插值似乎提供了比多项式和样条插值更高的估计值。它假设数据点之间存在线性趋势,这对于非线性数据可能并不准确。多项式插值似乎提供了较低的估计值并能够捕捉更复杂的关系,但也容易过拟合。最后,样条插值提供了平滑的估计值,介于线性插值和多项式插值之间,提供了简单性与准确性之间的平衡。在这种具体情况下,我们会选择样条插值,因为它提供了一条平滑的曲线,避免了突变,结果更现实,更接近数据中预期的趋势。虽然基于提供的数据推荐使用样条插值,但验证插值结果与已知数据点或领域知识的符合性仍然是至关重要的。
注意
插值方法,如线性插值、多项式插值和样条插值,也可以用来处理时间序列数据中的异常值。
选择和调整插值参数来填充缺失值,需要理解数据的特征和分析的具体需求。对于具有线性趋势的简单数据,线性插值既高效又有效。然而,如果数据表现出非线性模式,多项式插值可能提供更好的拟合,且多项式的阶数(order
)会影响曲线的复杂度;较低的阶数适用于简单趋势,而较高的阶数可能能捕捉更多的细节,但也有过拟合的风险。样条插值提供了一种平滑而灵活的方法,立方样条(order=3
)因其平滑性和灵活性而被广泛使用。调优这些方法时,可以从较简单的方法开始,逐步测试更复杂的方法,同时监控过拟合现象,并确保拟合与数据的潜在趋势一致。采用交叉验证、视觉检查和统计指标来评估和优化插值选择。
现在我们已经探讨了时间序列中处理缺失数据的各种技术,接下来总结不同的填充方法是非常重要的,以便理解它们独特的应用和有效性。
比较不同的缺失值处理方法
处理时间序列数据中的缺失值是一个复杂的过程,需要仔细考虑数据集的具体背景和特征。决定是丢弃值、使用 bfill,还是应用插值,应根据对后续分析影响的仔细评估以及保留时间序列中关键信息的需要来指导。下表总结了不同的技术,并可作为指导:
方法 | 使用时机 | 优点 | 缺点 |
---|---|---|---|
填充缺失值 | 小比例的缺失值 | - 简单性- 避免插值不确定性 | - 信息丢失- 潜在偏差 |
向后填充(Bfill) | 缺失值预计之前有一致的值 | - 保留总体趋势- 适用于递增趋势 | - 如果缺失值与随后的值不同,可能会传播错误 |
向前填充(Ffill) | 缺失值预计遵循一致的值 | - 实现简单- 保持最近状态直到新数据可用 | - 如果趋势变化,可能会误导数据- 如果缺失值与之前的值不同,则会传播错误 |
线性插值 | 缺失值需要根据相邻的数据点进行估算 | - 实现简单易懂- 保留整体趋势 | - 可能无法捕捉非线性趋势- 对离群值敏感 |
多项式插值 | 缺失值需要通过更复杂的关系进行估算 | - 捕捉复杂关系- 对多项式阶数具有灵活性 | - 可能导致过拟合和振荡- 计算量大 |
样条插值 | 缺失值需要通过平滑过渡来估算 | - 提供平滑曲线- 避免高阶多项式的振荡 | - 实现较为复杂- 计算量大 |
表 11.1 – 不同时间序列缺失数据处理方法的比较
在研究了填充时间序列数据缺失值的各种方法之后,另一个同样重要的方面是:时间序列与其自身滞后值的相关性。
时间序列数据分析
自相关和偏自相关是时间序列分析中的关键工具,它们提供了数据模式的洞察并指导模型选择。在离群值检测中,它们有助于区分真实的异常和预期的变动,从而实现更准确、更具上下文感知的离群值识别。
自相关和偏自相关
自相关是指将时间序列与其自身滞后值进行相关分析。简而言之,它衡量时间序列中每个观察值与其过去观察值的关系。自相关是理解时间序列数据中存在的时间依赖性和模式的关键概念。
偏自相关函数(PACF)是时间序列分析中的一种统计工具,用于衡量在去除中间滞后效应后,时间序列与其滞后值之间的相关性。它提供了一个更直接的衡量不同时间点观察值之间关系的方式,排除了较短滞后效应的间接影响。
自相关和偏自相关在以下情况中有帮助:
-
时间模式:它们有助于识别随时间重复的模式。这对于理解时间序列数据的固有结构至关重要。
-
平稳性评估:它们有助于评估时间序列的平稳性。缺乏平稳性可能会影响统计分析的可靠性以及模型预测的准确性。
-
模型的滞后选择:它们指导时间序列模型中适当滞后的选择,如自回归(AR)分量在自回归滑动平均(ARIMA)模型中的应用。
-
季节性检测:在特定滞后期的自相关函数(ACF)图中出现显著峰值,表明存在季节性,为进一步分析提供了线索。
-
异常检测:自相关函数中出现不寻常的模式可能表明数据中存在异常值或离群点,需要进一步调查和清理。
现在,让我们对来自股票价格数据集的close_filled
序列进行 ACF 和 PACF 分析。此分析将帮助我们确定适当的参数(p
和q
),以便在接下来的部分中进行 ARIMA 建模。
股票市场案例中的 ACF 和 PACF
我们将继续使用到目前为止的示例,并添加 ACT 和 PACF 图表。像往常一样,您可以查看完整代码:github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/4.analisis/autocorrelation.py
。让我们开始吧:
-
创建自相关图:
plot_acf(df['close'].dropna(), lags=40, ax=plt.gca())
-
创建部分自相关图:
plot_pacf(df['close'].dropna(), lags=40, ax=plt.gca())
结果图如下所示:
图 11.10 – ACF 和 PACF 图
让我们解释一下前面图表中可以看到的内容。对于自相关函数(ACF),我们可以看到以下内容:
-
ACF 图显示了序列与其滞后值在不同滞后期(本例中
lags=40
)的相关性。ACF 图的X轴表示滞后期的数量,指示计算相关性时回溯的时间点数。 -
ACF 图的Y轴表示原始时间序列与其滞后值之间的相关系数。相关值的范围从-1 到 1。
-
蓝色阴影区域表示置信区间。超出阴影区域的柱状条被认为具有统计显著性,表明在这些滞后期存在强烈的自相关性,并可能为ARIMA 模型中的 q 参数(MA 阶数)提供潜在的值,正如我们在接下来的部分中将看到的。
-
在固定间隔处出现显著峰值表明时间序列数据中存在季节性。
-
如果 ACF 图在滞后 1 期存在显著的自相关(如我们案例中的蓝色阴影区域之外的尖峰),则表明该序列与其即时前值之间有很强的相关性。这可能意味着该序列是非平稳的,可能需要差分(d > 0)。
对于 PACF,我们可以看到以下内容:
-
PACF 图显示了时间序列与其滞后值之间的相关性,去除了由较短滞后解释的效应。
-
PACF 图中的显著峰值表明滞后 1 和潜在的滞后 2 可能是 ARIMA 模型中p 参数(AR 阶数)的良好候选项。
注意
当我们在 ACF 和 PACF 图中指定lags=40
时,我们是在检查时间序列在 40 个不同滞后区间的自相关和偏自相关。这意味着我们将看到序列如何与自身在滞后 1 到 滞后 40之间的相关性。
ACF 和 PACF 图对于识别时间序列中的基本结构至关重要。在接下来的部分中,我们将把 ACF 和 PACF 分析与异常值检测和处理联系起来,确保我们的时间序列模型准确捕捉到潜在模式。
处理异常值
时间序列数据通常表现出季节性模式(例如,假期期间的销售峰值)和趋势(例如,多年来的逐步增长)。在这种情况下,异常值可能并不是异常现象;它可能反映了正常的季节性效应或基础趋势的变化。例如,黑色星期五期间零售销售的突然激增是可以预期的,不应视为异常值。诸如时间序列季节性分解(STL)、自相关和季节性指数等技术可以帮助理解数据的预期行为,从而为识别异常值提供更清晰的基础。
使用季节性分解识别异常值
识别时间序列中的异常值的一种方法是将序列分解为趋势、季节性和残差组件,因为异常值通常出现在残差组件中。要将序列分解为趋势、季节性和残差组件,我们可以使用 STL 方法。该方法通过分析残差组件(理想情况下应该是白噪声)来帮助识别和处理异常值。让我们看看如何使用股市数据来实现这一点。你可以在github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/5.outliers/1.seasonal_decomposition.py
找到完整的代码示例:
result = seasonal_decompose(df['close'], model='additive', period=252)
在这段代码中,我们在假设每年有 252 个工作日的情况下对时间序列进行分解。我们还将计算残差的 Z 值,以便使用以下代码识别异常值:
df['resid_z'] = zscore(df['residual'].dropna())
最后,让我们绘制分解后的序列:
图 11.11 – 分解后的时间序列
可以通过分析残差组件来检测异常值。残差组件中显著偏离零的值或突增表示潜在的异常值:
图 11.12 – 分解值表
基于图 11.11中的分解时间序列,我们可以通过检查残差
和resid_z
列来分析异常值。通常,绝对值大于 2 或 3 的 Z 分数被视为潜在的异常值。在该数据集中,最大正残差出现在2020-01-06
(Z 分数:1.468043)、2020-01-17
(Z 分数:1.300488)和2020-01-27
(Z 分数:1.172529)上,而最大负残差出现在2020-01-15
(Z 分数:-1.721474)和2020-01-22
(Z 分数:-1.082559)上。尽管这些数值显示出与趋势和季节性成分的某些偏差,但没有一个 Z 分数超过典型的±2 或±3 阈值,表明该数据集没有极端异常值。残差似乎相对均匀地分布在零周围,表明分解模型拟合良好。然而,具有最大偏差的日期(2020-01-06
、2020-01-15
和2020-01-17
)可能值得进一步调查,看看是否有任何异常事件或因素可以解释它们偏离预期值的原因。
深入挖掘这些数据以了解波动背后的原因,并仔细检查后,我们可以看到这些日期的偏差是由特定事件和系统问题引起的:
免责声明!
以下事件对应的是虚构事件!
-
2020-01-06
:股市交易系统的技术故障导致价格暂时激增 -
2020-01-15
:错误的交易输入导致价格突然下跌,随后被修正 -
2020-01-17
:一次重大经济公告导致波动性增加,并使股价短暂上涨 -
2020-01-22
:关于季度财报结果的误传引发了暂时的恐慌性抛售 -
2020-01-27
:关于并购的谣言引发了投机性购买,暂时抬高了价格
这些发现帮助我们理解到,残差的偏差并非随机发生,而是由于特定的、可识别的事件所致。虽然这些事件在统计上不符合显著异常值的标准,但它们突显了股价数据中固有的波动性和噪声。鉴于股价的噪声特性,即使没有显著的异常值,平滑技术仍然变得至关重要!
处理异常值 – 基于模型的方法 – ARIMA
ARIMA 模型广泛用于时间序列数据的预测。它们根据过去的观测值预测未来的数值,使得通过将实际值与预测值进行比较,从而有效地识别异常值。ARIMA 模型由三个主要部分组成:
-
自回归(AR):利用观测值与多个滞后观测值之间的依赖关系(p)
-
集成(I):通过对观测值的差分处理,使时间序列平稳化(d)
-
移动平均(MA):利用观测值与应用于滞后观测值的移动平均模型的残差误差之间的依赖关系(q)
ARIMA 模型在处理以下异常值时有效:
-
加性异常值(AO):时间序列中的突升或突降
-
创新异常值(IO):影响整个序列的变化,从发生点开始向后延伸
让我们讨论一下 ARIMA 模型如何在我们一直在处理的股票价格数据示例中用于异常值检测和平滑。您可以在 github.com/PacktPublishing/Python-Data-Cleaning-and-Preparation-Best-Practices/blob/main/chapter11/5.outliers/3.arima.py
找到完整示例:
-
对
close_filled
序列拟合 ARIMA 模型:model = ARIMA(df['close_filled'], order=(2,1,1)) results = model.fit()
-
计算残差和 Z 分数:
df['residuals'] = results.resid df['residuals_z'] = zscore(df['residuals'].dropna())
-
基于 Z 分数阈值(例如,±3)识别任何异常值:
outliers_arima = df[np.abs(df['residuals_z']) > 3]
-
可视化原始
close_filled
序列和从 ARIMA 模型获得的平滑序列:df['arima_smooth'] = results.fittedvalues
以下是输出结果:
图 11.13 – ARIMA 平滑和异常值检测
-
生成诊断图以评估模型拟合,包括残差分析、分位数-分位数(Q-Q)图和标准化残差:
results.plot_diagnostics(figsize=(14,8))
-
结果图如下:
图 11.14 – 残差分析、Q-Q 图和标准化残差
让我们深入探讨一下 图 11.14 中显示的诊断图:
-
标准化残差:标准化残差是通过其标准差对 ARIMA 模型的残差进行缩放得到的。为了使 ARIMA 模型被认为是一个良好的拟合,标准化残差应当像白噪声一样,意味着它们不应显示出明显的模式。这意味着残差是随机分布的,均值为零,方差恒定。如果残差中出现模式,则表明模型未能捕捉到数据中的某些潜在结构,可能需要进一步的调整。在我们的案例中,残差看起来像白噪声。
-
直方图加核密度估计(KDE):结合残差的**核密度估计(KDE)**图,提供了对残差分布的可视化评估。对于拟合良好的 ARIMA 模型,残差应遵循正态分布。直方图应呈现典型的钟形曲线,KDE 图应叠加一条与之匹配的平滑曲线。若与正态分布存在偏差,如偏态或重尾,表明残差不是正态分布,这暗示模型可能存在问题。在我们的案例中,我们没有看到残差中有显著的偏态或尾部。
-
正态 Q-Q 图:Q-Q 图将残差的分位数与正态分布的分位数进行比较。如果残差服从正态分布,Q-Q 图上的点将沿着 45 度线排列。显著偏离这条线的点表示偏离正态分布。在我们的案例中,我们没有看到任何显著的偏差。
-
自相关图(残差的自相关函数(ACF)):自相关图展示了残差的自相关函数(ACF)。对于一个合理指定的 ARIMA 模型,残差应该没有显著的自相关。这意味着没有任何滞后项的自相关系数应该具有统计学上的显著性。ACF 图中的显著峰值表明残差仍然与其过去的值相关,暗示模型尚未完全捕捉到时间序列的结构。这可以指导进一步的模型优化,比如增加 AR 或 MA 组件的阶数。在我们的案例中,一切看起来都很好。
什么是滞后 0?
在自相关图(ACF 图)中,滞后 0 是指时间序列与自身在滞后 0 时的自相关,实际上是时间序列与自身在相同时间点的相关性。根据定义,这个相关性总是 1,因为任何时间序列在滞后 0 时与自身完全相关。这意味着滞后 0 时的自相关值总是 1,这就是为什么在 ACF 图中滞后 0 处会看到一个峰值的原因。
玩弄不同的设置并观察它们对 ARIMA 模型和残差的影响是一个好主意。
在探索使用 ARIMA 方法检测和处理我们股票价格数据集中的异常值后,我们发现异常值会显著影响我们时间序列模型的准确性和可靠性。虽然 ARIMA 方法有助于识别和调整这些突变,但考虑其他稳健的异常值检测和处理方法也非常重要。接下来的部分我们将介绍其中一种方法,即使用移动窗口技术。
移动窗口技术
移动窗口技术,也称为滚动或滑动窗口方法,涉及分析一个固定大小的数据子集或“窗口”,该窗口会在较大的数据集上顺序移动。在窗口的每个位置,都会应用特定的计算或函数,例如计算均值、中位数、总和或更复杂的统计量。当窗口通过一个或多个数据点滑动时,计算会使用新的数据子集进行更新。该方法在时间序列分析中尤其稳健,通常用于平滑数据、识别趋势或检测随时间变化的异常值。
移动窗口技术的优势在于它能够提供局部分析,同时与更广泛的数据集保持联系。例如,在平滑时间序列时,移动平均可以减少噪声并突出底层趋势,而不会扭曲整体信号。类似地,在金融数据中,移动窗口可以用来计算滚动平均值或波动性,提供市场条件的实时视图。
在本节中,我们将重点介绍两种主要方法:简单移动平均(SMA)和指数加权移动平均(EMA)。这两者都可以作为基础,稍后可以通过其他统计量(如中位数)进行调整。
SMA
SMA是常用的统计计算,表示一组数据点在指定时间内的平均值。它是一种移动平均,通过平滑数据中的波动,更容易识别趋势。SMA 通过将一组值相加,并将总和除以数据点的数量来计算。更先进的方法,如卡尔曼平滑,可以通过建模底层过程来估计缺失值:
SMAt = (Xt + Xt–1 + Xt–2 + …+ Xt–n+1)/n
在这里,我们有如下公式:
-
SMAt 是时刻t的 SMA。
-
Xt + Xt–1 + Xt–2 + …+ Xt–n+1 是该时间段的数据值。
-
n是参与计算的周期数。
现在,让我们介绍指数加权移动平均(EMA),以便我们可以对比这两者。
EMA
EMA对最近的数据点赋予更多的权重,对较早的数据点赋予较少的权重。它使用指数衰减公式:
EMAt = α • Xt + (1 – α) • EMAt–1
其中,α是平滑因子。
现在,让我们讨论一下 SMA 和 EMA 如何在我们一直在使用的股票价格数据示例中进行异常值检测和平滑。
使用 SMA 和 EMA 对股票价格进行平滑
继续使用我们之前呈现的股票价格数据示例,让我们看看 SMA 和 EMA 对数据的影响:
首先,让我们计算 12 个月窗口的 SMA:
-
定义 SMA 的
窗口
大小和 EMA 的跨度
大小:window_size = 20 span = 20
-
计算 SMA:
df['SMA'] = df['close'].rolling(window=window_size, min_periods=1).mean()
-
计算 EMA:
df['EMA'] = df['close'].ewm(span=span, adjust=False).mean()
-
计算 SMA 和 EMA 的残差:
df['SMA_residuals'] = df['close'] - df['SMA'] df['EMA_residuals'] = df['close'] - df['EMA'] sma_window = 12 data['SMA'] = data['Passengers'].rolling(window=sma_window).mean()
-
绘制原始时间序列和 SMA:
图 11.15 – SMA 和 EMA
在这个例子中,我们使用 20 的窗口大小和 20 的跨度分别计算了 SMA 和 EMA。SMA 的窗口大小决定了在每个时间点计算平均值时包含多少个之前的数据点。和 SMA 一样,你数据点的频率会影响跨度的选择。如果你的数据是按日计的,跨度 20 大约代表过去 20 天的历史数据。
让我们再多讨论一下生成的图表:
-
SMA:
-
平滑效果:SMA 通过对窗口内的值进行平均来平滑时间序列数据,减少噪声,突出底层趋势。
-
异常值影响:虽然 SMA 减少了异常值的影响,但它仍可能会受到异常值的影响,因为它对窗口内的所有值赋予相同的权重。
-
-
EMA:
-
平滑效果:EMA 也对数据进行了平滑处理,但对最近的观察值赋予更多的权重,使其对近期变化更具响应性。
-
异常值影响:EMA 不太受较旧异常值的影响,但由于其加权机制,可能更容易受到近期异常值的影响。
-
在平滑度和响应性之间找到平衡
较大的窗口大小会导致更平滑的移动平均,但可能会滞后于数据的变化。较小的窗口大小使得移动平均对短期波动更具响应性,但可能引入更多噪声。
记得我们在图 11.10中创建的自相关图吗?我们可以利用该分析,根据观察到的自相关模式来调整跨度或窗口大小。以下几点将帮助你选择窗口大小和跨度:
-
考虑数据点的频率(每日、每周、每月)。
-
如果自相关图显示在较短滞后期存在显著的自相关,EMA 采用较小跨度或 SMA 采用较小窗口大小可以帮助保持对近期变化的响应,同时减少短期噪声的影响。
-
如果你的数据呈现季节性模式,你可能会选择与季节周期相符的窗口大小或跨度。例如,如果存在每周季节性,可能考虑使用 5 或 7 的窗口大小。可以使用自相关图来帮助确定这一点。
为了评估窗口模型的表现,我们可以使用平均绝对误差(MAE),以及均方误差(MSE)和均方根误差(RMSE)。我们可以比较原始数据和这些模型生成的平滑值之间的误差,如下图所示:
图 11.16 – SMA 和 EMA 的性能指标
为了确保我们清楚理解图 11.16中呈现的不同指标,让我们更详细地看一下:
-
MAE:这表示一组预测中的平均误差幅度,提供了预测值和实际值之间绝对差异的简单平均值。
-
MSE:该指标衡量预测值和实际值之间的平均平方差,比 MAE 更重视较大的误差。
-
RMSE:RMSE 是 MSE 的平方根,提供了一个可解释的平均误差幅度度量,与原始数据的尺度一致。
现在我们知道这些术语的含义,让我们来解读它们在股票价格案例中的应用。较低的 MAE、MSE 和 RMSE 值表示平滑方法的表现更好。虽然 SMA 和 EMA 的 MAE 和 RMSE 值非常接近,但指数加权法(EMA)的 MSE 值较低。
下表对何时使用 SMA 和 EMA 进行了比较和总结:
标准 | SMA | EMA |
---|---|---|
平滑类型 | 对数据点进行简单和均匀的平滑处理 | 更敏感和适应性强,对近期数据点赋予更大权重 |
数据点加权 | 对窗口中的所有数据点赋予相等权重 | 对近期观察值赋予更多权重;较老的观察值获得指数递减的权重 |
对变化的响应性 | 滞后指标;对近期变化响应较慢 | 对近期变化更敏感;快速适应数据的变化 |
稳定性适应性 | 适合稳定且波动较小的时间序列 | 适合波动性较大或变化迅速的时间序列 |
对趋势的适应性 | 平滑长期趋势,适合识别整体模式 | 对变化趋势的适应较快,适合捕捉近期变化 |
使用场景示例 | 分析长期趋势和识别季节性模式 | 捕捉短期波动并对市场波动做出反应 |
计算复杂性 | 计算较简单,易于理解和实现 | 更复杂的计算涉及平滑因子 |
表 11.2 – SMA 与 EMA 的比较
除了移动平均技术之外,探索高级特征工程步骤,如滞后和差分,可以显著丰富我们对数据的理解和预测能力。我们将在下一节中进行探讨。
时间序列数据的特征工程
有效的特征工程在时间序列分析中至关重要,可以揭示有意义的模式并提高预测准确性。它涉及将原始数据转化为能够捕捉时间依赖性、季节性变化以及时间序列其他相关方面的信息特征。我们要探索的第一个技术是创建特征的滞后。
滞后特征及其重要性
滞后特征是时间序列特征工程中的关键部分,因为它们允许我们将时间序列数据转换为适合监督学习模型的格式。滞后特征涉及创建代表目标变量过去观察值的新变量:
-
Lag 1:来自上一个时间步的值
-
Lag 2:来自两个时间步之前的值
-
Lag k:来自 k 个时间步之前的值
通过将时间序列数据按指定的时间步数(称为滞后)进行平移,这些过去的值会作为当前时间戳的特征包含在模型中。正如我们所知道的,时间序列数据通常表现出时间依赖性,即当前值与过去的观察值相关。滞后特征有助于捕捉这些依赖关系,使模型能够从历史模式中学习。
现在,让我们讨论如何在我们一直在使用的股价数据示例中应用滞后特征。
在股价使用案例中创建滞后特征
继续我们之前提出的股价数据示例,让我们看看滞后特征对数据的影响:
-
首先,在
close
列中引入更多激进的离群值:outlier_indices = np.random.choice(df.index, size=10, replace=False) df.loc[outlier_indices[:5], 'close'] = df['close'] * 1.5 # Increase by 50% df.loc[outlier_indices[5:], 'close'] = df['close'] * 0.5 # Decrease by 50%
-
使用以下函数创建滞后特征:
def create_lagged_features(df, column, lags): for lag in lags: df[f'{column}_lag_{lag}'] =df[column].shift(lag) return df # Define the lags to create lags = [1, 5, 10, 20]
-
为
close
列创建滞后特征:df = create_lagged_features(df, 'close', lags)
-
绘制原始时间序列和滞后数据集:
图 11.17 – 原始特征与滞后特征
正如我们在图 11.17中所看到的,滞后 1(close_lag_1
)表示前一天的收盘价,滞后 5(close_lag_5
)表示 5 天前的收盘价,依此类推。你可以观察每个滞后值如何捕捉目标变量的历史值。添加滞后特征到时间序列时,数据的起始日期会向前移动,因为在指定的滞后期结束之前,前几个数据点不能使用。这种偏移意味着,如果你添加更多的滞后,缺乏完整滞后数据的初始数据点数量会增加,从而有效地将起始日期向前推移。
可以自由尝试不同的滞后值,查看其对数据集的影响。调整滞后值可以帮助你捕捉数据中的不同时间依赖关系和趋势。
时间序列差分
在第四章《清理杂乱数据与数据操作》中,我们讨论了如何使用diff()
函数计算两个日期时间对象之间的时间差,这有助于我们测量连续事件之间经过的时间。这个技巧有助于理解时间戳序列中的时间间隔。类似地,在时间序列分析中,差分是一种强大的技术,通过去除时间序列的水平变化,稳定时间序列的均值,从而消除趋势和季节性。正如我们在上一章中计算了时间差一样,我们可以将差分应用于股市数据,以突出随时间变化的变化。然而,我们还将引入一个新术语——季节性差分。
季节性差分
季节性差分是一种用于去除时间序列数据中的季节性模式的技术,使其更加平稳,适合分析和预测。季节性差分通过将某个观测值与相隔季节性周期的前一个观测值相减来实现。因此,我们需要借助之前提供的工具识别季节性周期,然后取该季节性周期对数据进行差分。
对于具有年度季节性模式的月度数据,我们可以使用以下公式:
y’t = yt – yt–12
对于季度数据,我们可以使用以下公式:
y’t = yt – yt–4
这里是季节性差分后的序列,和原始序列。
现在,让我们讨论如何在我们一直在处理的股价数据示例中使用差分。
对股价数据进行差分
为了展示季节性差分,我们将在股票市场数据中引入一些季节性。根据我们目前的分析,数据中并没有明显的季节性成分。让我们开始吧:
-
创建一个季节性成分(每周季节性,幅度较大):
seasonal_component = 50 * np.sin(2 * np.pi * np.arange(n) / 5) # 5-day seasonality
-
生成加入季节性的随机股票价格:
data = { 'open': np.random.uniform(100, 200, n) + seasonal_component, 'high': np.random.uniform(200, 300, n) + seasonal_component, 'low': np.random.uniform(50, 100, n) + seasonal_component, 'close': np.random.uniform(100, 200, n) + seasonal_component } df = pd.DataFrame(data, index=date_range)
-
计算第一次差分:
df['First Difference'] = df['close'].diff()
-
计算第二次差分:
df['Second Difference'] = df['First Difference'].diff()
-
最后,计算季节性差分(每周季节性):
df['Seasonal Difference'] = df['close'].diff(5)
让我们通过绘制第一次、第二次和季节性差分来演示差分操作:
图 11.18 – 原始序列与差分序列
在图 11.18中,我们可以观察到第一次、第二次和季节性差分。我们可以看到在原始图中,存在一些季节性,但在第一次差分后,季节性成分被最小化了。但我们如何从统计学角度评估这一点呢?让我们进行一些统计检验,检查时间序列的平稳性。
增广的迪基-富勒(ADF)检验
ADF检验是一种用于确定时间序列是否平稳的统计检验。ADF 检验检验原假设:时间序列样本中存在单位根。单位根的存在表示时间序列是非平稳的。备择假设是时间序列是平稳的。对于 ADF 检验,数值越负,表明反对原假设的证据越强。
p 值表示假设原假设为真时,获得至少与观察结果一样极端的检验结果的概率。在 ADF 检验中,我们希望看到一个小的 p 值来拒绝原假设 即非平稳性。
要得出一个时间序列是平稳的结论,我们通常需要看到以下几点:
-
p 值 < 0.05:这是统计检验中最常用的阈值。如果 p < 0.05,我们会在 5%的显著性水平上拒绝原假设。这意味着我们有足够的证据得出该序列是平稳的结论。
-
更小的 p 值:p < 0.01(1%显著性水平)和 p < 0.001(0.1%显著性水平)提供了更强的平稳性证据。
让我们编写代码进行这个检验:
def adf_test(series, title=''):
result = adfuller(series.dropna(), autolag='AIC')
print(f'Augmented Dickey-Fuller Test: {title}')
print(f'ADF Statistic: {result[0]}')
print(f'p-value: {result[1]}')
for key, value in result[4].items():
print(f' {key}: {value}')
print('\n')
现在是时候查看结果了!我们将对原始时间序列进行检验(检查它是否平稳),然后对每个差分后的时间序列进行检验。让我们解释一下这些结果:
Augmented Dickey-Fuller Test: Original Series
ADF Statistic: -3.5898552445987595
p-value: 0.005957961883734467
1%: -3.4367333690404767
5%: -2.8643583648001925
10%: -2.568270618452702
ADF 统计量-3.5899 小于 5%的临界值-2.8644,且 p 值低于 0.05。这表明我们可以拒绝单位根存在的原假设,暗示原始序列可能是平稳的。然而,结果相对接近临界值,表明平稳性接近临界:
Augmented Dickey-Fuller Test: First Difference
ADF Statistic: -11.786384523171499
p-value: 1.0064914317100746e-21
1%: -3.4367709764382024
5%: -2.8643749513463637
10%: -2.568279452717228
ADF 统计量为-11.7864,远低于 5%的临界值-2.8644,且 p 值极小。这强烈表明第一次差分后的序列是平稳的。与原始序列相比,ADF 统计量的显著下降表明第一次差分有效去除了剩余的趋势或单位根:
Augmented Dickey-Fuller Test: Second Difference
ADF Statistic: -14.95687341689794
p-value: 1.2562905072914351e-27
1%: -3.4367899468008916
5%: -2.8643833180472744
10%: -2.5682839089705536
ADF 统计量为-14.9569,远低于 5%的临界值,且 p 值极小。该结果表明第二次差分后的序列也是平稳的。然而,过度差分可能导致有意义的模式丧失并增加噪声,因此,在实现平稳性和保持序列的完整性之间必须保持平衡:
Augmented Dickey-Fuller Test: Seasonal Differencing
ADF Statistic: -11.48334880444129
p-value: 4.933051350797084e-21
1%: -3.4367899468008916
5%: -2.8643833180472744
10%: -2.5682839089705536
最后,ADF 统计量为-11.4833,远低于 5%的临界值,且 p 值非常小。这表明季节性差分成功地使得序列平稳。如果序列在特定的时间间隔内表现出周期性模式,季节性差分特别有效。
根据这些结果,第一次差分似乎是最合适的选择,原因如下:
-
原始序列在 1%的显著性水平下已经是平稳的,但第一次差分显著提高了平稳性。
-
第一次差分产生了一个非常显著的结果(p 值:1.006e-21),且不会导致过度差分的风险。
-
虽然第二次差分显示出更为显著的结果,但它可能导致过度差分,从而引入不必要的复杂性,并可能移除序列中的重要信息。
-
季节性差分也显示出强劲的结果,但除非数据中有明确的季节性模式,否则通常更倾向于使用较为简单的第一次差分方法。
总结来说,第一次差分在实现平稳性和避免过度差分之间取得了良好的平衡。现在,接下来我们讨论一些在时间序列领域最常见的应用场景。
在不同的行业中应用时间序列技术
能够分析时间模式为各行各业提供了竞争优势,尤其是在今天数据驱动的世界中。以下是一些不同行业中的常见应用场景:
领域 | 应用场景 | 解释 |
---|---|---|
金融 | 股票市场分析 | 分析历史股价和交易量,以做出明智的投资决策 |
投资组合管理 | 评估投资组合的表现,以优化资产配置 | |
风险评估 | 建模和预测金融风险,如市场波动和信用违约 | |
医疗健康 | 患者监测 | 持续跟踪生命体征和健康指标,及早发现异常 |
流行病学 | 分析疾病传播的时间模式并预测疫情爆发 | |
治疗效果 | 评估医学干预措施随时间的效果 | |
气象学 | 天气预报 | 分析历史天气模式以预测未来气候 |
气候变化研究 | 监测气候数据中的长期趋势和变化 | |
自然灾害预测 | 早期检测潜在灾害,如飓风、洪水和干旱 | |
制造业 | 生产计划 | 预测需求并优化生产计划 |
质量控制 | 监控并确保产品质量 | |
设备维护 | 基于机械性能历史的预测性维护 | |
营销 | 销售预测 | 基于历史数据预测未来销售 |
客户参与度 | 分析客户与产品和服务的互动模式 | |
活动优化 | 评估营销活动随时间的影响 | |
领域 | 用例 | 说明 |
交通运输 | 交通流量分析 | 监控并优化城市地区的交通模式 |
车辆追踪 | 追踪运输车队的移动和效率 | |
供应链优化 | 预测需求并优化商品在时间中的流动 |
表 11.3 – 时间序列技术应用场景
有了这些,我们可以总结这一章的内容。
总结
时间序列分析在从各种行业中提取有意义的见解并做出明智决策中起着至关重要的作用。随着技术的发展,复杂的时间序列技术将变得越来越重要,用于理解复杂的时间模式和趋势。无论是在金融、医疗保健还是交通运输中,分析和预测时间依赖数据的能力使组织能够适应、优化并在不断变化的环境中做出战略决策。
在这一章中,我们介绍了处理缺失值和异常值的技术、差分方法,以及时间序列分析中的特征工程。我们学习了如何使用 ffill 和 bfill 处理缺失值,并比较了它们对股票价格数据的影响。我们还应用了包括一阶、二阶和季节性差分在内的差分技术,以实现平稳性,并通过 ADF 检验进行评估。我们还探索了滞后特征以捕捉时间依赖关系,并使用 MAE、MSE 和 RMSE 等指标评估了模型性能。这些技能将使你能够有效地管理和分析时间序列数据。
在下一章中,我们将转向另一种类型的数据——文本。分析文本数据涉及独特的挑战和方法,这些方法与用于数字时间序列的数据分析不同。我们将深入探讨文本预处理,涵盖文本清理技术、分词策略和拼写修正方法,这些对于任何自然语言处理(NLP)任务都是至关重要的。
第三部分:下游数据清洗——消费非结构化数据
本部分聚焦于处理非结构化数据(如文本、图像和音频)时面临的挑战和技术,特别是在现代机器学习环境下,尤其是大型语言模型(LLMs)。它全面概述了如何为机器学习应用准备非结构化数据类型,确保数据经过适当预处理以便分析和模型训练。各章节涵盖了文本、图像和音频数据的基本预处理方法,为读者提供了在当今由 AI 驱动的环境中处理更复杂和多样化数据集的工具。
本部分包含以下章节:
-
第十二章*,LLMs 时代的文本预处理*
-
第十三章*,使用 LLMs 进行图像和音频预处理*