数据科学 IPython 笔记本 7.7 处理缺失数据

2022-06-03 17:20:05 浏览数 (1)

7.7 处理缺失数据

原文:Handling Missing Data 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是《Python 数据科学手册》(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 对象数组):

代码语言:javascript复制
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 层面完成,与具有原生类型的数组的常见快速操作相比,其开销要大得多:

代码语言:javascript复制
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()之类的聚合,你通常会得到错误:

代码语言:javascript复制
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 浮点表示:

代码语言:javascript复制
vals2 = np.array([1, np.nan, 3, 4]) 
vals2.dtype

# dtype('float64')

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

代码语言:javascript复制
1   np.nan

# nan

0 *  np.nan

# nan

请注意,这意味着值的聚合是定义良好的(即,它们不会导致错误),但并不总是有用:

代码语言:javascript复制
vals2.sum(), vals2.min(), vals2.max()

# (nan, nan, nan)

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

代码语言:javascript复制
np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)

# (8.0, 1.0, 4.0)

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

Pandas 中的NaNNone

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

代码语言:javascript复制
pd.Series([1, np.nan, 2, None])

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

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

代码语言:javascript复制
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 将NoneNaN视为基本可互换的,用于指示缺失值或空值。为了促进这个惯例,有几种有用的方法可用于检测,删除和替换 Pandas 数据结构中的空值。他们是:

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

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

检测控制

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

代码语言:javascript复制
data = pd.Series([1, np.nan, 'hello', None])

data.isnull()

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

如“数据索引和选择”中所述,布尔掩码可以直接用作SeriesDataFrame的索引:

代码语言:javascript复制
data[data.notnull()]

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

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

删除空值

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

代码语言:javascript复制
data.dropna()

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

对于DataFrame,还有更多选项。考虑以下DataFrame

代码语言:javascript复制
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()将删除包含空值的所有行:

代码语言:javascript复制
df.dropna()

0

1

2

1

2.0

3.0

5

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

代码语言:javascript复制
df.dropna(axis='columns')

2

0

2

1

5

2

6

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

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

代码语言:javascript复制
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

代码语言:javascript复制
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参数允许你为要保留的行/列指定最小数量的非空值:

代码语言:javascript复制
df.dropna(axis='rows', thresh=3)

0

1

2

3

1

2.0

3.0

5

NaN

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

填充空值

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

考虑下面的Series:

代码语言:javascript复制
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 条目,例如零:

代码语言:javascript复制
data.fillna(0)

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

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

代码语言:javascript复制
# 向前填充
data.fillna(method='ffill')

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

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

代码语言:javascript复制
# 向后填充
data.fillna(method='bfill')

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

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

代码语言:javascript复制
df

0

1

2

3

0

1.0

NaN

2

NaN

1

2.0

3.0

5

NaN

2

NaN

4.0

6

NaN

代码语言:javascript复制
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 值仍然存在。

0 人点赞