处理缺失数据
Python 数据科学手册(抢先版)

许多教学中的数据和现实世界中的数据的区别在于,现实世界的数据很少干净且均匀分布的。特别是在许多有趣的数据集中,有着大量的缺失数据。让事情变得更加复杂的是,不同的数据来源可能会出现不同的形式的缺失数据。

在这一章中,我们将会讨论处理缺失数据的一些常见思路。讨论Pandas是如何表达缺失数据的,并展示Python中内置的一些用来处理缺失数据的Pandas工具。在本中,我们通常将“null”, “NaN”, or “NA” 这些值认为是缺失数据。

处理缺失数据常用方法的利弊权衡

人们开发了许多方案用来在一组数据中表示缺失的数据的存在。一般来说,它们围绕以下这两种策略:用一个掩码来全局的表示缺失的值,或者选择一个哨兵值来表示缺失记录。

在掩码方法中,掩码可以是一个完全独立的布尔数组,或者在数据表示中以一个独立的位来表明该值为空的状态。

在哨兵值的方法中,哨兵值可以使一些特定数据类型惯用的值,比如用-9999或者罕见的位模式表明缺失的整数值。哨兵值也可以是更加普遍的惯用值,比如说用NaN(Not a Number——不是一个数字,IEEE浮点数规约中的一个特殊的值)来表明一个缺失的浮点数值。

这些方法中没有一个是不进行折衷权衡的:使用一个单独的掩码数组就需要分配一个额外的布尔数组,这将增加存储和计算的开销。而哨兵值降低了可表示的合法值的范围,并且可能需要CPU和GPU额外的(通常是非优化的)算法逻辑。像NaN这样的通用特殊值并不是对于所有的数据类型的可用的。

和许多没有全局最优解的情况类似,不同的开发语言和系统使用不同的传统。举例来说,R语言使用每个数据类型中保留的位模式作为哨兵值来表示缺失的数据,而SciDB系统则为每一个数据点附上额外的一个字节表示数据缺失的(NA)状态。

Pandas中缺失数据的处理

Pandas对于如何处理缺失值的选择受限于它对于NumPy包的依赖。NumPy中没有对于非浮点数数据类型缺失值的内置表示。

Pandas本可以像R语言一样为每一个单独的数据类型使用特定的位模式来表示空值,但是这一方法被证明在Pandas中是相当笨重且难以实现。R语言仅包含四中基本数据类型,但NumPy支持的远多于这些:比如说,R语言只有一个整数类型,但是如果考虑到可用精度、符号和编码,NumPy支持十四种基本整数类型。如果为所有的NumPy中的数据类型都保留一个特定的位模式,这将会导致对各种数据类型的各种特定场景的操作的开销大大增加,并且其实现很有可能需要新建一个NumPy包。

NumPy是支持掩码数组的,比如说数组中附有一个单独的布尔掩码数组用来将数据标记为“好”和“坏”。Pandas本来可以沿用这一特性,但是存储、计算和代码维护上的开销使得这一选择根本没有吸引力。

考虑到这些限制,Pandas的实现选择为缺失数据赋予哨兵值,并且进一步选择使用两个在Python中已经存在的空值:特殊的浮点数空值NaN和Python的None对象。这一选择也有一些副作用,我们在下文中会提道,但是在实际应用中的大部分场景下都是一个不错的折衷之选。

None:Python式的缺失数据处理

Pandas首先使用的哨兵值是None。None是一个Python中的单例对象,在Python的代码中经常被用于表示缺失的数据。由于这是一个Python对象,它不是能被NumPy/Pandas中随意一个数组使用,它只能在数据类型为“Object”的数据中使用。(比如说类型为Python对象的数组)

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

这里的dtype=object表示NumPy根据数组内容推断出的最匹配的数组元素类型就是它们是Python对象。尽管这类对象数组在某些情况下是比较适用的,但是任何对于数据的运算将在Python层面上完成,这将会比基于原生类型数组的传统的快速运算有更多的开销。

for dtype in ['object', 'int']:
print("dtype =", dtype)
%timeit np.arange(1E6, dtype=dtype).sum()
print()

在一个数组中使用Python对象也意味着如果你在一个有None值的数组上执行诸如sum()和min()的聚合运算,通常情况下会报错。

vals1.sum()

这是因为Python中将一个整数和None相加的结果是undefined。

NaN: 缺失的数值型数据

另一个缺失数据的表示NaN(“Not a Number”,不是一个数字的缩写)与前面提到的None有所不同,这是一个特殊浮点数值。它可以被所有使用标准IEEE浮点数值表示方法的系统所识别。

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

这里可以注意到,NumPy为这个数组选择的是原生的浮点数类型。这意味着不像上面的对象数组,这个数组支持在已编译代码中的那些快速运算。你应该要意识到NaN像一个数据病毒,它会影响到任何其他它接触到的对象。不管是什么运算,任何有NaN参与的运算结果将会是另外一个NaN。

1 + np.nan
0 *  np.nan

请注意,这意味着对包含NaN值的求和或求最大值等操作是明确定义的(不会导致一个错误),但也不是很有用的。

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

请记住,NaN是一个特别的浮点数值,在整型、字符串或其他类型中并不存在等效的值。

一些例子

以上每一个哨兵值表示方法都有其用武之地,Pandas中对于这两者的处理几乎是可以交替互换的,会根据合适的场景在两个哨兵值之间互相转换。

data = pd.Series([1, np.nan, 2, None])
data

记住,尽管None是一个Python对象类型的数据缺失表示、NaN是浮点型的数据缺失表示,但在Pandas中字符串、布尔值或整数值并没有对应类型的缺失值的表示。当这些类型需要表示数据缺失时,Pandas通过类型转换来解决这个问题。例如,如果我们将一个整数数组中的值设为np.nan,该数组将会自动被转换为浮点型,从而使得NaN可以适用于数组。

x = pd.Series(range(2), dtype=int)
x[0] = None
x

需要注意的是,除了将整数数组转化为浮点类型,Pandas还会自动将None转化为NaN。尽管与像R语言这样的特定领域语言中所使用的更加统一的方法相比,Pandas的这种处理会令人感到有点随性,但是在实际应用中,Pandas的哨兵(类型转换)方法很好用,并且以我个人使用经验来说,很少出问题。

类型 当存储缺失数据时的类型转换 缺失数据对应的哨兵值
floating浮点值 无改变 np.nan
Objcet

对象

无改变 None 或者 np.nan
Integer整型 转化为 64位浮点数float64 np.nan
Boolean布尔值 转化为 对象Objcet None 或者 np.nan

请记住,在Pandas中,字符串数据总是以Python对象类型来存储的。

空值运算

我们已经看到,Pandas将None和NaN按需交替使用来表示缺失或者空值。为了充分发挥这一习惯用法的作用,在Pandas中有几个有用的方法来检测、去除和替代Pandas数据结构中的空值,它们是:

• isnull(): 生成一个布尔掩码数组来标示出缺失的值

• notnull(): 与isnull()的作用相反

• dropna(): 返回一个过滤缺失值/空值后的数据

• fillna(): 返回一个数据拷贝,其中的缺失值都已被填充或者估算

检测空值

Pandas数据结构中有两种检测空数据的有用的方法:isnull() 以及 notnull()。

这两个都会返回一个基于数据的布尔类型的标记数组,例如:

data = pd.Series([1, np.nan, 'hello', None])
data.isnull()

正如在第X章提到的,布尔掩码数组可以直接作为一个Series或者DataFrame的索引值

data[data.notnull()]

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

处理空值

除了上面用到的掩码方法之外,还有其他简便的方法,dropna() 和 fillna()。这两个方法分别用于去除缺失数据和填充缺失数据。对于一个Series来说,其结果是显而易见的:

data.dropna()

而对于一个DataFrame来说,还可以有更多的选择。如下面这个DataFrame:

df = pd.DataFrame([[1, np.nan, 2],
[2, 3, 5],
[np.nan, 4, 6]])
df

我们不能从DataFrame中把每一个单独的值去除掉,我们只能去除整行或者整列。根据不用的应用,你可能会想选择两者之一。所以说dropna()方法为DataFrame提供了不少选择。

默认情况下,dropna()方法将会去除所有数据中包含空值的行。

df.dropna()

或者,你可以基于不同的轴(axis)来去除缺失值:axis=1 将会去除所有包含空值的列。

df.dropna(axis=1)

但是这样也会把好的数据也除去了。你可能对于去除行或列值均为空(或大部分为空)的那些数据感兴趣。这可以通过how和thresh参数来进行指定,它们允许你对可以通过的空值的数量进行精确的控制。

how参数的默认值是‘any’,使得任何包含空值的行或者列(取决于轴关键字)将会被剔除。你也可以将how指定为‘all’,这样的话只有所有值都为空的行和列才会被去除。

df[3] = np.nan
df
df.dropna(axis=1, how='all')

请记住,为了使得代码更为清晰易懂,你可以使用axis=’rows’ 而不是 axis=0 , 以及axis=’columns’ 而不是 axis=1.

为了更加细粒度的控制,thresh参数让你可以指定保留的行或者列的最小非空值数。

df.dropna(thresh=3)

这里第一行和最后一行被去除了,因为它们仅包含2个非空值。

填充空值

有的时候与其去掉空值,你更愿意用一个合法的值来进行替代。这个值可能是一个数,比如零。或者它可能是某种形式的基于合法值的插值或插补。你可以直接用isnull()方法作为掩码来做到这一点,但因为这种操作太常见了,Pandas提供了fillna()方法,它会返回一个数组的拷贝,其中的空值已被替换。

Consider the following Series:参考下面这个Series:

data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))</p>
data

我们可以用一个单独的值来填充缺失值,比如说零。

data.fillna(0)

我们可以指定一个前置值填充,将前一个值传到空值处填充。

# forward-fill
data.fillna(method='fill')

或者我们可以指定一个后置填充,将下一个值传到空值处填充。

# back-fill
data.fillna(method='bill')

对于DataFrame来说,其选择是相似的,但是我们还可以指定一整个轴的值来进行填充。

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

请注意,如果前一个值在前置填充的过程中不可用,那么缺失值将仍然保留。

总结

本文中,我们看到了Pandas是如何处理空值/缺失值的,以及一些专门为了一致地处理DataFrame和Series中的缺失数据而设计的方法。在现实世界的数据集中,缺失数据是生活的真实写照,我们将在接下来的章节中经常看到这些(处理缺失数据的)工具。

杰克•范德普拉斯(Jake VanderPlas)

杰克Ÿ范德普拉斯是Python科学计算组件的长期用户和开发者。他现在是华盛顿大学跨学科研究主管,他主要进行他自己的天文学研究,并在各个领域为的科学家提供建议和咨询。

Not a number