# 7.7 处理缺失数据

> 原文：[Handling Missing Data](http://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/pandas/03.04-Missing-Values.ipynb)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)（Python Data Science Handbook）的摘录。

许多教程中的数据与现实世界中的数据之间的差异在于，真实世界的数据很少是干净和同构的。特别是，许多有趣的数据集缺少一些数据。为了使事情变得更复杂，不同的数据源可能以不同的方式标记缺失数据。

在本节中，我们将讨论缺失数据的一些一般注意事项，讨论 Pandas 如何选择来表示它，并演示一些处理 Python 中的缺失数据的 Pandas 内置工具。在整本书中，我们将缺失数据称为空值或`NaN`值。

## 缺失数据惯例中的权衡

许多方案已经开发出来，来指示表格或`DataFrame`中是否存在缺失数据。通常，它们围绕两种策略中的一种：使用在全局表示缺失值的掩码，或选择表示缺失条目的标记值。

在掩码方法中，掩码可以是完全独立的布尔数组，或者它可以在数据表示中占用一个比特，在本地表示值的空状态。

在标记方法中，标记值可能是某些特定于数据的惯例，例如例如使用`-9999`或某些少见的位组合来表示缺失整数值，或者它可能是更全局的惯例，例如使用`NaN`（非数字）表示缺失浮点值，这是一个特殊值，它是 IEEE 浮点规范的一部分。

这些方法都没有权衡：使用单独的掩码数组需要分配额外的布尔数组，这会增加存储和计算的开销。标记值减少了可以表示的有效值的范围，并且可能需要 CPU 和 GPU 算法中的额外（通常是非最优的）逻辑。 像`NaN`这样的常见特殊值不适用于所有数据类型。

在大多数情况下，不存在普遍最佳选择，不同的语言和系统使用不同的惯例。例如，R 语言使用每种数据类型中的保留位组合，作为表示缺失数据的标记值，而 SciDB 系统使用表示 NA 状态的额外字节，附加到每个单元。

## Pandas 中的缺失数据

Pandas 处理缺失值的方式受到其对 NumPy 包的依赖性的限制，NumPy 包没有非浮点数据类型的 NA 值的内置概念。

Pandas 可以遵循 R 的指导，为每个单独的数据类型指定位组合来表示缺失值，但这种方法结果相当笨拙。虽然 R 包含四种基本数据类型，但 NumPy 支持更多：例如，R 具有单个整数类型，但是一旦考虑到编码的可用精度，签名和字节顺序，NumPy 支持十四个基本整数类型。

在所有可用的 NumPy 类型中保留特定的位组合，将产生各种类型的各种操作的大量开销，甚至可能需要 NumPy 包的新分支。 此外，对于较小的数据类型（例如 8 位整数），牺牲一个位用作掩码，将显着减小它可以表示的值的范围。

NumPy 确实支持掩码数组吗？也就是说，附加了一个独立的布尔掩码数组的数组，用于将数据标记为“好”或“坏”。Pandas 可能源于此，但是存储，计算和代码维护的开销，使得这个选择变得没有吸引力。

考虑到这些约束，Pandas 选择使用标记来丢失数据，并进一步选择使用两个已经存在的 Python 空值：特殊浮点值`NaN`和 Python `None`对象。我们将要看到，这种选择有一些副作用，但实际上在大多数相关情况下，最终都是很好的妥协。

### ``None``：Python 风格的缺失数据

Pandas 使用的第一个标记值是``None``，这是一个 Python 单例对象，通常用于 Python 代码中的缺失数据。因为它是一个 Python 对象，所以``None``不能用于任何 NumPy/Pandas 数组，只能用于数据类型为``'object'``的数组（即 Python 对象数组）：

```py
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 层面完成，与具有原生类型的数组的常见快速操作相比，其开销要大得多：

```py
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()``之类的聚合，你通常会得到错误：

```py
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'
'''
```

这反映了一个事实，即整数和`None`之间的加法是未定义的。

### ``NaN``：缺失的数值数据

另一个缺失的数据表示，``NaN``（“非数字”的首字母缩写）是不同的；它是所有系统都识别的特殊浮点值，使用标准 IEEE 浮点表示：

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

# dtype('float64')
```

请注意，NumPy 为此数组选择了一个原生浮点类型：这意味着与之前的对象数组不同，此数组支持推送到编译代码中的快速操作。你应该知道``NaN``有点像数据病毒 - 它会感染它触及的任何其他对象。无论操作如何，`NaN`的算术结果都是另一个`NaN`：

```py
1 + np.nan

# nan

0 *  np.nan

# nan
```

请注意，这意味着值的聚合是定义良好的（即，它们不会导致错误），但并不总是有用：

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

# (nan, nan, nan)
```

NumPy 确实提供了一些忽略这些缺失值的特殊聚合：

```py
np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)

# (8.0, 1.0, 4.0)
```

请记住，``NaN``是一个特殊浮点值；整数，字符串或其他类型没有等效的`NaN`值。

### Pandas 中的`NaN`和`None`

``NaN``和``None``都有它们的位置，并且 Pandas 的构建是为了几乎可以互换地处理这两个值，在适当的时候在它们之间进行转换：

```py
pd.Series([1, np.nan, 2, None])

'''
0    1.0
1    NaN
2    2.0
3    NaN
dtype: float64
'''
```

对于没有可用标记值的类型，当存在 NA 值时，Pandas 会自动进行类型转换。例如，如果我们将整数数组中的值设置为``np.nan``，它将自动向上转换为浮点类型来兼容 NA：

```py
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``值。（请注意，有人建议未来向 Pandas 添加原生整数 NA；截至本文撰写时，尚未包含此内容。）

虽然与 R 等领域特定语言中，更为统一的 NA 值方法相比，这种黑魔法可能会有些笨拙，但 Pandas 标记值方法在实践中运作良好，根据我的经验，很少会产生问题。

下表列出了引入 NA 值时 Pandas 中的向上转换惯例：

|类型     | 储存 NA 时的惯例 | NA 标记值      |
|--------------|-----------------------------|------------------------|
| ``floating`` |      不变                   | ``np.nan``             |
| ``object``   |      不变                   | ``None``或``np.nan`` |
| ``integer``  |   转换为``float64``         | ``np.nan``             |
| ``boolean``  |   转换为``object``          | ``None``或``np.nan`` |

请记住，在 Pandas 中，字符串数据始终与``object dtype``一起存储。

## 空值上的操作

正如我们所看到的，Pandas 将``None``和``NaN``视为基本可互换的，用于指示缺失值或空值。为了促进这个惯例，有几种有用的方法可用于检测，删除和替换 Pandas 数据结构中的空值。他们是：

- ``isnull()``: 生成表示缺失值的布尔掩码
- ``notnull()``: ``isnull()``的反转
- ``dropna()``: 返回数据的过滤后版本
- ``fillna()``: 返回数据的副本，填充了缺失值

我们将结束本节，简要探讨和演示这些例程。

### 检测控制

Pandas 数据结构有两种有用的方法来检测空数据：``isnull()``和``notnull()``。任何一个都返回数据上的布尔掩码。例如：

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

data.isnull()

'''
0    False
1     True
2    False
3     True
dtype: bool
'''
```

如“数据索引和选择”中所述，布尔掩码可以直接用作``Series``或``DataFrame``的索引：

```py
data[data.notnull()]

'''
0        1
2    hello
dtype: object
'''
```

``isnull()``和``notnull()``方法为``DataFrame``生成类似的布尔结果。

### 删除空值

除了之前使用的掩码之外，还有一些方便的方法，``dropna()``（删除 NA 值）和``fillna()``（填充 NA 值）。 对于``Series``，结果很简单：

```py
data.dropna()

'''
0        1
2    hello
dtype: object
'''
```

对于``DataFrame``，还有更多选项。考虑以下``DataFrame``：

```py
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()``将删除包含空值的所有行：

```py
df.dropna()
```

|  | 0 | 1 | 2 |
| --- | --- | --- | --- |
| 1 | 2.0 | 3.0 | 5 |

或者，你可以沿不同的轴删除 NA 值; ``axis = 1``删除包含空值的所有列：

```py
df.dropna(axis='columns')
```

|  | 2 |
| --- | --- |
| 0 | 2 |
| 1 | 5 |
| 2 | 6 |

但这也会丢掉一些好的数据; 你可能更愿意删除全部为 NA 值或大多数为 NA 值的行或列。这可以通过``how``或``thresh``参数来指定，这些参数能够精确控制允许通过的空值数量。

默认值是``how ='any'``，这样任何包含空值的行或列（取决于``axis``关键字）都将被删除。你也可以指定``how ='all'``，它只会丢弃全部为空值的行/列：

```py
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 |

```py
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``参数允许你为要保留的行/列指定最小数量的非空值：

```py
df.dropna(axis='rows', thresh=3)
```

|  | 0 | 1 | 2 | 3 |
| --- | --- | --- | --- | --- |
| 1 | 2.0 | 3.0 | 5 | NaN |

这里删除了第一行和最后一行，因为它们只包含两个非空值。

### 填充空值

有时比起删除 NA 值，你宁愿用有效值替换它们。这个值可能是单个数字，如零，或者可能是某种良好的替换或插值。你可以将``isnull()``方法用作掩码，原地执行此操作，但因为它是如此常见的操作，Pandas 提供``fillna()``方法，该方法返回数组的副本，其中空值已替换。

考虑下面的``Series``:

```py
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
'''
```

我们可以使用单个值填充 NA 条目，例如零：

```py
data.fillna(0)

'''
a    1.0
b    0.0
c    2.0
d    0.0
e    3.0
dtype: float64
'''
```

我们可以指定前向填充来传播前一个值：

```py
# 向前填充
data.fillna(method='ffill')

'''
a    1.0
b    1.0
c    2.0
d    2.0
e    3.0
dtype: float64
'''
```

或者我们可以指定反向填充，来向后传播下一个值：

```py
# 向后填充
data.fillna(method='bfill')

'''
a    1.0
b    2.0
c    2.0
d    3.0
e    3.0
dtype: float64
'''
```

对于``DataFrame``，选项也类似，但我们也可以指定``axis``，沿着该轴进行填充：

```py
df
```

|  | 0 | 1 | 2 | 3 |
| --- | --- | --- | --- | --- |
| 0 | 1.0 | NaN | 2 | NaN |
| 1 | 2.0 | 3.0 | 5 | NaN |
| 2 | NaN | 4.0 | 6 | NaN |

```py
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 值仍然存在。