数据导入与预处理-第5章-数据清理

2022-11-12 16:52:19 浏览数 (1)

数据导入与预处理-第5章-数据清理

  • 1. 数据清理概述
    • 1.1 数据清理概述
    • 1.2 什么是缺失值
    • 1.3 什么是重复值
    • 1.4 什么是异常值
  • 2. 数据清理案例
    • 2.1 缺失值
      • 2.1.1 缺失值的检测与处理方法
      • 2.1.2 删除缺失值
      • 2.1.3 填充缺失值
      • 2.1.4 插补缺失值
      • 2.1.5 缺失值处理案例
    • 2.2 重复值处理
      • 2.2.1 重复值的检测
      • 2.2.2 重复值的处理
      • 2.2.3 重复值处理案例
    • 2.3 异常值处理
      • 2.3.1 异常值的检测
        • 2.3.1.1 3σ原则
        • 2.3.1.2 箱形图检测异常值
      • 2.3.2 异常值的处理
        • 构建数据:
        • 基于 3σ原则 进行异常值检测
        • 基于箱型图进行异常检测
        • 替换采用replace函数:

1. 数据清理概述

1.1 数据清理概述

数据清理是数据预处理的一个关键环节,它占据整个数据分析或挖掘50%~70%的时间。在这一环节中,我们主要通过一定的检测与处理方法,将良莠不齐的“脏”数据清理成质量较高的“干净”数据。pandas为数据清理提供了一系列方法,本章将围绕这些数据清理方法进行详细地讲解。 数据清理概述

缺失值的检测与处理 重复值的检测与处理 异常值的检测与处理

数据清理是数据预处理中关键的一步,其目的在于剔除原有数据中的“脏” 数据,提高数据的质量,使数据具有完整性、唯一性、权威性、合法性和一致性等特点。数据清理的结果直接影响着数据分析或数据挖掘的结果。 数据清理主要解决前面介绍过的数据问题,常遇到的数据问题有3种:数据缺失、数据重复、数据异常,它们分别是由数据中存在缺失值、重复值、异常值而引起的。

1.2 什么是缺失值

缺失值是指样本数据中某个或某些属性的值是不全的,主要是由于机械故障、人为原因导致部分数据未能收集。

若直接使用有缺失值的数据进行分析,会降低分析结果的准确性,为此需通过合适的方式予以处理。缺失值主要有三种处理方式:删除填充插补

  1. 删除缺失值:删除缺失值是最简单的处理方式,这种方式通过直接删除包含缺失值的行或列来达到目的,适用于删除缺失值后产生较小偏差的样本数据,但并不是十分有效。
  2. 填充缺失值:填充缺失值是比较流行的处理方式,这种方式一般会将诸如平均数、中位数、众数、缺失值前后的数填充至空缺位置。
  3. 插补缺失值:插补缺失值是一种相对复杂且灵活的处理方式,这种方式主要基于一定的插补算法来填充缺失值。

常见的插补算法有线性插值和最邻近插值:线性插值是根据两个已知量的直线来确定在这两个已知量之间的一个未知量的方法,简单地说就是根据两点间距离以等距离方式确定要插补的值;最邻近插值是用与缺失值相邻的值作为插补的值。

1.3 什么是重复值

重复值是指样本数据中某个或某些数据记录完全相同,主要是由于人工录入、机械故障导致部分数据重复录入。 重复值主要有两种处理方式:删除保留,其中删除重复值是比较常见的方式,其目的在于保留唯一的数据记录。需要说明的是,在分析演变规律、样本不均衡处理、业务规则等场景中,重复值具有一定的使用价值,需做保留。

1.4 什么是异常值

异常值是指样本数据中处于特定范围之外的个别值,这些值明显偏离它们所属样本的其余观测值,其产生的原因有很多,包括人为疏忽、失误或仪器异常等。 处理异常值之前,需要先辨别哪些值是“真异常”和“伪异常”,再根据实际情况正确地处理异常值。 异常值的处理方式主要有保留删除替换。保留异常值也就是对异常值不做任何处理,这种方式通常适用于“伪异常”,即准确的数据;删除异常值和替换异常值是比较常用的方式,其中替换异常值是使用指定的值或根据算法计算的值替代检测出的异常值。

总而言之,缺失值、重复值、异常值都有多种处理方式,具体选用哪种方式进行处理要依据具体的处理需求和样本数据特点。

2. 数据清理案例

2.1 缺失值处理

2.1.1 缺失值的检测与处理方法

缺失值的检测可以采用isnull()、notnull()、isna()和notna()方法的用法,可以熟练地使用这些方法来检测缺失值。

isnull()、notnull()、isna()和notna()方法均会返回一个由布尔值组成、与原对象形状相同的新对象 其中isnull()和isna()方法的用法相同,它们会在检测到缺失值的位置标记True; notnull()和notna()方法的用法相同,它们会在检测到缺失值的位置标记False。

为避免包含缺失值的数据对分析预测结果产生一定的偏差,缺失值被检测出来之后一般不建议保留,而是选择适当的手段给予处理。缺失值的常见处理方式有三种:删除缺失值填充缺失值插补缺失值,pandas中为每种处理方式均提供了相应的方法。

2.1.2 删除缺失值

pandas中提供了删除缺失值的方法dropna(),dropna()方法用于删除缺失值所在的一行或一列数据,并返回一个删除缺失值后的新对象。

代码语言:javascript复制
DataFrame.dropna(axis=0, how='any', thresh=None, subset=None,inplace=False)

axis:表示是否删除包含缺失值的行或列。 how:表示删除缺失值的方式。 thresh:表示保留至少有N个非NaN值的行或列。 subset:表示删除指定列的缺失值。 inplace:表示是否操作原数据。

删除缺失值的前后对比:

2.1.3 填充缺失值

pandas中提供了填充缺失值的方法fillna(),fillna()方法既可以使用指定的数据填充,也可以使用缺失值前面或后面的数据填充。

代码语言:javascript复制
DataFrame.fillna(value=None, method=None, axis=None, inplace=False,
                            limit=None, downcast=None)

method:表示填充的方式,默认值为None。该参数还支持 'pad’或’ffill’和’backfill’或’bfill’几种取值,其中’pad’或’ffill’表示将最后一个有效值向后传播,也就是说使用缺失值前面的有效值填充缺失值;'backfill’或’bfill’表示将最后一个有效值向前传播,也就是说使用缺失值后面的有效值填充缺失值。 limit:表示可以连续填充的最大数量。

平均数填充:

后向填充:

2.1.4 插补缺失值

pandas中提供了插补缺失值的方法interpolate(),interpolate() 会根据相应的插值方法求得的值进行填充。

代码语言:javascript复制
DataFrame.interpolate(method='linear', axis=0, limit=None, inplace=False,
        limit_direction=None, limit_area=None, downcast=None, **kwargs)

method:表示使用的插值方法,该参数支持’linear’(默认值)、‘time’、 ‘index’、‘values’、 ‘nearest’ 、'barycentric’共6种取值,其中’linear’代表采用线性插值法进行填充;'time’代表根据时间长短进行填充;‘index’、'values’代表采用索引的实际数值进行填充;'nearest’代表采用最临近插值法进行填充;'barycentric’代表采用重心坐标插值法进行填充。 limit_direction:表示按照指定方向对连续的NaN进行填充。

线性插补:

2.1.5 缺失值处理案例

创建包含空缺值的DataFrame:

代码语言:javascript复制
import pandas as pd
import numpy as np
na_df = pd.DataFrame({'A':[1, 2, np.NaN, 4],
                          'B':[3, 4, 4, 5],
                          'C':[5, 6, 7, 8],
                          'D':[7, 5, np.NaN, np.NaN]})
na_df

输出为:

查看包含的空缺值

代码语言:javascript复制
# 使用isna()方法检测na_df中是否存在缺失值
na_df.isna()

输出为:

计算每列缺失值的总和:

代码语言:javascript复制
# 计算每列缺失值的总和
na_df.isnull().sum()

输出为:

看看缺失值所在的行:

代码语言:javascript复制
# 看看缺失值所在的行
na_df[na_df.isnull().T.any()]

输出为:

高亮缺失值:

代码语言:javascript复制
# 高亮缺失值
(na_df[na_df.isnull().T.any() == True]
.style
.highlight_null(null_color='skyblue'))

输出为:

删除缺失值 – 将缺失值出现的行全部删掉:

代码语言:javascript复制
# 删除缺失值 -- 将缺失值出现的行全部删掉
na_df.dropna()

输出为:

保留至少有3个非NaN值的行:

代码语言:javascript复制
# 保留至少有3个非NaN值的行
na_df = pd.DataFrame({'A':[1, 2, np.NaN, 4],
                          'B':[3, 4, 4, 5],
                          'C':[5, 6, 7, 8],
                          'D':[7, 5, np.NaN, np.NaN]})
na_df.dropna(thresh=3)

输出为:

缺失值补全|整体填充 将全部缺失值替换为 * :

代码语言:javascript复制
# 缺失值补全|整体填充 将全部缺失值替换为 *
na_df.fillna("*")

输出为:

缺失值补全 | 平均数填充到指定的列 :

代码语言:javascript复制
# 缺失值补全 | 平均数填充到指定的列
# 计算A列的平均数,并保留一位小数
col_a = np.around(np.mean(na_df['A']), 1)
# 计算D列的平均数,并保留一位小数
col_d = np.around(np.mean(na_df['D']), 1)
# 将计算的平均数填充到指定的列
na_df.fillna({'A':col_a, 'D':col_d})

输出为:

缺失值补全|上下均值填充:

代码语言:javascript复制
# 缺失值补全|上下均值填充
na_df.fillna(na_df.interpolate())

输出为:

缺失值补全 | 线性插值:

代码语言:javascript复制
# 缺失值补全 | 线性插值
na_df.interpolate(method='linear')

输出为:

2.2 重复值处理

2.2.1 重复值的检测

pandas中使用duplicated()方法来检测数据中的重复值。

代码语言:javascript复制
DataFrame.duplicated(subset=None, keep='first')

subset:表示识别重复项的列索引或列索引序列,默认标识所有的列索引。 keep:表示采用哪种方式保留重复项,该参数可以取值为’first’(默认值)、 'last '和 ‘False’,其中’first’代表删除重复项,仅保留第一次出现的数据项;'last '代表删除重复项,仅保留最后一次出现的数据项;'False’表示所有相同的数据都被标记为重复项。 duplicated()方法检测完数据后会返回一个由布尔值组成的Series类对象,该对象中若包含True,说明True对应的一行数据为重复项。

2.2.2 重复值的处理

重复值的一般处理方式是删除,pandas中使用drop_duplicates()方法删除重复值。

代码语言:javascript复制
DataFrame.drop_duplicates(subset=None, keep='first', inplace=False, 
                                               ignore_index=False)

keep:表示采用哪种方式保留重复项,该参数可以取值为’first’(默认值)、 'last ‘和’False’,其中’first’代表删除重复项,仅保留第一次出现的数据项;'last '代表删除重复项,仅保留最后一次出现的数据项;'False’表示删除所有的重复项。 inplace:表示是否放弃副本数据,返回新的数据,默认为False。 ignore_index:表示是否对删除重复值后的对象的行索引重新排序,默认为Flase。

2.2.3 重复值处理案例

创建DataFrame对象:

代码语言:javascript复制
# 创建DataFrame对象
import pandas as pd
import numpy as np

df = pd.DataFrame({'name': ['刘婷婷', '王淼', '彭岩', '刘华', '刘华', '周华'],
                'age': [24, 23, 29, 22, 22, 27],
                'height': [162, 165, 175, 175, 175, 178],
                'gender': ['女', '女', '男', '男', '男', '男']})
df

输出为:

duplicated用来检测df对象中的重复值,返回值为boolean数组

代码语言:javascript复制
# 检测df对象中的重复值
df.duplicated()   # 返回boolean数组

输出为:

查找重复值–将全部重复值所在的行筛选出来:

代码语言:javascript复制
# 查找重复值
# 将全部重复值所在的行筛选出来
df[df.duplicated()]

输出为:

查找重复值|指定列 :

代码语言:javascript复制
# 查找重复值|指定
# 上面是所有列完全重复的情况,但有时我们只需要根据某列查找重复值
df[df.duplicated(['gender'])]

输出为:

删除重复值 --删除全部的重复值

代码语言:javascript复制
# 删除重复值
# 删除全部的重复值

df.drop_duplicates()

输出为:

删除全部的重复值,但保留最后一次出现的值:

代码语言:javascript复制
# 删除重复值|指定
# 删除全部的重复值,但保留最后一次出现的值
df.drop_duplicates(keep = 'last')

输出为:

2.3 异常值处理

2.3.1 异常值的检测

异常值的检测可以采用 3σ原则 和 箱形图检测

2.3.1.1 3σ原则

3σ原则,又称为拉依达原则,它是先假设一组检测数据只含有随机误差,对该组数据进行计算处理得到标准偏差,按一定概率确定一个区间,凡是超过这个区间的误差不属于随机误差而是粗大误差,含有粗大误差范围内的数据(视为异常值)应予以剔除。 3σ原则并不适用于任意数据集,而只适用于符合或近似正态分布的数据集。 正态分布也称高斯分布,是统计学中十分重要的概率分布,它有两个比较重要的参数:μ和σ,其中μ是遵从正态分布的随机变量(值无法预先确定仅以一定的概率取值的变量)的均值,σ是此随机变量的标准差。

正态分布密度函数的特点是:关于μ对称,在μ处达到最大值,在正(负)无穷远处取值为0,在μ±σ处有拐点,呈现中间高两头低的形状 ,像一条左右对称的钟形曲线。

结合正态分布曲线图,3σ原则在各区间所占的概率如下: 数值分布在(μ-σ,μ σ)区间中的概率为68.2%。 数值分布在(μ-2σ,μ 2σ)区间中的概率为95.4%。 数值分布在(μ-3σ,μ 3σ)区间中的概率为99.7%。 大多数数值集中在(μ-3σ,μ 3σ)区间的概率最大,数值超出这个区间的概率仅占不到0.3%。所以,凡是误差超过(μ-3σ,μ 3σ)区间的数值均属于异常值。

正态分布检测: 在使用3σ原则检测异常值时,需要确保被检测的样本数据符合正态分布。那么,如何确定样本数据符合正态分布呢? 这里可以使用K-S(Kolmogorov-Smirnov)检测。 K-S检测是一个比较频率分布与理论分布或者两个观测值分布的检验方法,它根据统计量与P值对样本数据进行校验,其中统计量的大小表示与正态分布的拟合度。P值大于0.05,说明样本数据符合正态分布。 SciPy库中的kstest模块提供了基于K-S检测的功能。

代码语言:javascript复制
import scipy.stats as stats
data  = pd.read_excel('data.xlsx')                  
u = data['value'].mean()                                # 计算均值
std = data['value'].std()                                 # 计算标准差
stats.kstest(data['value'], 'norm', (u, std))     # 检测是否符合正态分布
2.3.1.2 箱形图检测异常值

除了使用3σ原则检测异常值之外,还可以使用箱形图检测异常值。需要说明的是,箱形图对检测数据没有任何要求,即使不符合正态分布的数据集是能被检测的。 箱形图是一种用于显示一组数据分散情况的统计图,它通常由上边缘、上四分位数、中位数、下四分位数、下边缘和异常值组成。箱形图能直观地反映出一组数据的分散情况,一旦图中出现离群点(远离大多数值的点),就认为该离群点可能为异常值。

Q3表示上四分位数,说明全部检测值中有四分之一的值比它大;Q1表示下四分位数,说明全部检测值中有四分之一的值比它小;IQR表示四分位数间距,即上四分位数Q3与下四分位数Q1之差,其中包含了一半检测值;空心圆点表示异常值,该值的范围通常为小于Q1 – 1.5IQR或大于Q3 1.5IQR

为了能够直观地从箱形图中查看异常值,pandas中提供了两个绘制箱形图的函数:plot()和boxplot(),其中plot()函数用于根据Series和DataFrame类对象绘制箱形图,该箱形图中默认不会显示网格线; boxplot()函数用于根据DataFrame类对象绘制箱形图,该箱形图中默认会显示网格线。

代码语言:javascript复制
DataFrame.boxplot(column=None, by=None, ax=None, fontsize=None, 
        rot=0, grid=True, figsize=None, layout=None, return_type=None, 
        backend=None, **kwargs)

rot:表示箱形图坐标轴旋转角度。 grid:表示箱形图窗口尺寸大小。 return_type:表示返回的对象类型,该参数取值可为’axes’ 、‘dict’和’both’。

如果需要从箱形图中获取异常值及其对应的索引,那么可以根据箱形图中异常值的范围计算,具体计算方式为:首先对数据集进行排序,然后根据排序后的数据分别计算Q1、Q3和IQR的值,最后根据异常值的范围(Q1 – 1.5IQR或大于Q3 1.5IQR)得出异常值。

在计算数据集的四分位数时,除了要先对数据集排序外,还要根据其中数据的总数量选择不同的计算方式:当数据的总数量为偶数时,数据集被中位数划分为个数相等(每组有n/2个)的两组数,其中第一组数的中位数为Q1,第二组数的中位数为Q3;当数据的总数量为奇数时,中位数会将数据集划分为个数相等(每组有 (n-1)/2 个)的两组数,其中第一组数的中数为Q1,第二组数的中数为Q3。

2.3.2 异常值的处理

构建数据:
代码语言:javascript复制
import pandas as pd
import numpy as np
data = {'name': ['user1', 'user2', 'user3', 'user4','user5','user6','user7','user8','user9','user10','user11', 'user12', 'user13', 'user14','user15','user16','user17','user18'],
        'old': [221, 21, 20, 19, 23,22,18,19,20,20, 19, 23,22,20, 19, 23,22,21],
        'weight':[121,122,132,135,128,124,129,133,362,135,128,124,129,135,128,124,129,73]
        }
columns1=['name', 'old','weight']
index1=['id1', 'id2', 'id3','id4','id5','id6','id7','id8','id9','id10','id11', 'id12', 'id13','id14','id15','id16','id17','id18']
df1= pd.DataFrame(data,columns=columns1,index=index1)
df1

输出为:

基于 3σ原则 进行异常值检测

判断是否符合正态分布:

代码语言:javascript复制
import scipy.stats as stats
data  = df1           
u = data['old'].mean()                              # 计算均值
std = data['old'].std()                             # 计算标准差
stats.kstest(data['old'], 'norm', (u, std))     # 检测是否符合正态分布

输出为:

P值小于0.05,说明样本数据符合正态分布。

为了演示,也依然使用 3σ原则 进行异常值检测:

代码语言:javascript复制
def three_sigma(ser):
    """
    :param ser: 被检测的数据,接收DataFrame的一列数据
    :return: 异常值及其对应的行索引
    """
    # 计算平均值
    mean_data = ser.mean()
    # 计算标准差
    std_data = ser.std()
    print("平均值mean_data:{},标准差std_data:{}".format(mean_data,std_data))
    #小于μ-3σ或大于μ 3σ的数值均为异常值
    rule = (mean_data-3*std_data>ser) | (mean_data 3*std_data<ser)
    # 返回异常值的行索引
    index = np.arange(ser.shape[0])[rule]
    # 获取异常值
    outliers = ser.iloc[index]
    return outliers

# 对df1的old列进行异常值检测
three_sigma(df1['old'])

输出为:

删除指定索引的行后,查看异常值情况:

代码语言:javascript复制
df1_drop = df1.drop(['id1'])
three_sigma(df1_drop['old'])

输出为:

基于箱型图进行异常检测

查看数据

代码语言:javascript复制
import pandas as pd
df1

输出为:

绘制箱型图,查看有无异常值:

代码语言:javascript复制
import matplotlib.pyplot as plt
import matplotlib

%matplotlib inline
font = {
    'family':'SimHei',
    'weight':'bold',
    'size':12
}
matplotlib.rc("font", **font)
matplotlib.rcParams['axes.unicode_minus']=False
df1.boxplot(column='old')
plt.show()

输出为:

返回异常值:

代码语言:javascript复制
import pandas as pd
import numpy as np
def box_outliers(ser):
    # 对待检测的数据集进行排序
    new_ser = ser.sort_values()
    # 判断数据的总数量是奇数还是偶数
    if new_ser.count() % 2 == 0:
        # 计算Q3、Q1、IQR
        Q3 = new_ser[int(len(new_ser) / 2):].median()
        Q1 = new_ser[:int(len(new_ser) / 2)].median()
    elif new_ser.count() % 2 != 0:
        Q3 = new_ser[int((len(new_ser)-1) / 2):].median()
        Q1 = new_ser[:int((len(new_ser)-1) / 2)].median()
    IQR = round(Q3 - Q1, 1)
    ma = round(Q3 1.5*IQR, 1)
    mi = round(Q1-1.5*IQR, 1)
          
    rule = (ma < ser)|(mi > ser)
    print("下限为{},上限为{}".format(mi,ma))
    index = np.arange(ser.shape[0])[rule]
    # 获取异常值及其索引
    outliers = ser.iloc[index]
    return outliers

box_outliers(df1['old'])

输出为:

删除指定索引后,查看异常值情况:

代码语言:javascript复制
df1_drop = df1.drop(['id1'])
box_outliers(df1_drop['old'])

输出为:

基于替换的方式处理异常值: 上面看到了最大值和最小值为26.5和14.5

代码语言:javascript复制
topnum1 = 26.5
bottomnum1 = 14.5

replace_value1=df1['old'][df1['old']<26.5].max()
df1.loc[df1['old']>topnum1,'old']=replace_value1
replace_value2=df1['old'][df1['old']>bottomnum1].min()
df1.loc[df1['old']<bottomnum1,'old']=replace_value2

df1

输出为:

可以看到 id1的异常值被替换为23了。 再次判断异常值:

代码语言:javascript复制
print(box_outliers(df1['old']))
df1.boxplot(column='old')

输出为:

替换采用replace函数:
代码语言:javascript复制
data = {'name': ['user1', 'user2', 'user3', 'user4','user5','user6','user7','user8','user9','user10','user11', 'user12', 'user13', 'user14','user15','user16','user17','user18'],
        'old': [221, 21, 20, 19, 23,22,18,19,20,20, 19, 23,22,20, 19, 23,22,21],
        'weight':[121,122,132,135,128,124,129,133,362,135,128,124,129,135,128,124,129,73]
        }
columns1=['name', 'old','weight']
index1=['id1', 'id2', 'id3','id4','id5','id6','id7','id8','id9','id10','id11', 'id12', 'id13','id14','id15','id16','id17','id18']
df1= pd.DataFrame(data,columns=columns1,index=index1)
df1.head(3)

输出为:

查看异常值:

代码语言:javascript复制
box_outliers(df1['old'])

输出为:

替换异常值:

代码语言:javascript复制
# 替换异常值
# 替换异常值
print(df1['old']['id1'])
print('-'*10)
df1['old'] = df1['old'].replace({221:23})
# 根据行索引获取替换后的值
df1.loc['id1']

输出为:

替换异常值后,查看异常情况:

代码语言:javascript复制
box_outliers(df1['old'])

输出为:

0 人点赞