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

作者: 布客飞龙 | 来源:发表于2019-01-13 21:57 被阅读10次

    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 对象数组):

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

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

    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 中的NaNNone

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

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

    对于没有可用标记值的类型,当存在 NA 值时,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值。(请注意,有人建议未来向 Pandas 添加原生整数 NA;截至本文撰写时,尚未包含此内容。)

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

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

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

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

    空值上的操作

    正如我们所看到的,Pandas 将NoneNaN视为基本可互换的,用于指示缺失值或空值。为了促进这个惯例,有几种有用的方法可用于检测,删除和替换 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
    '''
    

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

    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 值的行或列。这可以通过howthresh参数来指定,这些参数能够精确控制允许通过的空值数量。

    默认值是how ='any',这样任何包含空值的行或列(取决于axis关键字)都将被删除。你也可以指定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 值,你宁愿用有效值替换它们。这个值可能是单个数字,如零,或者可能是某种良好的替换或插值。你可以将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
    '''
    

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

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

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

    # 向前填充
    data.fillna(method='ffill')
    
    '''
    a    1.0
    b    1.0
    c    2.0
    d    2.0
    e    3.0
    dtype: float64
    '''
    

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

    # 向后填充
    data.fillna(method='bfill')
    
    '''
    a    1.0
    b    2.0
    c    2.0
    d    3.0
    e    3.0
    dtype: float64
    '''
    

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

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

    相关文章

      网友评论

        本文标题:数据科学 IPython 笔记本 7.7 处理缺失数据

        本文链接:https://www.haomeiwen.com/subject/wnxrdqtx.html