4.处理缺失数据

2018-08-24  本文已影响0人  michael的自由空间

教学中的数据和实际数据的区别在于,实际数据很少是干净整齐的。许多有趣的数据集都有某种程度上的数据缺失。更糟糕的是,不同的数据源数据丢失的方式也不同。
本章,我们将会探讨对缺失数据的一般性考虑,讨论Pandas是怎样选择表达的,我们也会演示几种用来处理缺失数据的Pandas内置工具。在整本书中,我们一般将缺失数据称为NULL、NA或NA值。

对缺失数据约定的权衡

对于在表格和DataFrame中缺失数据的表示有好几种方案。通常情况下,它们都是基于两种策略:使用一个用于全局指示缺失值的掩码,或者选择一个表示缺失条目的哨兵值。
使用掩码方法,掩码可能是一个独立完整的布尔数组,或者在数据表达中引入一个适当的bit位来局部的指示空值状态。
使用哨兵方法,哨兵值可以是一些约定好的特殊数值,比如为表示一个缺失的整型数值,可以使用-9999或少见的bit模式,也可以是更全局性的约定,例如使用NaN来表示一缺失的浮点型数值,NaN是IEEE浮点规范中的一个特殊值。
所有的方法都需要一些权衡:使用独立的掩码数组需要分配额外的布尔数组空间,这将对储存和计算都增加负担。哨兵值减少了可以有效表达的数据范围,也可能在CPU和GPU的计算时需要另外的逻辑。像NaN这样普通的特殊值并不是对所有数据类型都适用。
在大多数情况下,并没有一个普遍的最优选择存在,不同的语言和系统使用不同的约定。例如R语言在每种数据类型里面使用保留的bit模式来表明缺失的数据,而SciDB系统对每个单位附加额外的字节来指示缺失状态。

Pandas上的数据缺失

Pandas处理缺失数据的方法受到它所依赖的NumPy包的约束,NumPy里面对非浮点型数据并没有内置的缺失值表达。
Pandas可以采用R语言的bit模式来标记各种数据类型的空值,但这种方法被证明是相当的不方便。R语言只包含四种基本数据类型,而NumPy支持的数据类型要多得多:例如,R有一个但整型类型,但考虑到可用精度,符号性和字节的编码顺序,NumPy支持基本整型类型多达14种。为所有可用的NumPy类型保留一个bit模式,将会导致大量的负担来处理不同类型的不同操作,很有可能需要NumPy包引出一个新的分支来支持。另外,对于小规模的数据类型(如8位的整数),牺牲其中一个bit作为掩码,将会显著的减少它所能表示的数据范围。
NumPy也不支持掩码数组,就是那种用来表示是好数据还是坏数据的独立布尔数据。Pandas可以继承这些,但是存储空间,计算和代码维护的额外代价使这种方法不使一个吸引人的选项。
考虑到这些限制,Pandas选择使用哨兵值来表示缺失数据,并且选用了两种Python里面已经存在的空值:特殊的浮点值NaN和Python的None对象。如我们将看到的,这种选择有一些副作用,但实际上它是在考虑到各种利益情况下的一个较好的折中。

None:Python式的缺失数据

Pandas使用的第一哨兵值是None,一个通常用在Python代码种表示缺失的单态对象。因为它是一个Python对象,None不能用在任何专属的NumPy/Pandas数组中,而只能用在对象类型位‘object’的数组中:

import numpy as np
import pandas as pd
vals1 = np.array([1, None, 3, 4])
vals1
array([1, None, 3, 4], dtype=object)

dtype=object意味着从NumPy数组中可以推断出最通用的数据类型是Python对象。尽管这种对象数组在某些情况下有用,但任何数据操作都是在Python层面的,这种操作比对原生类型的快速操作要慢很多。

for dtype in ['object', 'int']:
    print("dtype =", dtype)
    %timeit np.arange(1E6, dtype=dtype).sum()
    print()
dtype = object
10 loops, best of 3: 78.2 ms per loop

dtype = int
100 loops, best of 3: 3.06 ms per loop

在数组种使用Python 对象意味着如果要在带有None值的数组上执行聚合操作如sum()或min(),通常会得到错误:

vals1.sum()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-4-749fd8ae6030> in <module>()
----> 1 vals1.sum()


/Users/jakevdp/anaconda/lib/python3.5/site-packages/numpy/core/_methods.py in _sum(a, axis, dtype, out, keepdims)
     30 
     31 def _sum(a, axis=None, dtype=None, out=None, keepdims=False):
---> 32     return umr_sum(a, axis, dtype, out, keepdims)
     33 
     34 def _prod(a, axis=None, dtype=None, out=None, keepdims=False):


TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

NaN:缺失数值数据

另一个缺失数据表示,NaN,就有些不同;它是一个能够被所有使用IEEE浮点表达式系统识别的特殊的浮点值。

vals2 = np.array([1, np.nan, 3, 4]) 
vals2.dtype
dtype('float64')

注意NumPy为这个数组选择一个原生的浮点类型:这意味着与之前的的对象数组不同,这个浮点数组支持在编译代码中的快速操作。你可能意识到NaN有点像数据病毒--它能感染任何它所接触的对象。无论是什么操作,与NaN计算的结果都是NaN:

1 + np.nan
nan
0 *  np.nan
nan

注意,这意味着对那些数据的聚合操作都是有效的(它们不会产生错误),只是没什么用处:

vals2.sum(), vals2.min(), vals2.max()
(nan, nan, nan)

NumPy提供了一些特殊的聚合方法来忽略那些缺失的数据:

np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
(8.0, 1.0, 4.0)

记住NaN是一个特殊的浮点值;对于整型,字符串和其他类型并没有对应的NaN值:

Pandas中的NaN和None

NaN和None在Pandas中都有使用,但对它们的处理被设计成几乎可以互换,在合适的情况下对它们进行转换:

pd.Series([1, np.nan, 2, None])
0    1.0
1    NaN
2    2.0
3    NaN
dtype: float64

对于那些没有哨兵值的类型,但空值出现时,Pandas会自动的进行类型转换。例如,我们给一个整型数组中的值设置为np.nan,整个数组将会自动的向上转型为浮点类型来适应NA值:

x = pd.Series(range(2), dtype=int)
x
0    0
1    1
dtype: int64
x[0] = None
x
0    NaN
1    1.0
dtype: float64

注意除了把整型数组转换为浮点型,Pandas也自动的把None转换为NaN值
这种转换与特定语言(如R语言)的统一的方法比起来看上去不那么优雅,但在实际的使用过程中,Pandas的哨兵/转换方法工作的相当好,很少导致问题。
下表列出了Pandas中当出现NA值时的转型约定:

Typeclass Conversion When Storing NAs NA Sentinel Value
floating No change np.nan
object No change None or np.nan
integer Cast to float64 np.nan
boolean Cast to object None or np.nan

记住,在Pandas上,字符串数据总时被存储为对象类型。

空值操作

如我们看到的,Pandas把None和NaN在表示缺失或空值时当成时基本上可互换的。为了简化这种约定,有几个有用的方法用于检测,移除以及替换Pandas数据结构中的空值。它们是:

我们将对这些函数进行简要探索和示范,然后结束这一部分。

空值检测

Pandas数据结构有两个有用的方法用于检测空值:isnull()和notnull()。两者都在数据上返回布尔掩码。例如:

data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
0    False
1     True
2    False
3     True
dtype: bool

我们在 Data Indexing and Selection 提到过,布尔掩码也可以直接被用作Series或FataFrame的索引:

data[data.notnull()]
0        1
2    hello
dtype: object

isnull()和notnull()方法对于DataFrame产生相似的布尔结果。

去掉空值

除了之前用过的掩码手段,还有很方便的函数,dropna(用于移除NA值)和fillna(填充NA值)。对于Series,结果非常直观:

data.dropna()
0        1
2    hello
dtype: object

对于DataFrame,会有更多选项。比如下面的DataFrame:

df = pd.DataFrame([[1,      np.nan, 2],
                   [2,      3,      5],
                   [np.nan, 4,      6]])
df
0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6

我们不能DataFrame中的单个值;我们只能去掉整行或整列。在不同的应用环境,你可能想用不同的方式,所以dropna()为DataFrame提供了许多选项。
默认情况下,dropna()将会去掉包含空值的所有行和列:

df.dropna()
0 1 2
1 2.0 3.0 5

或者,你可以沿着不同的轴去掉NA值;axis=1会去掉所有包含空值的列:

df.dropna(axis='columns')
2
0 2
1 5
2 6

但是这种方法也去掉了一些好的数据;你可能想只去掉只含有NA值的行或列,或者大部分是NA值的行或列。可以通过指定参数how或者thresh来精确的控制允许的空值数目。
how的默认值是‘any’,因此任何包含空值的行或列都会被去掉。你可以指定how=‘all’,这样就只是会去掉全部是空值的行/列:

df[3] = np.nan
df
0 1 2 3
0 1.0 NaN 2 NaN
1 2.0 3.0 5 NaN
2 NaN 4.0 6 NaN
df.dropna(axis='columns', how='all')
0 1 2
0 1.0 NaN 2
1 2.0 3.0 5
2 NaN 4.0 6

为了细粒度的控制,thresh参数允许你规定可以保留行/列所需要的最少非空数据数目:

df.dropna(axis='rows', thresh=3)
0 1 2 3
1 2.0 3.0 5 NaN

第一行和最后一行被去掉了,因为它们只包含两个非空数据。

填充空值

有时候与其去掉NA值,我们宁愿把它们换成有效的值。这个值可能是像0那样的单个数字,或者有效值的插值。可以通过使用isnull()方法作为过滤条件来原地替换,但是因为这个操作很常用,Pandas提供了fillna()方法,它可以返回空值被替换后的数组拷贝。
考虑如下Series:

data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))
data
a    1.0
b    NaN
c    2.0
d    NaN
e    3.0
dtype: float64

我们可以用单个数值如0来替换空值:

data.fillna(0)
a    1.0
b    0.0
c    2.0
d    0.0
e    3.0
dtype: float64

我们可以指定前值填充方法来使用空值前面的数据作为替换:

# forward-fill
data.fillna(method='ffill')
a    1.0
b    1.0
c    2.0
d    2.0
e    3.0
dtype: float64

或者我们可以指定后值填充方法使用空值后面的值作为替换值:

# back-fill
data.fillna(method='bfill')
a    1.0
b    2.0
c    2.0
d    3.0
e    3.0
dtype: float64

对于DataFrames,这些选项是类似的,但我们也可以指定发生填充的轴向:

df
0 1 2 3
0 1.0 NaN 2 NaN
1 2.0 3.0 5 NaN
2 NaN 4.0 6 NaN
df.fillna(method='ffill', axis=1)
0 1 2 3
0 1.0 1.0 2.0 2.0
1 2.0 3.0 5.0 5.0
2 NaN 4.0 6.0 6.0

注意在前向填充中,如果前值不可用,NA值将会保留。

上一篇下一篇

猜你喜欢

热点阅读