教学中的数据和实际数据的区别在于,实际数据很少是干净整齐的。许多有趣的数据集都有某种程度上的数据缺失。更糟糕的是,不同的数据源数据丢失的方式也不同。
本章,我们将会探讨对缺失数据的一般性考虑,讨论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数据结构中的空值。它们是:
-
isnull()
: 生成布尔掩码用来指示缺失的数据 -
notnull()
:isnull()
的反操作 -
dropna()
: 返回数据过滤后的版本 -
fillna()
: 返回缺失数据被填充后的数据拷贝
我们将对这些函数进行简要探索和示范,然后结束这一部分。
空值检测
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值将会保留。
网友评论