数据预处理|11 数据清理第三级--缺失值、离群值和错误
在第一级中,我们清理了表格,而没有注意到数据结构或记录的数值。在第二级中,我们的注意力是要有一个能支持我们分析目标的数据结构,但我们仍然没有太注意记录值的正确性或适当性。这就是第三层次的数据清理的目标。
在第三层次的数据清理中,我们将重点关注记录的数值,并将采取措施确保关于数据中记录的数值的三个事项得到解决。首先,我们将确保数据中的缺失值已经被发现,我们知道为什么会发生这种情况,并且已经采取了适当的措施来解决这些问题。第二,我们将确保我们已经采取了适当的措施,使记录的数值是正确的。第三,我们将确定数据中的极端点已经被发现,并且已经采取了适当的措施来解决这些问题。
第三级数据清理在与数据分析目标和工具的关系上与第二级类似。虽然一级数据清理可以在不关注数据分析目标和工具的情况下孤立地进行,但二级和三级数据清理必须在我们了解分析目标和工具的情况下进行。在上一章的例子1、2、3中,我们体验了如何针对分析工具进行II级数据清理。本章的例子也将与分析情况有很好的联系。
在这一章中,我们将介绍以下主要内容。
- 缺失值
- 异常值
- 误差
1 缺少的值
缺失值,顾名思义,是指我们期望得到但没有得到的数值。最简单地说,缺失值是指我们想用于分析目标的数据集中的空单元。例如,下面的截图显示了一个有缺失值的数据集的例子--第一个和第三个学生的平均成绩(GPA)缺失,第五个学生的身高缺失,第六个学生的性格类型缺失。

在Python中,缺失的值不是用空来表示的--它们是通过NaN来表示的,NaN是Not a Number的缩写。虽然Not a Number的字面意思并没有完全涵盖我们有缺失值的所有可能情况,但在Python中,只要我们有缺失值,就会使用NaN。
下面的截图显示了一个pandas DataFrame,它已经读取并呈现了图11.1中的表格。对比这两张截图后,你会发现图11.1中每一个空的单元格在图11.2中都是NaN。

我们现在知道什么是缺失值,以及它们在我们选择的分析环境--Python中是如何呈现的。不幸的是,缺失值并不总是以一种标准的方式呈现;例如,在pandas DataFrame上有NaN是呈现缺失值的一种标准方式。然而,不了解情况的人可能会用一些内部协议来呈现缺失值,如MV、None、99999和N/A。如果缺失值没有以标准的方式呈现,那么处理它们的第一步就是要纠正这一点。在这种情况下,我们会检测数据集作者的意思是缺失值,然后用np.nan来代替它们。
即使缺失值是以标准方式呈现的,检测它们有时可能就像用眼睛看数据集一样容易。当数据集很大的时候,我们不能依靠眼看数据来检测和理解缺失值。接下来,我们将把注意力转向如何检测缺失值,特别是对于较大的数据集。
1.1 检测缺失值
每个Pandas DataFrame都有两个函数,在检测哪些属性有缺失值以及有多少缺失值方面非常有用。.info()和.isna()。
下面的例子显示了如何使用这些函数来检测一个数据集是否有缺失值,以及有多少值被缺失。
1.2 检测缺失值的例子
Airdata.csv空气质量数据集包括2020年三个地点的每小时记录。该数据集--除了A、B、C三个地点的二氧化氮读数外--还有日期时间、温度、湿度、风速和风向读数。下面的截图显示了将文件读入air_df DataFrame的代码,并显示了数据集的第一和最后几行。
import pandas as pd
import matplotlib.pyplot as plt
air_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter11/Airdata.csv')
air_df.head()

我们可以使用的第一个方法是使用.info()函数来检测数据的任何一列是否有任何缺失值。下面的截图展示了这个函数在air_df上的应用。
air_df.info()

正如你在前面的截图中看到的,air_df有8784行(条目)数据,但NO2_Location_A、NO2_Location_B和NO2_Location_C列的非空值较少,这意味着这些属性有缺失值。 第二个方法是使用Pandas Series的.isan()函数来计算哪些属性有缺失值。Pandas DataFrames和Pandas Series都有.isan()函数,它输出相同的数据结构,所有的单元格都用布尔值填充,表示该单元格是否为NaN。下面的截图使用.isan()函数来计算air_df的每个属性中的NaN条目数。
print('Number of Missing Values: ')
for col in air_df.columns:
n_MV = sum(air_df[col].isna())
print('{}:{}'.format(col, n_MV))

air_df.isna().sum()

在前面的截图中,我们看到这三个地方的二氧化氮读数都有缺失值。这只是证实了我们在图11.4中使用.info()函数进行的缺失值检测。
现在我们知道了如何检测缺失值,让我们把注意力转移到理解是什么导致了这些值的缺失。在我们处理缺失值的过程中,我们首先需要知道为什么会发生这种情况。在下一个分章中,我们将重点讨论哪些情况会导致缺失值。
1.3 造成缺失值的原因
为什么会出现缺失值,可能有各种各样的原因。正如我们在本章中所看到的那样,了解一个数值缺失的原因是使我们能够有效处理缺失数值的最重要信息。下面列出了可能出现缺失值的最常见原因。
- 人为错误。
- 受访者可能会拒绝回答某个调查问题。
- 接受调查的人不理解这个问题。
- 提供的数值是一个明显的错误,所以被删除。
- 没有足够的时间来回答问题。
- 由于缺乏有效的数据库管理而丢失记录。
- 故意删除和跳过数据收集(可能有欺诈的意图)。
- 参与者在研究过程中退出。
- 第三方篡改或阻挠数据收集。
- 错过的观察。
- 传感器故障。
- 程序错误。
作为数据分析师,在处理数据时,有时你所拥有的只是数据,你没有任何人可以向你提出关于数据的问题。因此,重要的是对数据要有好奇心,想象一下缺失值背后的原因。当你要猜测是什么原因造成缺失值时,把前面的清单记在脑子里并理解这些原因会对你有好处。
不言而喻,如果你能接触到了解数据的人,关于找出缺失值的原因,最好的办法就是询问信息提供者。 不管是什么原因造成的缺失值,从数据分析的角度来看,我们可以将所有的缺失值归为三种类型。了解这些类型对于决定如何处理缺失值将是非常重要的。
1.4 缺失值的类型
一个属性中的一个或一组缺失值可能属于以下三种类型之一:完全随机缺失(MCAR),随机缺失(MAR),以及非随机缺失(MNAR)。这些类型的缺失值之间存在着一种顺序关系。从MCAR到MNAR,缺失值变得更有问题,更难处理。
missing completely at random (MCAR), missing at random (MAR), and missing not at random (MNAR).
当我们没有任何理由相信这些值是由于任何系统性的原因而缺失的时候,就会使用MCAR。当一个缺失值被归类为MCAR时,有缺失值的数据对象可能是任何一个数据对象。例如,如果一个空气质量传感器由于互联网连接的随机波动而无法与其服务器通信以保存记录,那么缺失的值就属于MCAR类型。这是因为互联网连接问题可能发生在任何一个数据对象上,但它只是碰巧发生在它所发生的对象上。
另一方面,当数据中的某些数据对象更有可能出现缺失值时,我们就有MAR。例如,如果高风速有时会导致传感器发生故障,使其无法给出读数,那么在高风中发生的缺失值就被归类为MAR。理解MAR的关键是,导致出现缺失值的系统性原因并不总是导致缺失值,而是增加了数据对象出现缺失值的倾向。
最后,MNAR发生在我们确切知道哪个数据对象会有缺失值的时候。
例如,如果一个倾向于排放过多空气污染物的发电厂为了避免向政府支付罚款而篡改了传感器,那么由于这种情况而没有收集的数据对象将被归类为MNAR。MNAR缺失值是最有问题的,弄清它们发生的原因并阻止它们发生往往是数据分析项目的首要任务。
接下来,我们将学习如何使用数据分析工具来诊断缺失值的类型。在下一节中,我们将看到一个例子,展示了三种类型的缺失值。
1.5 对缺失值的诊断
一个有缺失值的属性实际上有两个变量的信息:它本身和一个隐藏属性。隐藏属性是一个二进制属性,当有缺失值时,其值为1,否则为0。为了弄清缺失值的类型(MCAR、MAR和MNAR),我们需要做的就是调查有缺失值的属性的隐藏二元变量与数据集中的其他属性之间是否存在关系。下面的列表显示了我们期望看到的基于每一种缺失值类型的关系种类。
- MCAR。我们不期望隐藏的二元变量与其他属性有有意义的关系。
- MAR:我们希望隐藏的二元变量与其他属性中至少有一个有意义的关系。
- MNAR。我们希望隐藏的二元变量和至少一个其他属性之间有很强的关系。
下面的小节展示了三种具有不同类型缺失值的情况,我们将使用我们的数据分析工具包来帮助我们诊断它们。
我们将继续使用我们之前看到的air_df数据集。我们看到,NO2_Location_A、NO2_Location_B和NO2_Location_C分别有120、560和176个缺失值。我们将逐一解决每一列下缺失值的诊断问题。
1.5.1 诊断NO2_Location_A的缺失值
为了诊断缺失值的类型,有两种方法供我们使用:视觉和统计方法。这些诊断方法必须针对数据集中的所有属性运行。数据中有四个数字属性。温度、湿度、风向和风速。数据中还有一个DateTime属性,可以解包为四个分类属性:月、日、小时和工作日。我们需要运行分析的方式对于数字属性和分类属性来说是不同的。因此,首先,我们将学习数字属性,然后我们将把注意力转向分类属性。
让我们从 "温度 "数字属性开始。另外,我们先从视觉上做诊断,然后再从统计学上做诊断。
基于温度的缺失值诊断
视觉诊断是通过比较两个种群的温度值来完成的:第一,NO2_Location_A有缺失值的数据对象,第二,NO2_Location_A无缺失值的数据对象。在第5章 "数据可视化 "中,在比较种群下,我们学习了如何使用数据可视化来比较种群。这里,我们将使用这些技术。我们可以使用boxplot或者直方图来做这件事。让我们同时使用这两种方法--首先是boxplot,然后是histogram。
下面的截图显示了代码和比较两个种群的boxplot。该代码与我们在第五章 "数据可视化 "中学到的非常相似,所以我们只讨论可视化的意义。
import matplotlib.pyplot as plt
import seaborn as sns
BM_MV = air_df.NO2_Location_A.isna()
MV_labels = ['With Missing Values','Without Missing Values']
box_sr = pd.Series('',index=BM_MV.unique())
for poss in BM_MV.unique():
BM = BM_MV == poss
box_sr[poss] = air_df[BM].Temperature
plt.boxplot(box_sr,vert=False)
plt.yticks([1,2],MV_labels)
plt.show()

观察前面的截图中的图表,我们可以看到温度的值在两个人群中没有意义的变化。这表明温度的变化不可能导致或影响NO2_Location_A下缺失值的发生。
我们也可以用柱状图来做这个分析。这在第五章 "数据可视化 "中的 "种群比较 "中也有展示。下面的截图显示了创建直方图和比较两个种群的代码。
BM_MV = air_df.NO2_Location_A.isna()
temp_range = (air_df.Temperature.min(),air_df.Temperature.max())
MV_labels = ['With Missing Values','Without Missing Values']
plt.figure(figsize=(10,4))
plt.style.use('ggplot')
for i,poss in enumerate(BM_MV.unique()):
plt.subplot(1,2,i+1)
BM = BM_MV == poss
air_df[BM].Temperature.hist()
plt.xlim = temp_range
plt.title(MV_labels[i])
plt.show()

前面的截图证实了我们在使用boxplots时得出的相同结论。由于我们没有看到两个群体之间的明显差异,我们得出结论,"温度 "的值不可能影响或导致缺失值的发生。
最后,我们还想用一种统计方法来确认:双样本t检验。
双样本t检验评估一个数字属性的值在两组中是否有显著的不同。这里的两组是在NO2_Location_A下有缺失值的数据对象和在NO2_Location_A下没有缺失值的数据对象。
简而言之,双样本t检验假设两组之间的属性值没有显著差异,然后计算如果假设正确的话,数据变成这样的概率。这个概率被称为P值。
因此,如果p值非常小,我们就有有意义的证据来怀疑双样本t检验的假设可能是错误的。
我们可以用Python轻松地进行任何假设检验。下面的截图使用scipy.stats模块中的ttest_ind函数来做双样本t检验。
from scipy.stats import ttest_ind
BM_MV = air_df.NO2_Location_A.isna()
ttest_ind(air_df[BM_MV].Temperature,air_df[~BM_MV].Temperature)
'''
Ttest_indResult(statistic=0.05646499065315542, pvalue=0.9549726689684548)
'''
正如你在前面的截图中所看到的,要使用ttest_ind()函数,我们所需要做的就是传递两组数字。
t检验的p值非常大0.95(满分1),这意味着我们没有任何理由怀疑两组之间的温度值会有意义的不同。这个结论证实了我们用boxplots和柱状图得出的结论。
在这里,我们展示了仅基于一个数字属性的诊断缺失值的代码。其余数字属性的代码和分析是类似的。现在你知道了如何对一个数字属性进行分析,接下来我们将创建一个代码,输出我们使用数字属性进行缺失值诊断所需要的所有内容。
根据所有数字属性诊断缺失值
为了对缺失值做一个完整的诊断,需要对所有的属性做一个类似于我们对温度属性所做的分析。虽然分析的每一部分都很容易理解和解释,但由于诊断分析有很多部分,这就要求我们用非常有组织的方式进行编码和分析。
为了有条不紊地做到这一点,我们将首先创建一个函数,执行我们显示的可以对温度进行的所有三种分析。除了数据集之外,该函数还接受我们要进行分析的数字属性的名称和布尔掩码,对于有缺失值的数据对象,该掩码为真,对于没有缺失值的数据对象,该掩码为假。该函数输出boxplots、柱状图和输入属性的t检验的p值。下面的截图中的代码显示了这个函数是如何创建的。该代码相当长,如果你想复制它,请在本书专用的GitHub仓库中的第11章数据清理第三级--缺失值、异常值和错误文件夹中找到它。
from scipy.stats import ttest_ind
def Diagnose_MV_Numerical(df,str_att_name,BM_MV):
MV_labels = {True:'With Missing Values',False:'Without Missing Values'}
labels=[]
box_sr = pd.Series('',index = BM_MV.unique())
for poss in BM_MV.unique():
BM = BM_MV == poss
box_sr[poss] = df[BM][str_att_name].dropna()
labels.append(MV_labels[poss])
plt.boxplot(box_sr,vert=False)
plt.yticks([1,2],labels)
plt.xlabel(str_att_name)
plt.show()
plt.figure(figsize=(10,4))
att_range = (df[str_att_name].min(),df[str_att_name].max())
for i,poss in enumerate(BM_MV.unique()):
plt.subplot(1,2,i+1)
BM = BM_MV == poss
df[BM][str_att_name].hist()
plt.xlim = att_range
plt.xlabel(str_att_name)
plt.title(MV_labels[poss])
plt.show()
group_1_data = df[BM_MV][str_att_name].dropna()
group_2_data = df[~BM_MV][str_att_name].dropna()
p_value = ttest_ind(group_1_data,group_2_data).pvalue
print('p-value of t-test: {}'.format(p_value))
简单地说,前面的代码是图11所示代码的参数化和组合版本。运行前面的代码,即创建一个Diagnose_MV_Numerical()函数后,运行下面的代码将对数据中的所有数字属性运行这个函数,它允许你调查NO2_Location_A的缺失值是否由于任何与数据集中的数字属性有关的系统性原因而发生。
numerical_attributes = ['Temperature', 'Humidity', 'Wind_Speed', 'Wind_Direction']
BM_MV = air_df.NO2_Location_A.isna()
for att in numerical_attributes:
print('Diagnosis Analysis of Missing Values for {}:'.format(att))
Diagnose_MV_Numerical(air_df,att,BM_MV)
print('- - - - - - - - - - - - divider - - - - - - - - - - - ')

运行前面的代码将产生四个诊断报告,每个数字属性一个。每份报告有三个部分:使用图表的诊断,使用直方图的诊断,以及使用t检验的诊断。研究前面代码片断的后续报告可以看出,NO2_Location_A下的缺失值的趋势并没有根据数据中任何一个数字属性的值而改变。
接下来,我们将对分类属性做一个类似的编码和分析。就像我们对数字属性所做的那样,让我们先对一个属性进行诊断,然后我们将创建代码,可以一次性输出我们需要的所有分析。我们要做诊断的第一个属性是工作日。
基于工作日的缺失值诊断
你可能会感到困惑,air_df数据集没有一个名为工作日的分类属性,你是对的,但解开air_df.DataTime属性可以得到以下属性:工作日、日、月和小时。
如果你认为这听起来像是二级数据清理,你是完全正确的。为了能够更有效地进行第三级数据清理,我们需要先进行一些第二级数据清理。下面的代码执行了所述的第二级数据清理。
air_df.DateTime = pd.to_datetime(air_df.DateTime)
air_df['month'] = air_df.DateTime.dt.month
air_df['day'] = air_df.DateTime.dt.day
air_df['hour'] = air_df.DateTime.dt.hour
air_df['weekday'] = air_df.DateTime.dt.day_name()
air_df.head()

运行前面的代码后,在继续阅读之前,请检查air_df的新状态,并研究被添加到它的新列。你会看到,月、日、小时和工作日的分类属性被解包为它们自己的属性。
现在,这个数据清理二级已经完成,我们可以根据工作日的分类属性,对air_df.NO2_Location_A列的缺失值进行诊断。
正如我们在第五章 "数据可视化 "中所看到的,柱状图是一种数据可视化技术,可以根据分类属性来比较人群。下面的截图显示了我们在第5章 "数据可视化 "中所学到的,在使用条形图比较人群的例子中,对这种情况的第一种方式进行了修改。
BM_MV = air_df.NO2_Location_A.isna()
MV_labels = ['Without Missing Values','With Missing Values']
plt.figure(figsize=(10,4))
for i,poss in enumerate(BM_MV.unique()):
plt.subplot(1,2,i+1)
BM = BM_MV == poss
air_df[BM].weekday.value_counts().plot.bar()
plt.title(MV_labels[i])
plt.show()

观察前面的截图,我们可以看到缺失值可能是随机发生的,我们没有一个有意义的趋势来相信缺失值的发生有一个系统性的原因,是由于airt_df.weekday的值的变化。
我们也可以用独立度的卡方检验统计测试来做类似的诊断。
简而言之,对于这种情况,该检验假设缺失值的发生与工作日属性之间没有关系。基于这一假设,该检验计算出一个p值,即如果假设为真,我们所拥有的数据发生的概率。利用这个p值,我们可以决定我们是否有任何证据来怀疑缺失值的系统原因。
什么是P值?这是我们在本章中第二次看到P值的概念。
在所有的统计测试中,P值是同一个概念,它具有相同的意义。每个统计检验都会假设一些东西(这被称为空假设),而p值是根据这个假设和观察结果(数据)计算出来的。p值是指如果无效假设为真,已经发生的数据的概率。
使用p值的一个流行经验法则是采用著名的5%的显著性水平。0.05的显著性水平表示,如果p值结果大于0.05,那么我们就没有任何证据来怀疑无效假设不正确。虽然这是一个相当好的经验法则,但最好是了解p值,然后用数据可视化来补充统计测试。
下面的截图显示了使用scipy.stats中的chi2_contingency()进行的独立性的卡方检验。代码首先使用pd.crosstab()来创建一个或然率表,它是一个可视化工具,用来研究两个分类属性之间的关系(这在第5章数据可视化中的可视化两个分类属性之间的关系部分有介绍)。然后,代码将contigency_table传递给chi2_contingency()函数来进行测试。该测试输出一些数值,但不是所有的数值都对我们有用。p值是第二个值,是0.4127。
from scipy.stats import chi2_contingency
BM_MV = air_df.NO2_Location_A.isna()
contigency_table = pd.crosstab(BM_MV,air_df.weekday)
contigency_table

chi2_contingency(contigency_table)
'''
(6.048964133655503,
0.41772751510388023,
6,
array([[1230.95081967, 1230.95081967, 1230.95081967, 1230.95081967,
1254.62295082, 1230.95081967, 1254.62295082],
[ 17.04918033, 17.04918033, 17.04918033, 17.04918033,
17.37704918, 17.04918033, 17.37704918]]))
'''
拥有0.4127的p值证实了我们在图11.10下的观察,即air_df.NO2_Location_A中缺失值的发生与工作日的值之间没有关系,缺失值发生的方式可能只是一个随机的机会。
在这里,我们展示了只基于一个分类属性的诊断缺失值的代码。其余分类属性的代码和分析是类似的。现在你知道了如何对一个数字属性进行诊断,接下来我们将创建一个代码,输出我们使用分类属性进行缺失值诊断所需要的所有信息。
根据所有的分类属性来诊断缺失值
为了对缺失值做一个完整的诊断,需要对所有其他的分类属性做一个类似于我们对Weekday属性所做的分析。为了有条不紊地完成这项工作,我们将首先创建一个函数,执行我们所展示的对工作日进行的两项分析。除了数据集之外,该函数还接收我们想要进行分析的分类属性的名称和布尔掩码,对于有缺失值的数据对象,掩码为真,对于没有缺失值的数据对象,掩码为假。该函数输出条形图,以及输入属性的独立度卡方检验的p值。下面的代码片断显示了这个函数是如何创建的。
from scipy.stats import chi2_contingency
def Diagnose_MV_Categorical(df,str_att_name,BM_MV):
MV_labels = {True:'With Missing Values',False:'Without Missing Values'}
plt.figure(figsize=(10,4))
for i,poss in enumerate(BM_MV.unique()):
plt.subplot(1,2,i+1)
BM = BM_MV == poss
df[BM][str_att_name].value_counts().plot.bar()
plt.title(MV_labels[poss])
plt.show()
contigency_table = pd.crosstab(BM_MV,df[str_att_name])
p_value = chi2_contingency(contigency_table)[1]
print('p-value of Chi_squared test: {}'.format(p_value))
运行前面的代码,即创建一个Diagnose_MV_Categorical()函数后,运行下面的代码将对数据中的所有分类属性运行这个函数,它允许你调查NO2_Location_A的缺失值是否是由于任何与数据集中的分类属性有关的系统原因而发生的。
categorical_attributes = ['month', 'day','hour', 'weekday']
BM_MV = air_df.NO2_Location_A.isna()
for att in categorical_attributes:
print('Diagnosis Analysis of Missing Values for {}:'.format(att))
Diagnose_MV_Categorical(air_df,att,BM_MV)
print('- - - - - - - - - - - - divider - - - - - - - - - - - ')

当你运行前面的代码时,它将产生四个诊断报告,每个分类属性一个报告。每个报告都有两部分,如下所示。
- 使用条形图进行诊断
- 使用独立的卡方检验进行诊断 研究这些报告表明,
NO2_Location_A下的缺失值的趋势并没有根据数据中任何一个分类属性的值而改变。
结合我们在本章前面对数字属性的学习和刚才对分类属性的学习,我们确实看到数据中的任何一个属性--即温度、湿度、风速、风向、工作日、日、月和小时--都不可能影响缺失值的趋势。
根据我们对缺失值进行的所有诊断,我们得出结论,NO2_Location_A的缺失值属于MCAR类型。
现在我们已经能够确定NO2_Location_A的缺失值,让我们也对NO2_Location_B和NO2_Location_C的缺失值运行我们迄今为止学到的诊断。我们将在下面两个小节中这样做。
1.5.2 诊断NO2_Location_B的缺失值
为了诊断NO2_Location_B的缺失值,我们需要做与NO2_Location_A完全相同的分析。编码部分非常容易,因为我们已经做了这些,大部分都是这样。下面的代码使用我们已经创建的Diagnose_MV_Numerical()和Diagnose_MV_Categorical()函数来运行所有需要的诊断,以便找出NO2_Location_B下发生的缺失值类型。
categorical_attributes = ['month', 'day','hour', 'weekday']
numerical_attributes = ['Temperature', 'Humidity', 'Wind_Speed', 'Wind_Direction']
BM_MV = air_df.NO2_Location_B.isna()
for att in numerical_attributes:
print('Diagnosis Analysis of Missing Values for {}:'.format(att))
Diagnose_MV_Numerical(air_df,att,BM_MV)
print('- - - - - - - - - - - - divider - - - - - - - - - - - ')
for att in categorical_attributes:
print('Diagnosis Analysis of Missing Values for {}:'.format(att))
Diagnose_MV_Categorical(air_df,att,BM_MV)
print('- - - - - - - - - - - - divider - - - - - - - - - - - ')

当你运行前面的代码时,会产生一个长报告,调查缺失值的发生趋势是否受到任何分类或数字属性值的影响。
在研究了该报告之后,你可以看到有几个属性似乎与缺失值的发生有意义的关系。这些属性是温度、风速、风向和月份。下面的截图显示了对风速的诊断分析,它与缺失值的关系最强。

在前面的截图中,你可以看到所有三个分析工具都显示,在NO2_Location_B下有缺失值的数据对象和没有缺失值的数据对象之间,风速的值存在着明显的差异。简而言之,较高的Wind_Speed值往往会增加NO2_Location_B有缺失值的机会。
在这次诊断之后,将结果与卖给我们空气质量传感器的公司分享。以下是发给该公司的电子邮件。
亲爱的先生/女士,我写这封邮件是为了与您分享我们从您那里购买的序列号为231703612的电化学传感器似乎出现了故障模式。当温度较低,而风速较高时,传感器似乎会跳过记录。我们想让你知道,如果你能告诉我们你对这种模式的看法,我们将不胜感激。
真诚的,Iman Ahmadian

几天后,我们收到以下电子邮件。
尊敬的分析团队,感谢您分享您的关注和有关电化学传感器问题的信息。
你与我们分享的内容与我们最近的发现是一致的。我们已经了解到,你所列出的传感器的型号在大风条件下往往会出现故障。
对于未来的案例,你会期望序列号以2317开头的传感器遇到类似的问题。
我们对这种不便表示诚挚的歉意,并将非常乐意为您提供50%的折扣,购买我们没有这种故障的全新传感器。如果您希望使用这一折扣,请引用这封电子邮件与我们的销售部门联系。
最好的祝愿 尼玛-加德利

我们有了--现在我们知道为什么NO2_Location_B下的一些缺失值会发生。我们知道,温度值会导致缺失值的增加,所以我们可以得出结论,NO2_Location_B下的缺失值是MAR类型的。
这里要问的一个好问题是,如果高风速值是造成缺失值的罪魁祸首,为什么缺失值也显示出与温度、风向和月份有意义的模式?原因是 "风速 "与 "温度"、"风向 "和 "月份 "有密切关系。利用你在第5章 "数据可视化 "中 "调查两个属性之间的关系 "一节中所学到的知识,将其纳入分析。由于这些强烈的关系,看起来好像其他属性也会影响缺失值的趋势。我们从与传感器制造商的沟通中得知,情况并非如此。
到目前为止,我们已经能够诊断出NO2_Location_A和NO2_Location_B下的缺失值。接下来,我们将对NO2_Location_C进行诊断。
1.5.3诊断NO2_Location_C的缺失值
我们只需要改变NO2_Location_B中诊断缺失值的代码中的一行,这样我们就可以诊断出NO2_Location_C中的缺失值。你需要将第三行代码从BM_MV = air_df.NO2_Location_B.isna()改为BM_MV = air_df.NO2_Location_C.isna()。一旦这个变化被应用,代码被运行,你将得到一个基于数据中所有分类和数字属性的诊断报告。在继续阅读之前,请尝试浏览并解释诊断报告。
诊断报告显示了缺失值的趋势与大多数属性之间的关系--即温度、湿度、风速、日、月、小时和工作日。然而,与工作日属性的关系是最强的。下面的截图显示了一个基于工作日的缺失值诊断。
截图中的柱状图显示,缺失值只发生在星期六。卡方检验的P值非常小。

基于小时和日的诊断也显示出有意义的模式(这里没有打印小时和日属性的诊断报告,但请看你刚刚创建的报告)。只有当小时属性的值为10、11、12、13、14、15、16、17、18、19和20时,或者当日属性的值为25、26、27、28和29时,缺失值才同样发生。从这些报告中,我们可以推断出,缺失值可预测地发生在每个月的最后一个星期六,从上午10点到晚上8点。
这就是我们在数据中看到的模式,但为什么呢?
在让C地的地方当局知道后,发现C地电厂的一群员工一直在利用电厂的资源从事各种加密货币的挖掘。这种滥用资源的行为只发生在每月的最后一个星期六,因为该电厂有一整天的时间进行定期和预防性维护。由于这群员工为了掩盖他们的行踪和避免被抓,他们决定篡改为调节电厂空气污染而设置的传感器。他们不知道,篡改数据收集会在数据集上留下痕迹,而这种痕迹不容易从像你这样的高质量数据分析师的眼中隐藏。
这最后一条信息和诊断结果使我们得出结论:NO2_Location_C的缺失值是一个MNAR值。这样的值被遗漏是由于一个直接的原因,即为什么首先要收集数据。很多时候,当一个数据集有大量的MNAR缺失值时,这个数据集就会变得毫无价值,无法起到有意义的分析作用。处理MNAR缺失值的第一步是防止它们再次发生。
在学习了如何检测和诊断缺失值之后,现在是讨论处理缺失值的最佳时机。让我们直接开始吧。
1.6 处理缺失值的问题
如以下列表所示,有四种不同的方法来处理缺失值。
- 保持原样。
- 删除有缺失值的数据对象(行)。
- 删除有缺失值的属性(列)。
- 估算和归纳一个值。
在不同的情况下,前面的每种策略都可能是最好的策略。
不管怎么样,在处理缺失值时,我们有以下两个目标。
- 尽可能多地保留数据和信息
- 在我们的分析中引入尽可能少的偏差
同时实现这两个目标并不总是可能的,往往需要取得一种平衡。为了在处理缺失值时有效地找到这种平衡,我们需要了解并考虑以下项目。
- 我们的分析目标
- 我们的分析工具
- 造成缺失值的原因
- 缺失值的类型(MCAR,MAR,MNAR)
在大多数情况下,如果对前面的项目有足够的了解,处理缺失值的最佳行动方案就会展现在你面前。在下面的小节中,我们将首先介绍处理缺失值的四种方法中的每一种,然后我们将通过一些例子把我们所学到的知识付诸实践。
1.6.1 第一种方法--保持缺失值的原样
如标题所示,这种方法将缺失值作为缺失值保留,并进入下一阶段的数据预处理。在以下两种情况下,这种方法是处理缺失值的最佳方式。
首先,在你将与他人分享这些数据,而你不一定是要使用这些数据进行分析的人的情况下,你会使用这种策略。通过这种方式,你将允许他们根据自己的分析需要来决定如何处理缺失值。
第二,如果你的数据分析目标和将要使用的数据分析工具都能无缝处理缺失值,保持原样是最好的方法。例如,我们在第7章 "分类 "中了解到的K-Nearest Neighbors(KNN)算法,可以被调整来处理缺失值,而不必删除任何数据对象。正如你所记得的,KNN计算数据对象之间的距离来寻找最近的邻居。
因此,每次在计算一个有缺失值的数据对象和其他数据对象之间的距离时,都会为缺失值假设一个值。假设值的选择方式是,假设值不会有帮助,所以具有缺失值的数据对象将被选中。换句话说,只有当一个有缺失值的数据对象的非缺失值显示出非常高的相似度,抵消了缺失值的假设值的负面作用时,它才会被选为最近的邻居之一。
1.6.2 第二种方法--删除有缺失值的数据对象
这种方法的选择必须非常谨慎,因为它可能会违背成功处理缺失值的两个目标:不向数据集引入偏见,不从数据中删除有价值的信息。例如,当数据集中的缺失值属于MNAR或MAR类型时,我们应该避免删除缺失值的数据对象。这是因为这样做意味着你正在删除数据集中有意义的不同部分的人口。
即使缺失值是MCAR类型的,我们也应该首先尝试找到其他处理缺失值的方法,然后再转向删除数据对象。当没有其他方法来处理缺失值时,从数据集中删除数据对象应该被视为最后的手段。
1.6.3 第三种方法--删除缺失值的属性
当一个数据集中的大部分缺失值都来自于一两个属性时,我们可以考虑删除这些属性作为处理缺失值的一种方式。当然,如果该属性是一个关键属性,没有它你就无法进行项目,那么面对关键属性中太多的缺失值就意味着项目无法进行。但是,如果这些属性对项目来说不是绝对必要的,那么删除缺失值太多的属性可能是正确的做法。
当一个属性的缺失值数量足够多时(大概超过25%),估计和输入缺失值就变得毫无意义,放任该属性比估计缺失值要好。
1.6.4 第四种方法--估计和归纳缺失值
在这种方法中,我们将利用我们的知识、理解和分析工具来填补缺失的价值。术语imputing抓住了这样做对数据集的本质--我们用数值代替缺失值,同时知道这可能会导致我们的分析出现偏差。
如果缺失值是MCAR或MAR类型的,而我们选择的分析方法不能处理缺失值的数据集,那么归纳缺失值可能是最好的方法。
有四种一般的方法来估计缺失值的替代。下面的清单概述了这些方法。
- 用一般的中心趋势(平均数、中位数或模式)进行估算。这对MCAR缺失值来说是比较好的。
- 用与缺失值更相关的一组数据的中心趋势来替代。这对MAR缺失值来说比较好。
- 回归分析。不太理想,但如果我们必须对有MNAR缺失值的数据集进行处理,这种方法对这样的数据集比较好。
- 内插法。当数据集是一个时间序列数据集,并且缺失值是MCAR类型的。
关于估计和估算过程的一个常见的误解是,我们要用最准确的替代物来估算缺失值。这是不正确的。
归因时,我们的目的不是要最好地预测缺失值,而是要用对我们的分析产生最少偏差的值来归因。例如,对于聚类分析来说,如果一个数据集有MCAR缺失值,那么用整个人口的中心趋势进行估算是最好的方法。原因是中心趋势值在对数据对象进行分组的过程中会起到中立投票的作用,如果有缺失值的数据对象被推到了一个聚类中,这并不是由于估算值的原因。
现在我们已经有机会了解处理缺失值的不同方法,让我们把事情放在一起,看看在选择正确策略时的逐步决策过程。
1.6.5 在处理缺失值时选择正确的方法
下图总结了我们到目前为止在处理缺失值方面所讨论的内容。该图显示,在处理缺失值时,必须从四个方面选择正确的方法:分析目标、分析工具、缺失值的原因和缺失值的类型(MCAR、MAR、MNAR)。

1.6.6 例子1
使用air_df,我们在本章前面检测并诊断了其缺失值,我们想画一个柱状图,显示地点A的每小时平均二氧化氮值。
如果你还记得,air_df.NO2_Location_A中的缺失值属于MCAR缺失值类型。由于缺失值不属于MNAR类型,而且柱状图可以很容易地处理缺失值,所以我们选择的处理缺失值的策略将是保持其原样。下面的截图显示了代码和它所创建的柱状图。
air_df.groupby('hour').NO2_Location_A.mean().plot.bar()
plt.show()

在前面的截图中,你观察到.groupby()和.mean()函数能够处理缺失值。当数据被聚合且缺失值的数量不多时,数据的聚合处理缺失值,而不需要进行归集。事实上,.mean()函数忽略了有缺失值的属性的存在,并根据有值的数据对象计算出平均值。
1.6.7 例子2
使用air_df,我们在本章前面检测并诊断了其缺失值,我们想画一张线图,比较地点A每个月第一天的二氧化氮变化。
我们知道air_df.NO2_Location_A中的缺失值是MCAR类型的;但是,假设我们不知道线图是否能处理缺失值。因此,让我们试一试,看看保持原样的策略是否有效。下面的截图显示了我们需要的线段图,没有处理缺失值。
month_poss = air_df.month.unique()
hour_poss = air_df.hour.unique()
plt.figure(figsize=(15,4))
for mn in month_poss:
BM = (air_df.month == mn) & (air_df.day ==1)
wdf = air_df[BM]
plt.plot(wdf.NO2_Location_A.values,
label=mn)
plt.legend(ncol=6)
plt.xticks(hour_poss)
plt.show()

在前面的截图中,我们看到,由于存在缺失值,线图在中间被切断。如果该图符合我们的分析需要,那么我们就完成了,不需要再做什么。但是,如果我们想处理缺失值并消除线图中的空点,我们就需要使用插值,因为缺失值是MCAR类型的,而且数据是时间序列数据。下面的代码片断显示了如何处理缺失值,然后绘制完整的线图。
NO2_Location_A_noMV = air_df.NO2_Location_A.interpolate(method='linear')
month_poss = air_df.month.unique()
hour_poss = air_df.hour.unique()
plt.figure(figsize=(15,4))
for mn in month_poss:
BM = (air_df.month == mn) & (air_df.day ==1)
plt.plot(NO2_Location_A_noMV[BM].values,
label=mn)
plt.legend(ncol=6)
plt.xticks(hour_poss)
plt.show()

前面的代码片断使用.interploate()函数来估算缺失值。当使用method='linear'时,该函数用其前后的数据点的平均值进行估算。在我们眼里,这将显得好像空点是用尺子连接起来的一样。
1.6.8 例子3
使用air_df,我们想画一个条形图,比较地点A和地点B的每小时平均二氧化氮值。
我们记得,air_df.NO2_Location_A中的缺失值是MCAR类型的,而air_df.NO2_Location_B中的缺失值是MAR类型的。由于这两个属性都没有MNAR缺失值,而且条形图可以处理缺失值,我们可以使用保持原样的策略。下面的截图显示了为这种情况创建条形图所需的代码。
air_df.groupby('hour')[['NO2_Location_A','NO2_Location_B','NO2_Location_C']].mean().plot.bar()
plt.show()

1.6.8 例子4
使用air_df,我们想画一个条形图,比较地点A、地点B和地点C的每小时平均二氧化氮值。
我们记得,在NO2_Location_A、NO2_Location_B和NO2_Location_C中,缺失值的类型分别为MCAR、MAR和MNAR。正如我们提到的,处理MCAR和MAR缺失值比处理MNAR缺失值要容易得多。对于MCAR和MAR,我们已经看到,我们可以使用保持原样的策略。
对于MNAR,我们需要回答这个问题。MNAR的缺失值是基本属性吗?回答这个问题需要对分析目标有一个深刻的理解。在两种不同的分析情况下,我们可能需要以不同的方式来处理缺失值。
在一个分析情境中,一个空气污染监管政府机构要求提供柱状图。在这种情况下,我们不能越过NO2_Location_C中的MNAR缺失值,我们需要拒绝他们的请求,而是将缺失值的存在告知监管机构,而不是发送他们所要求的东西。这是因为柱状图会产生误导,因为缺失值是由于数据被篡改,目的是为了淡化空气污染数据。
在另一种情况下,一个想调查不同地区一般空气污染的研究人员要求提供柱状图。在这种情况下,即使缺失值属于MNAR类型,但其背后的系统性原因对我们的分析目标并不重要。因此,我们可以对所有三栏采用保持原状的策略。创建柱状图与我们在图11.17中所做的非常相似。运行air_df.groupby('hour') [['NO2_Location_A', 'NO2_Location_B', 'NO2_Location_C']].mean()。plot.bar()将创建所要求的视觉效果。
1.6.9 例子5
我们想用kidney_disease.csv数据集来对慢性肾脏病(CKD)和非CKD的病例进行分类。该数据集显示了400名患者的数据,有5个独立的属性--即红细胞(rc)、血清肌酐(sc)、包装细胞容积(pcv)、比重(sg)和血红蛋白(hemo)。当然,该数据集也有一个名为诊断的从属属性,患者被标记为CKD或非CKD。决策树是我们想要使用的分类算法。
在我们对数据集的初步观察中,我们注意到数据集有缺失值,在使用我们在检测缺失值下学到的代码后,我们得出结论,rc、sc、pcv、sg和hemo的缺失值数量分别为131、17、71、47和52。
这意味着rc、sc、pcv、sg和hemo的缺失值的百分比分别为32.75%、4.25%、17.75%、11.75%和13%。
在继续阅读之前,利用你在本章中所学到的知识来确认前一段的信息。
patient_df = pd.read_csv('https://raw.githubusercontent.com/PacktPublishing/Hands-On-Data-Preprocessing-in-Python/main/Chapter11/kidney_disease.csv')
patient_df.head()

print('Number of missing values:')
for col in patient_df.columns:
n_MV = sum(patient_df[col].isna())
print('{}:{}'.format(col,n_MV))

当不同属性的缺失值数量较多(超过15%)时,可能大部分缺失值都发生在相同的数据对象上,这对我们的分析来说可能会有很大的问题。因此,在对每个属性的缺失值进行诊断之前,让我们使用seaborn模块中的heatmap()函数来可视化整个数据集的缺失值。下面的截图显示了代码和它产生的热图。
plt.figure(figsize=(4,7))
sns.heatmap(patient_df.isna())
plt.show()

前面截图中的热图显示,缺失值在一定程度上分散在各个数据对象中,当然也不是说不同属性下的缺失值只来自特定的数据对象。
接下来,我们把注意力转向每个属性的缺失值诊断。在执行了本章所学的内容后,我们可以得出结论,sc属性的缺失值属于MCAR类型,而rc、pcv、sg和hemo的缺失值属于MAR类型。所有MAR缺失值的趋势都与诊断依赖属性高度相关。
categorical_attributes = ['diagnosis']
numerical_attributes = ['sc','pcv','sg','hemo']
BM_MV = patient_df.rc.isna()
for att in numerical_attributes:
print('Diagnosis Analysis for {}:'.format(att))
Diagnose_MV_Numerical(patient_df,att,BM_MV)
print('- - - - - - - - - divider - - - - - - - - ')
for att in categorical_attributes:
print('Diagnosis Analysis for {}:'.format(att))
Diagnose_MV_Categorical(patient_df,att,BM_MV)
print('- - - - - - - - - divider - - - - - - - - - ')

categorical_attributes = ['diagnosis']
numerical_attributes = ['rc','sc','sg','hemo']
BM_MV = patient_df.pcv.isna()
for att in numerical_attributes:
print('Diagnosis Analysis for {}:'.format(att))
Diagnose_MV_Numerical(patient_df,att,BM_MV)
print('- - - - - - - - - divider - - - - - - - - ')
for att in categorical_attributes:
print('Diagnosis Analysis for {}:'.format(att))
Diagnose_MV_Categorical(patient_df,att,BM_MV)
print('- - - - - - - - - divider - - - - - - - - - ')

现在我们对缺失值的类型有了更好的了解,我们需要把注意力转向分析目标和工具的本质。我们想用决策树算法进行分类。当我们想要处理缺失值时,在算法中使用数据集之前,我们需要首先考虑算法如何使用数据,然后尝试选择一种策略,同时优化处理缺失值的两个目标。让我们来提醒自己处理缺失值的两个目标,如下所示。
- 尽可能多地保留数据和信息
- 在我们的分析中引入尽可能少的偏差
我们知道,决策树本身并不是为处理缺失值而设计的,我们所知道的决策树算法的工具--来自sklearn.tree模块的DecisionTreeClassifier()函数--如果输入数据有缺失值,会给出一个错误。知道了这一点,我们就知道保持原样的策略是不可行的。
我们也刚刚意识到,一些缺失值的趋势可以成为因果属性的预测因素。这一点很重要,因为如果我们要对缺失值进行归算,那就会从数据集中删除这一有价值的信息;有价值的信息是,一些属性的缺失值(MAR的)可以预测因果属性。因此,无论我们使用哪种归因方法,我们都将为每个有MAR缺失值的属性在数据集中添加一个二元属性,描述该属性是否有缺失值。这些新的二进制属性将被添加到分类任务的独立属性中,以预测诊断依赖属性。
下面的代码片段显示了这些二进制属性被添加到patient_df数据集中。
patient_df['rc_BMV'] = patient_df.rc.isna().astype(int)
patient_df['pcv_BMV'] = patient_df.pcv.isna().astype(int)
patient_df['sg_BMV'] = patient_df.sg.isna().astype(int)
patient_df['hemo_BMV'] = patient_df.hemo.isna().astype(int)
在继续阅读之前,先运行前面的几行代码,研究一下patient_df的状态。
现在让我们把注意力转向缺失值的归纳。如果你不记得决策树算法是如何完成分类任务的,请在继续阅读之前回到第7章,分类,以唤起你的记忆。决策树算法根据属性值将数据对象连续分成几组,当数据对象的值大于或小于属性的中心倾向时,它们更有可能被归入一个特定的标签。因此,通过用属性的中心趋势进行估算,我们不会给数据集引入偏见,所以估算的值不会导致分类器更频繁地预测一个标签而不是另一个。
因此,我们得出结论,用属性的中心趋势进行估算是解决缺失值的一种合理方式。我们现在需要回答的问题是。我们应该使用哪种中心趋势--中位数还是平均值?这个问题的答案是,如果属性没有很多离群值,那么平均数会更好。
在调查了有缺失值的属性的boxplots之后,你会发现sc有太多的离群值,而其余的属性都没有高度偏斜。因此,下面的代码片段显示了用patient_df.sc.median()对patient_df.sc的缺失值进行估算,并对其余有缺失值的属性用其平均值进行估算。
numerical_attributes = ['pcv','rc','sc','sg','hemo']
for att in numerical_attributes:
patient_df[att].plot.box(vert=False)
plt.show()

patient_df.sc.fillna(patient_df.sc.median(),inplace=True)
patient_df.fillna(patient_df.mean(),inplace=True)
前面的代码片段使用了.fillna()函数,这在估算缺失值时非常有用。运行前面的代码后,重新创建图11.18所示的热图,看看你的数据中缺失值的状况。
plt.figure(figsize=(4,7))
sns.heatmap(patient_df.isna())
plt.show()

吁! 检测、诊断和处理缺失值的工作现在已经完成了。现在,数据集已经为分类任务进行了预处理。我们需要做的就是使用我们在第7章 "分类 "中学到的代码来运行决策树算法。
下面的代码片段显示了第七章 "分类 "中针对这种分析情况所修改的代码。
from sklearn.tree import DecisionTreeClassifier, plot_tree
predictors = ['rc', 'sc', 'pcv', 'sg', 'hemo', 'rc_BMV', 'pcv_BMV', 'sg_BMV', 'hemo_BMV']
target = 'diagnosis'
Xs = patient_df[predictors]
y= patient_df[target]
classTree = DecisionTreeClassifier(min_impurity_decrease= 0.01, min_samples_split= 15)
classTree.fit(Xs, y)
前面的代码片段创建了一个决策树模型,并使用我们预处理的数据对其进行训练。请注意,min_impurity_decrease= 0.01和min_samples_split= 15是决策树算法的超参数,使用调整过程来调整。
下面的代码片段使用classTree训练的决策树模型,直观地画出它的树形,以供分析和使用。
from sklearn.tree import plot_tree
plt.figure(figsize=(15,15))
plot_tree(classTree,
feature_names=predictors,
class_names=y.unique(),
filled=True,
impurity=False)
plt.show()

现在我们可以使用前面的决策树来对来院的病人进行决策。
到目前为止,你在本章中已经取得了很大的进展。你现在已经能够从技术和分析的角度检测、诊断和处理缺失值了。在本章中,我们将讨论极端点和离群点的问题。
2 异常值
离群值,又称极端点,是指其数值与其他数据对象差异过大的数据对象。能够识别和处理它们,从以下三个角度来看是很重要的。
- 离群点可能是数据中的数据错误,应该被检测和删除。
- 不是错误的离群值会使对离群值存在敏感的分析工具的结果产生偏差。
- 离群值可能是欺诈性的条目。
我们将首先介绍我们可以用来检测异常值的工具,然后我们将介绍根据分析情况处理异常值的方法
2.1 检测异常值
我们用于检测离群值的工具取决于所涉及的属性数量。如果我们只对基于一个属性的异常值检测感兴趣,我们称之为单变量异常值检测;如果我们想基于两个属性检测,我们称之为双变量异常值检测;最后,如果我们想基于两个以上的属性检测异常值,我们称之为多变量异常值检测。我们将介绍我们可以用于上述每个类别的离群点检测的工具。我们还将介绍检测时间序列数据的异常值,因为在这方面有更好的工具。
2.1.1 单变量离群点检测
我们将用于单变量异常值检测的工具取决于属性的类型。对于数值属性,我们可以使用箱线图或[Q1-1.5*IQR, Q3+1.5*IQR]统计范围。异常值的概念对于单一的分类属性没有太大意义,但我们可以使用频率表或条形图等工具。
下面两个例子的特点是单变量离群点检测。在这些例子中,我们将使用 responses.csv 和 columns.csv 文件。这两个文件用于记录在斯洛伐克进行的一项调查的日期。
由于一级数据清理的原因,该数据集使用两个文件来保存记录--直观的和可编码的属性名称。columns.csv文件保留了可编码的属性名称及其完整的标题,而response.csv文件有一个数据对象(调查回应)表,其属性使用可编码的标题来命名。
下面的截图显示了将这两个文件读入Pandas DataFrames以及两个DataFrames的前两行。

检测一个数字属性的异常值的例子
在这个例子中,我们想检测response_df.Weight 数字属性中的异常值。我们有两种方法可以做到这一点;两种方法都会导致相同的结论。第一种方法是可视化的;我们将使用一个boxplot。下面的截图显示了为response_df.Weight创建一个boxplot的代码。

在下限之前和上限之后的圆圈代表数据中与其他数字在统计学上有太大差异的数据对象。这些圆圈在boxplot分析中被称为传单。
我们可以通过不同的方式来访问boxplot中作为传单的数据对象。 首先,我们可以从视觉上做到这一点。我们可以看到,传单的Weight值大于105,所以我们可以使用布尔掩码来过滤掉这些数据对象。运行response_df[response_df.Weight>105]将列出前面截图中呈现的异常值。
其次,我们可以直接从boxplot本身访问离群值。如果你注意前面的截图,你会注意到在本书中第一次将绘图函数的输出--这里是plt.boxplot()--分配给一个新变量--这里是fig。这样做的原因是,到目前为止,数据可视化的最终目标是可视化本身,而我们不需要访问可视化的细节。
然而,在这里,我们想访问传单并找出它们的值,以避免可能的视觉错误。
我们可以以类似的方式访问每个Matplotlib可视化的所有方面。如果你运行print(fig)并研究其结果,你会发现fig是一个字典,其键是可视化的不同元素。由于本例中的可视化是一个boxplot,其元素是cap、whiskers、fliers、box和median。每个键都与一个或多个matplotlib.lines.Line2D编程对象的列表有关。这是一个Matplotlib在其内部流程中使用的编程对象,但在这里我们想用它来给我们提供fliers的值。每个matplotlib.lines.Line2D对象都有.get_data()函数,可以给你显示在绘图上的值。例如,运行fig['fliers'][0].get_data()可以得到图11.21中显示为fliers的重量值。
print(fig)
'''
{'whiskers': [<matplotlib.lines.Line2D object at 0x0000020F9E7773A0>, <matplotlib.lines.Line2D object at 0x0000020F9E296F10>], 'caps': [<matplotlib.lines.Line2D object at 0x0000020F9E296DF0>, <matplotlib.lines.Line2D object at 0x0000020F9E296490>], 'boxes': [<matplotlib.lines.Line2D object at 0x0000020F9E777A90>], 'medians': [<matplotlib.lines.Line2D object at 0x0000020F9E296820>], 'fliers': [<matplotlib.lines.Line2D object at 0x0000020F9E892F40>], 'means': []}
'''
fig['fliers'][0].get_data()
'''
(array([120., 110., 111., 120., 113., 125., 165., 120., 150.]),
array([1., 1., 1., 1., 1., 1., 1., 1., 1.]))
'''
我们不需要用boxplot来寻找离群值。膨胀图本身使用以下公式来计算膨胀图的上限和下限。Q1和Q3是数据的第一和第三季度。

任何不在上限和下限之间的东西都将被标记为离群值。
下面的代码使用.quantile()函数和前面的公式来输出离群值。
Q1 = response_df.Weight.quantile(0.25)
Q3 = response_df.Weight.quantile(0.75)
IQR = Q3-Q1
BM = (response_df.Weight > (Q3+1.5 *IQR)) | (response_df.Weight < (Q1-1.5 *IQR))
response_df[BM]

使用我们在这个例子中涉及的两种方法中的任何一种,你会发现有九个数据对象的Weight值在统计学上与其他数据对象相差太大。这些异常值的Weight值是120、110、111、120、113、125、165、120和150。在继续阅读之前,请确保使用这两种方法确认这一点。
接下来,我们将看到一个例子,展示基于一个分类属性检测异常值的情况。
检测一个分类属性的异常值的例子
在这个例子中,我们想检测 response_df.Education 分类属性中的离群值。为了检测一个分类属性中的异常值,我们可以使用频率表或条形图。正如我们在第5章 "数据可视化 "中所学到的,你可以运行response_df.Education.value_counts()来获得一个频率表,而运行response_df.Education.value_counts().plot.bar()将创建一个条形图。运行这两行代码,以确认教育值为博士学位的数据对象在这一分类属性中是一个异常值。
现在我们已经具备了单变量离群点检测的工具。让我们把注意力转向双变量离群点检测。
response_df.Education.value_counts().plot.bar()

2.1.2 双变量离群点检测
由于单变量离群值检测只涉及一个属性,双变量离群值检测则涉及两个属性。在双变量离群点检测中,离群点是指其两个属性的数值组合与其他属性差别过大的数据对象。与单变量离群值检测类似,我们将用于双变量离群值检测的工具取决于属性的类型。对于数字-数字属性,最好使用散点图;对于数字-分类属性,最好使用多个boxplots;对于分类-分类属性,我们使用的工具是彩色编码的或然率表。
以下三个例子的特点是分类和数字属性的三种可能配对组合中的一种。
检测两个数字属性中的异常值的例子
在这个例子中,我们想检测由两个数字属性(response_df.Height和response_df.Weight)描述的离群值时。在检测两个数字属性的异常值时,最好使用散点图。运行 response_df.plot.scatter(x='Weight', y='Height') 将导致以下输出。
response_df.plot.scatter(x='Height',y='Weight')

根据前面的截图,我们可以清楚地看到两个离群值,一个是重量值大于120,另一个是高度值小于70。为了过滤掉这两个异常值,我们可以使用一个布尔掩码。下面的代码片断显示了如何做到这一点。
BM = (response_df.Weight>130) | (response_df.Height<70)
response_df[BM]
当前面的代码运行时,你会看到三个数据对象。如果你检查这些数据对象的身高和体重值,你会看到其中一个对象的身高值是缺失的,因此没有显示在散点图上。
这个例子的特点是,当两个属性是数字时,进行双变量离群检测。下一个例子是两个属性为分类时的双变量离群点检测。
检测两个分类属性中的异常值的例子
在这个例子中,我们想检测两个分类属性的离群值,即 response_df.god 和 response_df.Education。由于这两个属性是分类的,最好使用或然率表来检测离群值。运行pd.crosstab(response_df['Education'],response_df['God'])将创建一个或然率表。为了帮助看到异常值,你可以通过使用seaborn模块中的.heatmap()将该表变成热图。下面的代码将从或然率表中创建一个热图。
pd.crosstab(response_df['Education'],response_df['God'])

cont_table = pd.crosstab(response_df['Education'],response_df['God'])
sns.heatmap(cont_table,annot=True, center=0.5 ,cmap="Greys")

从前面的截图中,我们可以看到,有一些情况下,一个数据对象的一些组合值跨越了 response_df.god 和 response_df.Education。为了过滤掉这些异常值,我们也可以使用一个布尔掩码,但是由于分类属性的值会有很多打字,我们可能最好使用另一个Pandas DataFrame函数。.query()函数,正如它的名字所示,也可以帮助我们根据属性值对DataFrame进行过滤。
每次运行下面几行代码来过滤掉我们发现的异常值的每个数据对象。
response_df.query('Education== "currently a primary school pupil" & God==2')

response_df.query('Education== "currently a primary school pupil" & God==4')

response_df.query('Education== "doctorate degree" & God==3')


在这个例子中,我们涵盖了分类-分类的双变量离群点检测。在前面的例子中,我们介绍了数字-数字的双变量离群值检测。接下来,我们将介绍数字-分类双变量离群值检测。
检测两个属性(一个是分类属性,另一个是数字属性)中的异常值的例子
在这个例子中,我们想检测两个属性的离群值,即 response_df.Education 和 response_df.Age。注意 response_df.Education 是分类的,而 response_df.Age 是数字的。 在对一个数字属性和一个分类属性进行双变量离群检测时,我们使用多个boxplots。实质上,我们将为分类属性的每个类别创建一个横跨数字属性的boxplot。运行sns.boxplot(x=response_df.Age,y=response_df.Education)将创建以下boxplot,可用于离群点检测。
sns.boxplot(x=response_df.Age,y=response_df.Education)

这是我们在本书中第一次使用sns.boxplot()。我们确实在第5章 "数据可视化 "中学习了如何使用Matplotlib来做这个。在继续阅读之前,请尝试用Matplotlib重新创建boxplot。你会发现,使用seaborn函数要容易得多。
看一下多个boxplots,我们可以看到我们有一些教育类别的离群值:大学/学士学位、中学和小学。为了过滤掉这些异常值,我们可以使用布尔掩码或query()函数。
下面的代码显示了我们如何创建一个布尔掩码来包括所有的传单。
BM1 = (response_df.Education=='college/bachelor degree') & (response_df.Age>26)
BM2 = (response_df.Education == 'secondary school') & ((response_df.Age>24) | (response_df.Age<16))
BM3 = (response_df.Education == 'primary school') & ((response_df.Age>19) | (response_df.Age<16))
BM = BM1 | BM2 | BM3
response_df[BM]

2.1.3多变量离群点检测
检测两个以上属性的离群值被称为多变量离群值检测。
进行多元离群点检测的最好方法是通过聚类分析。下面的例子是一个多变量离群点检测的案例。
使用聚类分析检测四个属性的异常值的例子
在这个例子中,我们想看看我们是否有基于以下四个属性的离群值。乡村,音乐,金属或硬摇滚,以及民谣。如果你查看columns_df上对这些属性的完整描述,你会发现这些属性描述了四种音乐中每一种的数据对象的喜欢程度。如前所述,进行多变量离群点检测的最好方法是通过聚类分析。在第8章聚类分析中,我们学习了K-Means算法,在这里,我们将用它来查看我们是否有离群值
如果K-Means将一个数据对象或只有少数数据对象归入一个聚类,这将是我们的线索,即我们的数据中存在多变量的异常值。如果你还记得,K-Means算法的一个很大的弱点是必须指定聚类的数量,即k,。为了确保K-Means算法的弱点不会阻碍有效的离群点检测,并使分析有最好的成功机会,我们将使用不同的k值:2、3、4、5、6和7。我们需要分多个步骤进行,具体如下。
- 首先,我们将创建一个Xs属性,其中包括我们希望用于聚类分析的属性。下面的代码片断显示了这是如何做到的。
dimensions = ['Country', 'Metal or Hardrock','Folk','Musical']
Xs = response_df[dimensions]
- 第二,我们需要检查是否有任何缺失的值。你可以使用
Xs.info()来快速检测缺失值。
Xs.info()

- 如果存在缺失值,我们需要做一个与图11.18类似的分析,以检查所有缺失值是否来自其中一个数据对象。如果是这样的话,一个数据对象有两个以上的缺失值,这可能是一个值得关注的理由。然而,如果缺失值似乎是随机发生在Xs之间,我们可以用Q3+1.5*IQR来估算它们。
plt.figure(figsize=(4,7))
sns.heatmap(Xs.isna())
plt.show()

为什么不用中心趋势来归纳呢?我们不这样做的原因是,如果我们用中心趋势进行归因,就会降低有缺失值的数据对象被检测为异常值的可能性。我们不想用我们的缺失值归算来帮助一个有可能成为离群值的数据对象。在这种情况下,缺失值分布在数据对象和Xs的各个维度。因此,我们可以使用下面这行代码,用Q3+IQR*1.5来推算缺失值。
Q3 = Xs.quantile(0.75)
Q1 = Xs.quantile(0.25)
IQR = Q3 - Q1
Xs = Xs.fillna(Q3+IQR*1.5)
- 接下来,我们当然不会忘记用
Xs = (Xs - Xs.min())/(Xs.max()-Xs.min())对数据集进行标准化。 - 最后,我们可以用一个循环来对不同的K进行聚类分析并报告其结果。下面一行代码显示了如何做到这一点。
from sklearn.cluster import KMeans
for k in range(2,8):
kmeans = KMeans(n_clusters=k)
kmeans.fit(Xs)
print('k={}'.format(k))
for i in range(k):
BM = kmeans.labels_==i
print('Cluster {}: {}'.format(i,Xs[BM].index.values))
print('--------- Divider ----------')

一旦前面的代码运行成功,你可以滚动查看它的打印结果,在没有一个Ks下,K-Means将一个数据对象或少数数据对象归入一个群组。这将使我们得出结论,Xs中不存在多元离群点。
2.1.4 时间序列异常点检测
时间序列数据中的异常值最好用线图来检测,原因是时间序列的连续记录之间存在着密切的关系,利用这种密切关系是检查记录正确性的最好方法。你所需要的是对照最接近的连续记录来评估记录的价值,而这很容易用线图来完成。我们将在本章中看到一个时间序列离群点检测的例子--请看本章末尾检测系统误差下的例子。
现在我们已经涵盖了所有三种可能的离群值检测--单变量、双变量和多变量--我们可以把注意力转向处理离群值。
2.2 处理离群值
当我们在要分析的数据集中检测到异常值时,我们还需要有效地处理异常值。下面的列表强调了我们可以用四种方法来处理异常值。
- 什么都不做
- 用上限或下限替换
- 进行对数转换
- 删除有异常值的数据对象
2.2.1 第一种方法--什么都不做
虽然可能没有这种感觉,尤其是在经历了这么多的圈套来检测异常值之后,在大多数分析情况下,什么都不做是最好的策略。原因是,我们使用的大多数分析工具都可以轻松地处理异常值。事实上,如果你知道你想使用的分析工具可以处理异常值,你可能首先就不会进行异常值检测。然而,离群值检测本身可能是你需要的分析方法,或者你需要使用的分析工具容易出现离群值。
下面截图中的表格列出了我们在本书中涉及的所有分析工具/目标,并指定了处理异常值的最佳方法。

正如你在图11.25中看到的,在大多数分析情况下,采用第一种方法会更好:什么都不做。现在,让我们继续,学习下一个方法。
2.2.2 第二种方法--用上盖或下盖替换
当满足以下条件时,应用这种方法可能是明智的。
- 离群值是单变量的。
- 分析目标和/或工具对离群点很敏感。
- 我们不希望因为删除数据对象而丢失信息。
- 值的突然变化不会导致分析结论的重大改变。
如果符合标准,在这种方法中,离群值会被替换成正确的上限或下限。上限和下限是我们在本章前面的单变量离群点检测部分讨论的统计概念。它们也是任何boxplot的一个重要部分。我们用Q1-1.5IQR属性的下限替换比其他数据对象小得多的单变量离群值,用Q3+1.5IQR属性的上限替换比其他数据对象大得多的单变量离群值。
2.2.3 第三种方法--进行对数转换
这种方法不仅仅是一种处理异常值的方法,也是一种有效的数据转换技术,我们将在相关章节中介绍。作为一种处理离群值检测的方法,它只适用于某些情况。当一个属性遵循指数分布时,只有一些数据对象与其他对象有很大的不同才是典型的。在这些情况下,应用对数转换将是最好的方法。
2.2.4 第四种方法--删除有离群值的数据对象
当其他方法无济于事或不可能时,我们可能会沦落到删除带有离群值的数据对象。这是我们最不喜欢的方法,只应在绝对必要时使用。我们希望避免这种方法的原因是,数据并不是不正确的;离群值是正确的,但碰巧与其他人群有太大的不同。这是我们的分析工具没有能力处理实际的人口。
请注意!
至于何时以及是否应该采用删除因是离群值的数据对象的方法,我想与你分享一个重要的建议。首先,只对你为具体分析而创建的数据集的预处理版本应用这种方法,而不是对源数据。
这个分析需要去除有异常值的数据对象的事实并不意味着所有的分析都需要这样做。其次,要优先告知受众所产生的分析结果,因为他们会知道在处理异常值时的这种侵入性方法。
2.2.5 在处理异常值时选择正确的方法
在处理异常值时选择正确的方法,必须从分析目标和分析工具中获得信息。如图11.25所示,在大多数情况下,处理异常值的最好方法是采用不做任何处理的方法。如果其他方法是必要的,请确保只将它们应用于你为分析所创建的数据的预处理版本,而不要改变源数据集。
我们现在知道了处理异常值所需要的一切,所以让我们看几个例子,把我们所学的东西付诸实践。
2.2.6 例子1
我们之前看到 response_df.Weight属性有一些离群值。我们想用直方图来画出人口在这个属性上的分布。
由于我们分析的最终目标是将人口分布可视化,离群值的存在可能会消耗一些可视化空间,因此,去除离群值可以打开可视化空间。
下面的代码片段及其输出显示了如何为response_df.Weight创建两个直方图版本,一个有离群值,另一个没有。
response_df.Weight.plot.hist(histtype='step')
plt.show()
BM = response_df.Weight<105
response_df.Weight[BM].plot.hist(histtype='step')
plt.show()

在前面的截图中,从分析的角度来看,你可以想象在哪些情况下,这两种视觉效果会更合适。例如,如果我们对看到大多数人口在40和100之间的频率变化感兴趣,那么没有离群值的直方图会更好。另一方面,如果人口的真实代表是我们的最终目标,那么带有离群值的直方图将是理想的。
在前面的截图中,从数据预处理的角度来看,请注意这样一个事实:为了创建一个没有离群值的直方图,我们没有编辑response_df,而是临时创建了一个DataFrame,目的就是为了创建一个没有离群值的直方图。
现在,让我们考虑另一个例子。
2.2.7 例子2
我们想可视化两个属性之间的关系,response_df.Height 和 response_df.weight。由于这两个属性都是数字的,我们知道可视化这种关系的最佳方法是散点图。我们还希望在可视化中包含线性回归 (LR) 线,以增加其分析值。
我们被告知 LR 容易出现异常值。让我们借此机会了解原因。我们将首先采用什么都不做的方法并创建一个可视化来查看如果数据中有异常值进行回归分析会发生什么。
以下屏幕截图显示了 seaborn 模块中的 .regplot() 函数用于创建散点图可视化的应用程序:
sns.regplot(x='Height',
y='Weight',data=response_df)
plt.show()

你可以在前面的截图中看到,我们在图11.22中检测到的异常值消耗了可视化的空间,不允许关系充分显示。
然而,下面的截图显示了在可视化的最后一步删除离群值的代码。
BM = (response_df.Weight>130) | (response_df.Height<70)
sns.regplot(x='Height',
y='Weight',data=response_df[~BM])
plt.show()

对比最后两张截图,我们可以看到去除这两个离群值后,可视化可以更好地显示这两个变量之间的关系。你可以在前面的截图中看到,较高的高度值会导致较高的重量值
2.2.8 例子3
在这个例子中,我们想用回归来捕捉体重、身高和性别之间的线性关系来预测体重,这一点我们在前面的例子中看到过。
换句话说,我们想在以下公式中找到β0和β1的值。

正如我们在图11.25中看到的,回归分析对离群值很敏感。我们还在图11.28中观察到,体重和身高都有离群值。我们还需要检查性别是否有离群值。
这将是一个很长的例子,所以请自始至终忍受一下。在这个例子中,我们将逐一讨论以下步骤。
- 处理缺失值
- 检测单变量离群值并处理它们
- 检测双变量离群值并处理它们
- 检测多变量离群值并处理它们
- 应用LR
让我们从第一步开始。
处理缺失值的问题
然而,在这之前,我们首先需要处理这三个属性中的缺失值,因为当输入数据有缺失值时,来自sklearn.linear_model的LinearRegression会出现错误。下面的代码片段显示了我们将如何开始预处理这个例子的数据。
select_attributes = ['Weight','Height','Gender']
pre_process_df = pd.DataFrame(response_df[select_attributes])
pre_process_df.info()

运行前面的代码后,你可以看到,体重和身高有20个缺失值,性别有6个缺失值。假设我们知道这些缺失值是MCAR类型的。
为了处理回归分析的缺失值,我们不能使用保持原样的策略,因为我们计划使用的工具不能处理离群值。代入值也不是一个好的选择,因为这将在数据中产生偏差。因此,剩下的唯一可做的选择就是放弃数据对象。下面这行代码使用.dropna()函数来删除缺失值的数据对象。
运行这段代码后,重新运行pre_process_df.info()以确认pre_process_df不再有缺失值。
现在我们确定pre_process_df中没有缺失值,我们可以把注意力转向检测和处理异常值,因为LR很容易出现异常值。我们需要检测数据是否有单变量、双变量或多变量的异常值。在下面的小节中,我们将一个步骤一个步骤地进行。
pre_process_df.dropna(inplace=True)
pre_process_df.info()

检测单变量离群值并处理它们
下面的截图显示了在这个例子中为数字属性创建了图表,为分类属性创建了条形图的代码。
num_attributes = ['Weight','Height']
for i,att in enumerate(num_attributes):
plt.subplot(1,3,i+1)
pre_process_df[att].plot.box()
plt.subplot(1,3,3)
pre_process_df.Gender.value_counts().plot.bar()
plt.tight_layout()
plt.show()

在前面的截图中,我们可以看到,身高和体重都有离群值,但性别没有。因此,在继续进行LR分析之前,我们需要处理离群值。正如图11.25所建议的,我们可以使用以下两种方法之一来处理异常值。
- 删除这些数据对象
- 用它们的统计上限或下限取代它们
但哪种方法更好呢?当数据对象是单变量离群值时,最好使用第二种方法,因为替换统计上限或下限将有助于保留数据对象,同时减轻有离群值的数据对象的负面作用。
另一方面--这也普遍适用--当数据对象是双变量或多变量的离群值时,最好将其删除。这是因为这些离群值将不允许回归模型捕捉非离群值数据对象中的模式。
在双变量异常值的特殊情况下,即属性对是分类数字的,用特定人群的上限或下限替换异常值可能也是明智的。
因此,让我们首先处理单变量离群值,用统计学上的下限和上限替换它们。下面的代码将pre_process_df.Weight的离群值替换为该属性的统计上限。
Q3 = pre_process_df.Weight.quantile(0.75)
Q1 = pre_process_df.Weight.quantile(0.25)
IQR = Q3 - Q1
upper_cap = Q3+IQR*1.5
BM = pre_process_df.Weight > upper_cap
pre_process_df.loc[pre_process_df[BM].index,'Weight'] = upper_cap
运行前面的代码后,运行pre_process_df.Weight.plot.box(),看看离群值是否被处理了。另外,在继续替换pre_process_df.Height中的flier之前,要注意两个事项,如下所示。
- 首先,通过观察图11.29,你会发现
pre_process_df.Weight中的flier只比属性的统计上限值大。这就是为什么在前面的代码中,我们没有用属性的统计下限进行替换。当我们对pre_process_df.Height做同样的程序时,这将会改变。 - 第二,我们可以让boxplot本身提取属性的统计上限和下限,但相反,我们分别使用公式
Q1-1.5*IQR和Q3+1.5*IQR来计算统计上限和下限。这是因为我们不想在自己有公式计算的情况下,让计算机不必要地画图而浪费计算资源。
pre_process_df.Weight.plot.box()

接下来,我们将对pre_process_df.Height进行同样的程序,以处理单变量离群值。下面的代码显示了如何做到这一点。
在成功运行前面的代码后,运行pre_process_df.Weight.plot.box()来检查离群值的状态。
现在,单变量的异常值已经被处理了,让我们看看是否有双变量或多变量的异常值。
Q3 = pre_process_df.Height.quantile(0.75)
Q1 = pre_process_df.Height.quantile(0.25)
IQR = Q3 - Q1
lower_cap = Q1-IQR*1.5
upper_cap = Q3+IQR*1.5
BM = pre_process_df.Height < lower_cap
pre_process_df.loc[pre_process_df[BM].index,'Height'] = lower_cap
BM = pre_process_df.Height > upper_cap
pre_process_df.loc[pre_process_df[BM].index,'Height'] = upper_cap
pre_process_df.Height.plot.box()

检测双变量离群值并处理它们
运行pre_process_df.plot.scatter(x='Height', y='Weight')将显示我们没有基于高度和体重数字属性的双变量离群值。然而,运行下面的代码将告诉我们,我们在身高和性别以及体重和性别下确实有双变量离群值。
pre_process_df.plot.scatter(x='Height',y='Weight')

plt.subplot(1,2,1)
sns.boxplot(y=pre_process_df.Height,x=pre_process_df.Gender)
plt.subplot(1,2,2)
sns.boxplot(y=pre_process_df.Weight, x=pre_process_df.Gender)
plt.tight_layout()

鉴于前面截图中公认的双变量离群值,我们将需要处理它们。由于这些离群值在分类-数字属性对中是双变量的,我们可能要用特定人群的上限或下限来替换它们。
下面的代码替换了高度-性别属性对的离群值。
for poss in pre_process_df.Gender.unique():
BM = pre_process_df.Gender == poss
wdf = pre_process_df[BM]
Q3 = wdf.Height.quantile(0.75)
Q1 = wdf.Height.quantile(0.25)
IQR = Q3 - Q1
lower_cap = Q1-IQR*1.5
upper_cap = Q3+IQR*1.5
BM = wdf.Height > upper_cap
pre_process_df.loc[wdf[BM].index,'Height'] = upper_cap
BM = wdf.Height < lower_cap
pre_process_df.loc[wdf[BM].index,'Height'] = lower_cap
非常类似的代码将取代Weight-Gender属性对的离群值,如图所示。
for poss in pre_process_df.Gender.unique():
BM = pre_process_df.Gender == poss
wdf = pre_process_df[BM]
Q3 = wdf.Weight.quantile(0.75)
Q1 = wdf.Weight.quantile(0.25)
IQR = Q3 - Q1
lower_cap = Q1-IQR*1.5
upper_cap = Q3+IQR*1.5
BM = wdf.Weight > upper_cap
pre_process_df.loc[wdf[BM].index,'Weight'] = upper_cap
BM = wdf.Weight < lower_cap
pre_process_df.loc[wdf[BM].index,'Weight'] = lower_cap
在成功运行前面的代码后,运行下面截图中的代码,也就是检测双变量异常值并处理它们下的代码,将显示我们已经处理了双变量异常值。
plt.subplot(1,2,1)
sns.boxplot(y=pre_process_df.Height,x=pre_process_df.Gender)
plt.subplot(1,2,2)
sns.boxplot(y=pre_process_df.Weight, x=pre_process_df.Gender)
plt.tight_layout()

检测多变量异常值并处理它们
为了检测多变量异常值,标准的方法是使用聚类分析;但是,当三个属性中有两个是数字属性,另一个是分类属性时,我们可以使用特定的可视化技术进行异常值检测。
下面的代码为性别分类属性的每种可能性创建了一个身高和体重的散点图。
cat_attribute_poss = pre_process_df.Gender.unique()
for i,poss in enumerate(cat_attribute_poss):
BM = pre_process_df.Gender == poss
pre_process_df[BM].plot.scatter(x='Height',y='Weight')
plt.title(poss)
plt.show()

根据前面的截图,我们可以得出结论,数据中没有多变量的离群值。如果有的话,我们唯一的选择就是删除它们,因为离群值会对LR性能产生负面影响。另外,如前所述,对于多变量异常值来说,用大写字母和小写字母替换异常值是不可取的。
在处理了离群值和缺失值之后,我们终于准备好使用LR来估计身高、性别和体重之间的关系,以预测体重。
应用LR
在对pre_process_df应用LR之前,我们需要采取另一个预处理步骤。
请注意,Gender属性是分类的,不是数字的,而LR只能处理数字。因此,下面的代码将进行数据转换,使该属性成为二进制编码。
pre_process_df.Gender.replace({'male':0,'female':1},inplace=True)
下面的代码分别在data_X和data_Y中准备好独立和依赖属性,然后使用sklearn中的LinearRegression()。
linear_model来将预处理的数据拟合到模型中。
from sklearn.linear_model import LinearRegression
X = ['Height','Gender']
y = 'Weight'
data_X = pre_process_df[X]
data_y = pre_process_df[y]
lm = LinearRegression()
lm.fit(data_X, data_y)
如果前面的代码运行成功,那么我们可以运行下面截图中的代码,从拟合的lm值中获取估计的β值。
print('intercept (b0) ', lm.intercept_)
coef_names = ['b1','b2']
print(pd.DataFrame({'Predictor': data_X.columns,
'coefficient Name':coef_names,
'coefficient Value': lm.coef_}))

因此,可以从前面的截图中的输出驱动以下方程。该方程现在可以根据个人的身高和性别值来预测个人的体重值。

例如,我的身高是189厘米,性别是男性(0)。使用以下公式,我的体重可以预测为82.895。

这很好,但是我现在的体重是86公斤,所以有4公斤左右的误差。
2.2.9 例子4
在这个例子中,我们想重复之前的例子,但这次我们想用多层感知器(MLP)来预测基于性别和身高的体重。
这个例子与上一个例子的数据预处理不同之处在于,MLP对异常值有弹性,我们不需要担心数据集有异常值。
然而,我们确实需要处理缺失值和性别属性的二进制代码。下面的代码重新创建了pre_process_df,处理了缺失值,并对性别属性进行了二进制编码转换。
select_attributes = ['Weight','Height','Gender']
pre_process_df = pd.DataFrame(response_df[select_attributes])
pre_process_df.dropna(inplace=True)
pre_process_df.Gender.replace({'male':0,'female':1},inplace=True)
运行前面的代码后,pre_process_df已经准备好用于MLP。
下面的代码分别在data_X和data_Y中准备好独立和依赖属性,然后使用sklearn.linear_model中的MLPRegressor()将预处理过的数据拟合到模型中。
from sklearn.neural_network import MLPRegressor
X = ['Height','Gender']
y = 'Weight'
data_X = pre_process_df[X]
data_y = pre_process_df[y]
mlp = MLPRegressor(hidden_layer_sizes=5, max_iter=3000)
mlp.fit(data_X, data_y)
一旦前面的代码运行成功,我们就可以使用训练好的mlp属性来进行预测。下面的代码片段显示了如何使用mlp根据我的身高和性别值来提取我的体重值的预测。
newData = pd.DataFrame({'Height':189.5,'Gender':0},
index=[0])
mlp.predict(newData) # array([79.89579149])
在我最后一次运行前面的代码时,我得到的预测结果是80.0890。你会记得,MLP是一个随机变量,每次运行它,我们都会期待一个新的结果。总之,由于我的体重是86,MLP大约是6公斤。这是否意味着与MLP相比,lm(之前的例子)是一个预测器?不一定--毕竟,我只是一个数据点。需要更多的测试数据来做这个判断。
让我们看看另一个例子,其特点是为了应用聚类分析而处理异常值,然后再继续下一个项目。
2.2.10 例子5
在这个例子中,我们想使用chicago_population.csv。这个数据集中的数据对象是芝加哥的社区。这些数据对象由以下属性描述。
- 人口。社区的人口
- 收入。社区的收入中位数
- 拉美人:拉美人在人口中的百分比
- 黑人。黑人:黑人在人口中的百分比
- 白人。白人在人口中的百分比
- 亚洲人:亚洲人在人口中的百分比
- 其他。其他种族在人口中的百分比
芝加哥市长希望为这77个社区指派5名沟通联络员。办公室的数据分析员建议采用K-Means聚类法,将社区分成五组,并根据聚类组的特点分配适当的联络员。
首先,我们将把文件读入community_df pandas DataFrame,并检查数据集中是否有缺失值。下面的代码显示了如何做到这一点。
community_df = pd.read_csv('chicago_population.csv')
community_df.info()

阅读前面代码的输出,我们会发现community_df中没有缺失值。接下来,我们需要检测异常值并加以处理。
检测单变量离群值并处理它们
下面的代码使用sns.boxplot()来创建所有数字属性的boxplots。
numerical_atts = ['population', 'income', 'latino', 'black', 'white', 'asian','other']
plt.figure(figsize=(12,3))
for i,att in enumerate(numerical_atts):
plt.subplot(1,len(numerical_atts),i+1)
sns.boxplot(y=community_df[att])
plt.tight_layout()
plt.show()

在前面的截图中,我们可以看到我们在人口、亚洲和其他属性方面有一些单变量的离群值。
由于我们正在使用K-Means将社区聚类为五个同质组来分配沟通联络人,处理离群值的最好方法是用统计学上的小写或大写来代替它们。我们不希望离群值的极端值影响聚类的结果。
请注意,在应用聚类分析之前,这并不是处理异常值的唯一或最佳方法。如果我们使用聚类分析来找出数据中的固有模式,那么处理异常值的最好方法就是什么都不做。
下面的代码使用了与我们在例3下使用的类似的代码来过滤异常值,然后用适当的上限来替换它们。请注意,这段代码比我们在例3中看到的要聪明一些,因为处理离群值的过程在一个循环中被参数化了。
pre_process_df = community_df.set_index('name')
candidate_atts = ['population','asian','other']
for att in candidate_atts:
Q3 = pre_process_df[att].quantile(0.75)
Q1 = pre_process_df[att].quantile(0.25)
IQR = Q3 - Q1
lower_cap = Q1-IQR*1.5
upper_cap = Q3+IQR*1.5
BM = pre_process_df[att] < lower_cap
candidate_index = pre_process_df[BM].index
pre_process_df.loc[candidate_index,att] = lower_cap
BM = pre_process_df[att] > upper_cap
candidate_index = pre_process_df[BM].index
pre_process_df.loc[candidate_index,att] = upper_cap
运行前面的代码后,单变量离群值将被替换成适当的统计上限。
numerical_atts = ['population', 'income', 'latino', 'black', 'white', 'asian','other']
plt.figure(figsize=(12,3))
for i,att in enumerate(numerical_atts):
plt.subplot(1,len(numerical_atts),i+1)
sns.boxplot(y=pre_process_df[att])
plt.tight_layout()
plt.show()

检测双变量和多变量的异常值并处理它们
它对检测双变量和多变量异常值没有任何价值,因为在这个阶段,我们对它们的唯一策略是什么都不做--我们不能用大写或小写来代替它们,因为有不止一个数字属性;我们也不能删除数据对象,因为我们需要所有数据对象至少在一个聚类中。因此,pre_process_df的当前状态是聚类分析的最佳状态。
由于数据预处理已经完成,这个例子中唯一剩下的步骤就是进行聚类。这就是我们接下来要做的事情。
应用K-Means
下面的代码片段显示了第八章聚类分析中K-Means聚类的调整版代码。
from sklearn.cluster import KMeans
dimensions = ['population', 'income', 'latino', 'black', 'white', 'asian','other']
Xs = pre_process_df[dimensions]
Xs = (Xs - Xs.min())/(Xs.max()-Xs.min())
kmeans = KMeans(n_clusters=5)
kmeans.fit(Xs)
一旦前面的代码运行成功,集群就会形成。下面的截图显示了我们可以用来提取集群的代码和代码的输出。
for i in range(5):
BM = kmeans.labels_==i
print('Cluster {}: {}'.format(i,pre_process_df[BM].index.values))

我们还可以对刚刚形成的聚类进行中心点分析。中心点分析的代码在第8章聚类分析中的使用K-Means对超过两个维度的数据集进行聚类一节中介绍过。找到这段代码并调整它,以确认以下热图是中心点分析的结果。请注意,由于K-Means是一种随机算法,我们确实期望热图会有所不同。同时,我们期望从数据中出现的模式是相似的。
clusters = ['Cluster {}'.format(i) for i in range(5)]
Centroids = pd.DataFrame(0.0, index = clusters,
columns = Xs.columns)
for i,clst in enumerate(clusters):
BM = kmeans.labels_==i
Centroids.loc[clst] = Xs[BM].median(axis=0)
sns.heatmap(Centroids, linewidths=.5, annot=True,
cmap='binary')
plt.show()

我们可以从前面的截图中看到,每个群组中的社区都有明显的不同,这个结果对分配交流联络人有极大的帮助。
本章到目前为止,我们已经介绍了如何检测和处理缺失值和异常值的例子。接下来,我们将把注意力转向检测数据集中的错误并处理它们。
3 错误值
误差是任何数据收集和测量中不可避免的一部分。下面的公式最能体现这一事实。

真正的信号是我们试图测量并以数据的形式呈现的现实,但由于我们的测量系统或数据表现的无能,我们无法捕获真正的信号。因此,误差是真实信号和记录数据之间的差异。
例如,假设我们购买了七个温度计,我们想用这七个温度计准确地计算出室温。在某个特定的时间点,我们从它们身上取得以下读数。

看看前面的截图,你会说房间的温度--真实信号--是多少?答案是,我们无法测量或捕获真实信号--在这种情况下,房间的确切温度。有了七个温度计,我们也许能得出更准确的读数,但我们无法消除误差。
3.1 错误类型
有两种类型的错误:随机错误(random errors)和系统错误(systematic errors)。这两类误差的最大区别是,随机误差是不可避免的,但系统误差是可以避免的。
随机误差的发生是由于不可避免的不一致和我们测量设备的限制。我们在七个温度计的例子中看到的就是一个随机误差的案例。另一个例子是在使用调查表测量人们的意见时,由于不可避免的沟通不畅和误解而发生的随机误差。
另一方面,系统性错误是由于在整个数据收集过程中持续存在的问题而发生的可避免的不一致。系统性错误发生在随机错误之上,也就是说,随机错误总是存在的。例如,如果使用未经校准的温度计来测量室温,由于设备无法捕捉到真实的信号,我们有随机误差,同时由于在测量行为之前没有校准温度计,我们也有系统误差。
3.2 处理错误
我们将根据错误的类型,以不同的方式处理它们。随机误差是不可避免的,在最好的情况下,我们可能会使用平滑或聚合的方式来减轻它们。这些都是我们将在未来的某一章中介绍的技术。数据处理和转换。
然而,系统性错误是可以避免的,一旦认识到这一点,我们应该始终采取以下步骤来处理它们。
- 调整和改进数据收集,使系统性错误在未来不会发生。
- 如果有的话,尝试使用其他数据资源来寻找正确的数值,如果没有的话,我们将把系统性错误视为缺失的数值。
从第二步开始,你将把系统性错误作为缺失值来处理。
这很好,因为我们已经涵盖了数值,并且在处理缺失值方面得到了许多强大的工具和技术。
3.3 检测系统性错误
检测系统性错误不是很容易,很可能它们没有被注意到,并对我们的分析产生负面影响。我们检测系统性错误的最好机会是我们在检测异常值部分学到的技术。当检测到离群值,并且没有解释为什么离群值是正确的,那么我们可以得出结论,离群值是系统性错误。下面的例子将有助于阐明这一区别。
系统误差和纠正异常值的例子
在这个例子中,我们想分析CustomerEntries.xlsx。这个数据集包含了从2020年10月1日到2020年11月24日之间一家当地咖啡店的大约2个月的顾客访问数据。分析的目的是对一天中的各个时段进行剖析,以了解客户访问高峰发生在哪些时间和日期。
下面的截图显示了将文件读入hour_df pandas DataFrame的代码,以及使用.info()函数来评估数据集的缺失值状态。
hour_df = pd.read_excel('CustomerEnteries.xlsx')
hour_df.info()

我们可以从前面的截图中看到,数据集没有缺失值。接下来,我们将把注意力转向检查离群值。由于数据集本质上是一个时间序列,最好使用线图来查看是否有任何异常值。下面的截图显示了运行hour_df.N_Customers.plot()创建线图的输出。
hour_df.N_Customers.plot()
plt.show()

在前面的截图中,我们可以看到我们有一个明显的离群点的情况,在200和300索引之间。运行hour_df[hour_df.N_Customers>20]会发现,离群点发生在索引232,其时间戳为2020-10-26的16。
hour_df[hour_df.N_Customers>20]

为了检查这个离群点是否是一个系统性错误的案例,我们使用其他来源进行调查,我们意识到在那一天没有发生任何异常情况,这个记录可能只是一个人工数据输入错误。这表明这是一个系统性错误,因此我们需要采取以下两个步骤来处理系统性错误。
- 第一步:我们将这个错误告知负责数据收集的实体,并要求他们采取适当的措施来防止今后发生这样的错误。
- 步骤2:如果我们没有办法在合理的时间和精力内利用其他资源找到正确的值,我们就把这个数据条目视为缺失值,并用
np.nan来代替它。下面的代码可以解决这个问题。
err_index = hour_df[hour_df.N_Customers>20].index
hour_df.at[err_index,'N_Customers']=np.nan
hour_df.N_Customers.plot()
plt.show()

你可以在前面的截图中看到,我们不再看到单变量的离群点了。
虽然这个时间序列看起来像一个单变量数据集,但它并不是单变量的,我们可以随时进行二级数据清洗来解开新的列,如月、日、工作日、小时和分钟。在这个数据集中,时间和数据已经被分离,所以我们可以进行下面的双变量离群检测。
正如你所记得的,对一对数字分类属性进行双变量离群点检测的最好方法是使用多个boxplots。下面的截图显示了sns.boxplot(y=hour_df.N_Customers, x=hour_df.Time)的输出,这是我们需要的多个boxplots,以查看N_Customers和Time属性是否存在双变量离群值。
sns.boxplot(y=hour_df.N_Customers,x=hour_df.Time)
plt.show()

看看前面的截图,我们确实看到我们还有两个可能是系统性错误的离群值。第一个是N_Customers的最小值,它是零,在时间值17之下。该值与数据的其他部分一致。时间值17(或下午5点)似乎得到的顾客数量最少,我们可以想象在这个时间段偶尔没有顾客。
然而,在同一时间(下午5点)的第二个传单似乎更令人不安。运行hour_df.query("Time==17 and N_Customers>12"),对传单进行过滤后,我们可以看到异常点已经发生在2020年11月17日。经过调查,原来在2020年11月17日4:25,一个自行车俱乐部在半小时内停下来吃点心,这对该店来说是不正常的。因此,这个数据输入并没有错误,只是一个正确的离群值。
hour_df.query("Time==17 and N_Customers>12")

对hour_df进行预处理后,它现在有一个缺失值(用np.nan替换的系统误差)和两个双变量离群值。了解到这一点,我们允许自己进入最后一步:分析。
绘制一个条形图来显示和比较咖啡店每个工作小时的N_Customers(时间)的中心趋势,将是我们在这个分析中需要的可视化。
按规定的条形图可以很容易地处理缺失值,因为要计算中心趋势的数据汇总。由于我们在数据集中有异常值,我们选择使用中位数而不是平均值作为本次分析的中心趋势。运行下面这行代码将创建所述的条形图。
hour_df.groupby('Time').N_Customers.median().plot.bar()
plt.show()

正如你在这个例子中所经历的,我们用来检测和处理系统性错误的技术已经在缺失值和异常值的小节中有所涉及。简而言之,当我们找不到任何支持来相信一个离群值是一个正确的值时,我们就把它看作是一个系统性错误,因而也是一个缺失值。
4 总结
祝贺你在本章的学习。这一章涵盖了数据清理的第三级。我们一起学习了如何检测和处理缺失值、离群值和错误。对于这么长的一章来说,这听起来似乎是一个太短的总结,但正如我们所看到的,检测、诊断和处理这三个问题(缺失值、异常值和错误)中的每一个都会有很多细节和妙处。完成这一章是一个重要的成就,现在你知道如何检测、诊断和处理在处理数据集时可能遇到的所有这三个问题。
本章结束了我们长达三章的数据清洗之旅。在下一章,我们将进入另一个重要的数据预处理领域,那就是数据融合和整合。在进入下一章之前,请花一些时间做以下练习,以巩固你的学习成果。

浙公网安备 33010602011771号