0%

基于 Pandas 的数据清洗

我们进行数据分析时,得到的原始数据往往会有一些不合理的地方。比如,因为取样原因可能出现一些空值,重复取样可能会有重复数据,取样错误可能会有异常数据,等等。

这些不合理的数据是会影响我们数据分析的准确性的。而明显不合理的数据往往占少数,所以一般我们对其处理的方式是直接删除。对于大数据分析而言,少量的数据丢失不必心疼。没有它们,计算反而更准确。

在开始之前,导入各种需要的包,并按照惯例取别名:

1
2
3
import numpy as np
import pandas as pd
from pandas import Series, DataFrame

处理空值

在获取到的数据中,表示为空值的数据可能有两种:Nonenp.nan

1
2
type(None)    # NoneType
type(np.nan) # float

通过 type 查看它们的类型,我们可以知道,None 为 NoneType 类型,而 np.nan 为浮点型。浮点型比 NoneType 类型好的一方面是,浮点型可以进行数学运算,而 NoneType 不行。

1
2
None + 1    # 报错
np.nan + 1 # nan

DataFrame 中的空值都是 np.nan,是可以进行数学运算的。即便如此,运算后的结果仍然为空,还是没有意义的。所以我们需要对空值进行处理。

Pandas 处理空值,主要会用到下面几个方法:

  • isnull
  • notnull
  • any
  • all
  • dropna,删除含有空值的行/列
  • fillna,替换空值

首先,我们创建一个 7 行 5 列的 DataFrame,然后将其中几个数据替换为空值。

1
2
3
4
5
df = DataFrame(data=np.random.randint(0, 100, size=(7, 5)))
df.iloc[2, 4] = None
df.iloc[4, 2] = np.nan
df.iloc[6, 1] = None
df

创建出来的 DataFrame 为:

1
2
3
4
5
6
7
8
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
2 39 56.0 83.0 24 NaN
3 38 35.0 90.0 64 82.0
4 45 40.0 NaN 68 35.0
5 49 41.0 81.0 17 76.0
6 30 NaN 78.0 25 70.0

我们看到,即便我们用 None 赋值,在 DataFrame 中的数据仍然为 NaN。

现在,我们的数据中出现了空值。一般情况,如果控制不多,且不容易指定具体的值的情况下,我们会删除空值所在行。空值列不可轻易删除,因为列中的数据往往很多,且有其独特含义。

通过过滤删除空值所在行

我们可以搭配使用 isnull、notnull、any 和 all 方法,来实现空值所在行的查找和过滤。

isnull + any 过滤空值所在行

isnull,用来判断 DataFrame 中的元素是否为空。any 用来判断当行/列中是否有一个值为 True。

我们先使用 isnull,再对行使用 any 方法,即可检查改行是否有空值:

1
df.isnull().any(axis=1)

any(axis=1),如果返回的值为 True 意味着 any 作用到的 df 中的该行中至少有一个 True;如果返回的值为 False 则意味着 any 作用到的 df 中该行全部为 False。

返回的结果为:

1
2
3
4
5
6
7
8
0    False
1 False
2 True
3 False
4 True
5 False
6 True
dtype: bool

数据中的第 2、4、6 行含有空值,显示为 True,其他皆为 False。

我们可以通过上面运算得到的布尔值作为索引,获取到所有空值所在行:

1
df.loc[df.isnull().any(axis=1)]

输出的结果为:

1
2
3
4
	0	1	2	3	4
2 39 56.0 83.0 24 NaN
4 45 40.0 NaN 68 35.0
6 30 NaN 78.0 25 70.0

我们成功拿到了空值所在行的数据。

不过等一下,好像有点不太对劲。我们不是要拿到空值所在行,而是要删除空值所在行。我们要的是一行中所有元素都非空的正常数据。

要做到这一点其实很简单,只需要对上面的表达式进行取反即可:

1
df.loc[~df.isnull().any(axis=1)]

不过除此之外,我们还可以通过找到空值所在行的索引,然后将他们删除掉:

1
2
drop_index = df.loc[df.isnull().any(axis=1)].index    # 提取出空值所在行的索引
df.drop(labels=drop_index, axis=0) # 对于drop系列函数,0表示行,1表示列

我们顺利拿到不含空值的所有行数据:

1
2
3
4
5
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
3 38 35.0 90.0 64 82.0
5 49 41.0 81.0 17 76.0

notnull + all 过滤空值所在行

notnull,用来判断 df 数据是否为非空。all 用来判断 df 数据的某一行/列是否全为 True。

与 isnull 搭配 any 使用的方法类似,通过 notnull 与 all 的搭配,我们可以很容易判断每一行是否含有空值:

1
df.notnull().all(axis=1)

返回的结果为:

1
2
3
4
5
6
7
8
0     True
1 True
2 False
3 True
4 False
5 True
6 False
dtype: bool

通过布尔值索引,很容易拿到不含空值的行:

1
df.loc[df.notnull().all(axis=1)]

结果也很成功:

1
2
3
4
5
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
3 38 35.0 90.0 64 82.0
5 49 41.0 81.0 17 76.0

dropna 方法清洗空值所在行

因为清洗控制的操作太过常见,DataFrame 提供了 dropna 方法,可以直接删除控制所在的行或者列:

1
df.dropna(axis=0)

只一条数据,即可实现上面的复杂操作,结果依然正确:

1
2
3
4
5
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
3 38 35.0 90.0 64 82.0
5 49 41.0 81.0 17 76.0

fillna 方法替换空值

有的时候,比如空值很多很分散,如果删除所有空值所在行可能会丢失大量数据。又或许这个空值本身不是很重要,如果因为它而删除本行内掉其他有意义的数据,得不偿失。还有的时候,数据本身变化不大,使用本列前一个或者后面一个数值填充进来不会影响结果。

如果出现了上面的情况,我们往往不会将空值所在行直接删除,而是会替换空值。

使用 fillna 可以进行空值替换。

我们可以把空值替换为指定的值:

1
df.fillna(value=666)

从结果中可以看见,所有的空值都替换成了 666:

1
2
3
4
5
6
7
8
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
2 39 56.0 83.0 24 666.0
3 38 35.0 90.0 64 82.0
4 45 40.0 666.0 68 35.0
5 49 41.0 81.0 17 76.0
6 30 666.0 78.0 25 70.0

当然,更多时候,我们不会给每个空值都替换成一个相同的值,而是会使用空值的临近值填充空值。

通常情况,不同列之间的数据是不同的,所以我们往往使用的是同一列的数据填充空值。可以选择空值前或者后的数据填充到空值位置。

比如,使用空值前面的数据填充空值:

1
df.fillna(method='ffill', axis=0)

我们发现,所有的空值都填充上了它同一列的前一个元素的值:

1
2
3
4
5
6
7
8
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
2 39 56.0 83.0 24 72.0
3 38 35.0 90.0 64 82.0
4 45 40.0 90.0 68 35.0
5 49 41.0 81.0 17 76.0
6 30 41.0 78.0 25 70.0

同时,我们还可以使用空值后面的数据填充空值,只需把 ffill 改成 bfill 即可:

1
df.fillna(method='bfill', axis=0)

结果是这样的:

1
2
3
4
5
6
7
8
	0	1	2	3	4
0 96 63.0 82.0 42 76.0
1 82 17.0 89.0 1 72.0
2 39 56.0 83.0 24 82.0
3 38 35.0 90.0 64 82.0
4 45 40.0 81.0 68 35.0
5 49 41.0 81.0 17 76.0
6 30 NaN 78.0 25 70.0

结果大部分还是正常的,只是最后一列的空值,并没有被替换掉。这也很好理解,毕竟最后一列的后面没有数据,也就无从填充了。

处理重复数据

有些时候,可能会出现重复采样的情况。这时候,就会产生一些重复值。如果使用这些带有重复值的数据进行分析,有可能同一个样本的数据会对数据整体结果产生影响。也就是单条数据的权重是不同的。

这时候,我们可以将重复数据处理掉,只保留一条。

使用命令 dropduplicates 即可实现这一操作。

首先,创建一个有重复数据的 DataFrame:

1
2
3
4
5
df = DataFrame(data=np.random.randint(0, 100, size=(7, 4)))
df.iloc[1] = [6, 6, 6, 6]
df.iloc[4] = [6, 6, 6, 6]
df.iloc[6] = [6, 6, 6, 6]
df

生成的数据中,有三行重复数据:

1
2
3
4
5
6
7
8
	0	1	2	3
0 13 66 29 45
1 6 6 6 6
2 78 55 70 68
3 51 38 18 61
4 6 6 6 6
5 63 43 62 46
6 6 6 6 6

使用命令 dropduplicates 去除重复列,keep 参数可以指定保留哪一条数据,比如 first 保留第一个重复数据:

1
df.drop_duplicates(keep='first')

只有第一行的重复元素被保留了:

1
2
3
4
5
6
	0	1	2	3
0 13 66 29 45
1 6 6 6 6
2 78 55 70 68
3 51 38 18 61
5 63 43 62 46

如果将 keep 指定为 last:

1
df.drop_duplicates(keep='last')

将只有最后一行重复元素被保留:

1
2
3
4
5
6
	0	1	2	3
0 13 66 29 45
2 78 55 70 68
3 51 38 18 61
5 63 43 62 46
6 6 6 6 6

如果将 keep 设置为 False:

1
df.drop_duplicates(keep=False)

则不保留任何重复数据:

1
2
3
4
5
	0	1	2	3
0 13 66 29 45
2 78 55 70 68
3 51 38 18 61
5 63 43 62 46

处理异常数据

异常数据是因为各种偶然因素导致的数据过度波动。我们通常使用方差或者标准差来判定一个数据是否超过合理的波动范围而应该舍弃。

示例:自定义一个 1000 行 3 列(A,B,C)取值范围为 0-1 的数据源,然后将 C 列中的值大于其两倍标准差的异常值进行清洗。

首先,创建一个数据源,random.random 会在 0-1 之间随机取值:

1
2
df = DataFrame(data=np.random.random(size=(1000, 3)), columns=['A', 'B', 'C'])
df

部分数据为:

1
2
3
4
5
6
7
8
9
	A	B	C
0 0.227949 0.289083 0.249417
1 0.216621 0.309894 0.758762
2 0.327888 0.519255 0.439639
... ... ... ...
997 0.704879 0.035970 0.042757
998 0.627836 0.570444 0.149310
999 0.621530 0.250346 0.482654
1000 rows × 3 columns

判断 C 列中的值是否大于其两倍标准差:

1
df['C'] > 2 * df['C'].std()

对其取反作为索引,即可将所有波动过大的异常值清理掉:

1
df.loc[~(df['C'] > 2 * df['C'].std())]

获取到的数据就是波动很小的了:

1
2
3
4
5
6
7
8
9
10
11
	A	B	C
0 0.227949 0.289083 0.249417
2 0.327888 0.519255 0.439639
3 0.582895 0.589447 0.085444
4 0.877022 0.956978 0.349829
... ... ... ...
995 0.992804 0.741321 0.571399
997 0.704879 0.035970 0.042757
998 0.627836 0.570444 0.149310
999 0.621530 0.250346 0.482654
558 rows × 3 columns