Missing Data Functionality in NumPy

作者:Mark Wiebe <mwwiebe@gmail.com>
版权:版权所有2011 by Enthought,Inc
执照:CC By-SA 3.0(http://creativecommons.org/licenses/by-sa/3.0/ T0>)
日期:2011-06-23

Abstract

对NumPy中缺少数据感兴趣的用户通常指向ndarray的掩码数组子类,称为“numpy.ma”。这个类有许多用户强烈依赖于它的功能,但是习惯于在R项目中缺少的数据占位符“NA”的深度集成的人和其他发现编程接口具有挑战性或不一致性的用户倾向于不使用它。

该NEP提出将基于掩码的缺失数据解决方案集成到NumPy中,另外基于位图的缺失数据解决方案可以同时实现或者随后与基于掩码的解决方案无缝集成。

此提议中的基于掩码的解决方案和基于位模式的解决方案提供了完全相同的缺失值抽象,在性能,内存开销和灵活性方面存在一些差异。

基于掩码的解决方案更灵活,支持基于位模式的解决方案的所有行为,但是每当元素被掩蔽时保持隐藏的值不变。

基于位模式的解决方案需要更少的存储器,与在R中使用的64位浮点表示是位级兼容的,但是不保留隐藏的值,并且事实上需要从底层dtype窃取至少一个位模式以表示缺失值NA。

两种解决方案都是通用的,因为它们可以非常容易地与自定义数据类型一起使用,在掩蔽解决方案的情况下无需努力,并且在位图解决方案的情况下要求选择牺牲的位模式。

Definition of Missing Data

为了能够建立关于将由各种NumPy函数进行什么计算的直觉,必须应用缺失元素意味着什么的一致的概念模型。当人们使用“缺失数据”时,需要或想要的行为似乎是棘手的,但我相信它可以归结为两个不同的想法,每个想法在内部是自相矛盾的。

其中一个,“未知的现有数据”解释,可以严格地应用于所有计算,而另一个是有意义的一些统计操作,如标准差,但不是线性代数运算,如矩阵积。因此,使“未知的现有数据”为默认解释是优越的,在所有计算中提供一致的模型,并且对于其他解释有意义的那些操作,可以添加可选参数“skipna =”。

对于想要其他解释为默认的人,在其他地方提出的用_numpy_ufunc_成员函数定制子类ufunc行为的机制将允许创建具有不同默认值的子类。

Unknown Yet Existing Data (NA)

这是在R项目中采用的方法,将缺少的元素定义为具有不可知的有效值或不为NA(不可用)的元素。此建议将此行为用作涉及缺失值的所有操作的默认值。

在这种解释中,几乎任何缺少输入的计算都会产生缺失的输出。例如,如果'a'只包含一个缺失的元素,'sum(a)'将产生一个缺失值。当输出值不依赖于输入中的一个时,合理地输出不是NA的值,诸如logical_and(NA,False)== False。

一些更复杂的算术运算,如矩阵乘积,用这种解释很好地定义,结果应该和缺失值是NaN一样。实际上实现这样的事情到理论极限可能不值得,并且在许多情况下提出异常或返回所有缺失值可能更喜欢做精确的计算。

Data That Doesn’t Exist Or Is Being Skipped (IGNORE)

另一个有用的解释是,丢失的元素应该被视为数组中不存在,并且操作应该尽最大努力根据剩下的数据解释什么意思。在这种情况下,'mean(a)'将计算可用值的平均值,根据哪些值缺失来调整它使用的总和和计数。为了保持一致,所有缺失值的数组的平均值必须与没有缺失值支持的零大小数组的平均值产生相同的结果。

当将稀疏采样数据符合常规采样模式时,可能出现这种类型的数据,并且当尝试获得许多统计查询的最佳猜测答案时,这是一种有用的解释。

在R中,许多函数采用参数“na.rm = T”,这意味着将数据视为NA值不是数据集的一部分。此提议为此相同的目的定义了标准参数“skipna = True”。

Implementation Techniques For Missing Values

除了对缺失值有两种不同的解释,有两种不同的常用的缺失值实现技术。虽然在现有技术的实现之间存在一些不同的默认行为,但我相信,在新实现中做出的设计选择必须基于它们的优点,而不是通过对先前设计的rote复制。

根据应用程序上下文,掩码和位图具有不同的强弱点。因此,该NEP提议实施两者。为了能够编写通用的“缺失值”代码,不必担心它所使用的数组是采用了一种还是另一种方法,对于两种实现,缺失值语义将是相同的。

Bit Patterns Signalling Missing Values (bitpattern)

选择一个或多个比特模式,例如具有特定有效载荷的NaN,以表示丢失值占位符NA。

这种方法的一个后果是,分配NA会改变保存值的位,所以值失效。

此外,对于诸如整数的某些类型,必须牺牲良好和适当的值以实现该功能。

Boolean Masks Signalling Missing Values (mask)

掩码是布尔值的并行数组,每个元素一个字节或每个元素一个位,与现有数组数据一起分配。在这个NEP中,选择的约定是True表示元素有效(未屏蔽),False表示元素是NA。

通过小心地编写任何与值和掩码一起工作的C算法,可以使被掩蔽的值的存储器永远不被写入。此功能允许同一数据的多个同时查看,其中缺少什么的不同选择,邮件列表上的许多人请求的功能。

这种方法对基础数据类型的值没有限制,它可以采用任何二进制模式而不影响NA行为。

Glossary of Terms

因为上述关于不同概念及其关系的讨论难以理解,所以这里对本NEP中使用的术语的更简洁的定义。

NA(不可用/传播)
计算未知的值的占位符。该值可能会被掩码暂时隐藏,可能由于硬盘驱动器损坏而丢失,或出于任何原因。对于和和乘积,这意味着如果任何输入为NA,则产生NA。这与R项目中的NA相同。
IGNORE(忽略/跳过)
一个占位符,应该通过计算来处理,好像没有值或者可能存在。对于和,这意味着该值为零,而对于产品,这意味着该值是一个值。就好像数组以某种方式被压缩以不包括该元素。
位模式
一种用于实现NA或IGNORE的技术,其中从值的数据类型的所有可能的位模式中选择特定的一组位模式,以表示该元素是NA或IGNORE。
面具
用于实现NA或IGNORE的技术,其中使用与数据数组并行的布尔或枚举数组来表示哪些元素是NA或IGNORE。
numpy.ma
特定形式的掩码数组的现有实现,它是NumPy代码库的一部分。
Python API
所有暴露给Python代码的接口机制,在NumPy中使用缺失值。这个API被设计为Pythonic并且适合NumPy尽可能工作的方式。
C API
所有的实现机制暴露的CPython扩展写在C中,希望支持NumPy缺失值支持。此API设计为在C中尽可能自然,并且通常优先考虑灵活性和高性能。

Missing Values as Seen in Python

Working With Missing Values

NumPy将获得一个名为numpy.NA的全局单例,类似于None,但是语义反映其作为一个缺失值的状态。特别是,试图把它当作一个布尔将引发一个异常,与它的比较将产生numpy.NA而不是True或False。这些基本知识取自R项目中NA值的行为。要深入了解这些想法,http://en.wikipedia.org/wiki/Ternary_logic#Kleene_logic提供了一个起点。

例如,:

>>> np.array([1.0, 2.0, np.NA, 7.0], maskna=True)
array([1., 2., NA, 7.], maskna=True)
>>> np.array([1.0, 2.0, np.NA, 7.0], dtype='NA')
array([1., 2., NA, 7.], dtype='NA[<f8]')
>>> np.array([1.0, 2.0, np.NA, 7.0], dtype='NA[f4]')
array([1., 2., NA, 7.], dtype='NA[<f4]')

produce arrays with values [1.0, 2.0, , 7.0] / mask [Exposed, Exposed, Hidden, Exposed], and values [1.0, 2.0, , 7.0] for the masked and NA dtype versions respectively.

np.NA单例可以接受dtype = keyword参数,指示它应该被视为特定数据类型的NA。这也是用于以NumPy标量样方式保存dtype的机制。这里是这个样子:

>>> np.sum(np.array([1.0, 2.0, np.NA, 7.0], maskna=True))
NA(dtype='<f8')
>>> np.sum(np.array([1.0, 2.0, np.NA, 7.0], dtype='NA[f8]'))
NA(dtype='NA[<f8]')

向数组分配值总是导致该元素不是NA,如果必要,则透明地将其取消屏蔽。将numpy.NA分配给数组掩蔽该元素或为特定dtype分配NA位模式。在基于掩码的实现中,丢失值之后的存储可以永远不以任何方式访问,除了通过分配其值来对其进行掩码之外。

要测试是否缺少一个值,将提供函数“np.isna(arr [0])”。NumPy标量的一个主要原因是允许它们的值到字典中。

写入掩码数组的所有操作都不会影响值,除非它们也取消掩码该值。这允许仍然依赖于掩蔽元素之后的存储,如果它们仍然可以从另一个没有被掩蔽的视图访问的话。例如,在missingdata工作进度分支上运行以下命令:

>>> a = np.array([1,2])
>>> b = a.view(maskna=True)
>>> b
array([1, 2], maskna=True)
>>> b[0] = np.NA
>>> b
array([NA, 2], maskna=True)
>>> a
array([1, 2])
>>> # The underlying number 1 value in 'a[0]' was untouched

在基于掩码的实现和bitpattern实现之间复制值将透明地做正确的事情,将bitpattern转换为掩码值,或者在适当的情况下将掩码值转换为bitpattern。一个例外是如果掩码数组中的有效值恰巧具有NA位模式,则将该值复制到dtype的NA形式将导致其也变为NA。

当在具有NA dtypes的数组和屏蔽的数组之间进行操作时,结果将被屏蔽数组。这是因为在某些情况下,NA dtypes不能表示屏蔽数组中的所有值,因此去屏蔽数组是保留数据所有方面的唯一方法。

如果将np.NA或掩码值复制到数组,但不支持启用的缺失值,则会引发异常。向目标数组添加掩码将是有问题的,因为那么掩码将是消耗额外内存并以意想不到的方式降低性能的“病毒”属性。

默认情况下,字符串“NA”将用于表示str和repr输出中的缺失值。全局配置将允许改变,精确地延长nan和inf的处理方式。以下工作在当前草案实施:

>>> a = np.arange(6, maskna=True)
>>> a[3] = np.NA
>>> a
array([0, 1, 2, NA, 4, 5], maskna=True)
>>> np.set_printoptions(nastr='blah')
>>> a
array([0, 1, 2, blah, 4, 5], maskna=True)

对于浮点数,Inf和NaN是与缺失值不同的概念。如果在具有默认缺失值支持的数组中出现零除,将生成未掩蔽的Inf或NaN。为了屏蔽这些值,进一步的“a [np.logical_not(a.isfinite(a)] = np.NA”可以实现。对于位模式方法,后面部分中描述的参数化dtype('NA [f8,InfNan]')可以用于获得这些语义而不需要额外的操作。

通过掩码数组的手动循环:

>>> a = np.arange(5., maskna=True)
>>> a[3] = np.NA
>>> a
array([ 0.,  1.,  2., NA,  4.], maskna=True)
>>> for i in range(len(a)):
...     a[i] = np.log(a[i])
...
__main__:2: RuntimeWarning: divide by zero encountered in log
>>> a
array([       -inf,  0.        ,  0.69314718, NA,  1.38629436], maskna=True)

甚至与掩码值一起工作,因为'a [i]'返回具有关联的数据类型的NA对象,其可以由ufuncs正确地处理。

Accessing a Boolean Mask

这部分是由于对掩码中的True是否意味着“丢失”或“不丢失”的不同意见。另外,直接暴露掩码将排除潜在空间优化,其中使用位级而不是字节级掩码以获得八个内存使用改善的因素。

要直接访问掩码,提供了两个功能。它们对具有掩码和NA位模式的数组都等价工作,因此它们是根据NA和可用值而不是掩蔽和未掩蔽的值来指定的。函数是“np.isna”和“np.isavail”,它们分别测试NA或可用值。

Creating NA-Masked Arrays

使用NA掩码创建数组的常用方法是将关键字参数maskna = True传递给其中一个构造函数。创建新数组的大多数函数使用此参数,并在参数设置为True时生成带有所有其元素的NA掩码数组。

还有两个标志,指示和控制在掩码数组中使用的掩码的性质。这些标志可用于添加掩码,或确保掩码不是到另一个数组的掩码的视图。

首先是“arr.flags.maskna”,对于所有掩码的数组都为True,并且可以设置为True,以向没有数组的数组添加掩码。

第二个是“arr.flags.ownmaskna”,如果数组拥有掩码的内存则为True,如果数组没有掩码,则为False,或者具有另一个数组的掩码视图。如果在掩码数组中将其设置为True,数组将创建掩码的副本,以便对掩码的进一步修改不会影响从其获取视图的原始掩码。

NA-Masks When Constructing From Lists

NA掩模构造的初始设计是使所有构造完全明确。当与NA掩码的数组交互地工作时,这样做是笨拙的,并且创建对象数组而不是NA掩码的数组可能是非常令人惊讶的。

因此,每当从具有NA对象的列表中创建数组时,设计已经改变为启用NA掩模。可以有一些争论,是否应该创建NA掩码或NA位图案默认情况下,但由于时间的限制,它是唯一可行的解​​决NA掩码,并扩展NA掩码支持更完全NumPy似乎更多合理的开始另一个系统,并结束与两个不完整的系统。

Mask Implementation Details

掩码的存储器排序将始终匹配与其相关联的数组的顺序。Fortran风格的数组将有一个Fortran风格的掩码等。

当采用带有掩码的数组的视图时,视图将具有掩码,该掩码也是原始数组中的掩码的视图。这意味着取消隐藏视图中的值也会在原始数组中取消屏蔽它们,如果将掩码添加到数组中,则除非创建新数组而不复制数据,否则不可能除去该掩码。

通过首先创建数组的视图,然后向该视图添加掩码,仍然可以使用掩码临时处理数组,而不给它一个。可以通过创建多个视图并给每个视图一个掩码,同时使用多个不同的掩码来查看数据集。

New ndarray Methods

添加到numpy命名空间的新函数是:

np.isna(arr) [IMPLEMENTED]
    Returns a boolean array with True wherever the array is masked
    or matches the NA bitpattern, and False elsewhere

np.isavail(arr)
    Returns a boolean array with False wherever the array is masked
    or matches the NA bitpattern, and True elsewhere

添加到ndarray的新函数是:

arr.copy(..., replacena=np.NA)
    Modification to the copy function which replaces NA values,
    either masked or with the NA bitpattern, with the 'replacena='
    parameter suppled. When 'replacena' isn't NA, the copied
    array is unmasked and has the 'NA' part stripped from the
    parameterized dtype ('NA[f8]' becomes just 'f8').

    The default for replacena is chosen to be np.NA instead of None,
    because it may be desirable to replace NA with None in an
    NA-masked object array.

    For future multi-NA support, 'replacena' could accept a dictionary
    mapping the NA payload to the value to substitute for that
    particular NA. NAs with payloads not appearing in the dictionary
    would remain as NA unless a 'default' key was also supplied.

    Both the parameter to replacena and the values in the dictionaries
    can be either scalars or arrays which get broadcast onto 'arr'.

arr.view(maskna=True) [IMPLEMENTED]
    This is a shortcut for
    >>> a = arr.view()
    >>> a.flags.maskna = True

arr.view(ownmaskna=True) [IMPLEMENTED]
    This is a shortcut for
    >>> a = arr.view()
    >>> a.flags.maskna = True
    >>> a.flags.ownmaskna = True

Element-wise UFuncs With Missing Values

作为实现的一部分,ufuncs和其他操作将必须扩展以支持屏蔽计算。因为这是一个有用的功能,即使在一个屏蔽数组的上下文之外,除了使用屏蔽的数组ufuncs将采用一个可选的“where =”参数,允许使用布尔数组来选择计算应该在哪里完成。

>>> np.add(a, b, out=b, where=(a > threshold))

拥有这个'where ='参数的一个好处是,它提供了一种方法来临时用掩码处理对象,而不会创建一个蒙版的数组对象。在上面的例子中,这只会在'where'子句中为数组元素添加True,并且'a'和'b'都不需要屏蔽数组。

如果没有指定'out'参数,使用'where ='参数将产生一个带有掩码的数组作为结果,其中'where'子句的值为False时,缺少值。

对于布尔运算,R项目特殊情况下,logical_and和logical_or使得logical_and(NA,False)为False,而logical_or(NA,True)为True。另一方面,0 * NA不为0,但是这里NA可以表示Inf或NaN,在这种情况下,0 *后端值不会为0。

对于NumPy元素方面的ufuncs,设计将不支持输出的掩码同时依赖于掩码和输入值的能力。然而,NumPy 1.6的nditer,使得编写独立的函数很容易,它看起来和感觉就像ufuncs,但偏离了他们的行为。函数logical_and和logical_or可以被移动到与当前ufuncs向后兼容的独立函数对象中。

Reduction UFuncs With Missing Values

诸如“sum”,“prod”,“min”和“max”的缩减操作将与掩码值存在但其值未知的想法一致。

可选参数“skipna =”将被添加到这些函数,这些函数可以适当地解释它,以执行操作,就像只有未屏蔽的值存在一样。

使用'skipna = True',当所有输入值都被屏蔽时,'sum'和'prod'将分别产生加法和乘法身份,而'min'和'max'将产生屏蔽值。如果'skipna = True',需要计数的统计操作(如'mean'和'std')也将使用未屏蔽的值计数进行计算,并在所有输入都被屏蔽时产生屏蔽值。

一些例子:

>>> a = np.array([1., 3., np.NA, 7.], maskna=True)
>>> np.sum(a)
array(NA, dtype='<f8', maskna=True)
>>> np.sum(a, skipna=True)
11.0
>>> np.mean(a)
NA(dtype='<f8')
>>> np.mean(a, skipna=True)
3.6666666666666665

>>> a = np.array([np.NA, np.NA], dtype='f8', maskna=True)
>>> np.sum(a, skipna=True)
0.0
>>> np.max(a, skipna=True)
array(NA, dtype='<f8', maskna=True)
>>> np.mean(a)
NA(dtype='<f8')
>>> np.mean(a, skipna=True)
/home/mwiebe/virtualenvs/dev/lib/python2.7/site-packages/numpy/core/fromnumeric.py:2374: RuntimeWarning: invalid value encountered in double_scalars
  return mean(axis, dtype, out)
nan

函数'np.any'和'np.all'需要一些特殊的考虑,就像logical_and和logical_or一样。也许描述他们行为的最好方法是通过一系列的例子:

>>> np.any(np.array([False, False, False], maskna=True))
False
>>> np.any(np.array([False, np.NA, False], maskna=True))
NA
>>> np.any(np.array([False, np.NA, True], maskna=True))
True

>>> np.all(np.array([True, True, True], maskna=True))
True
>>> np.all(np.array([True, np.NA, True], maskna=True))
NA
>>> np.all(np.array([False, np.NA, True], maskna=True))
False

因为'np.any'是'np.logical_or'的减少,'np.all'是'np.logical_and'的减少,所以它们有一个'skipna ='参数像其他类似的减少是有意义的功能。

Parameterized NA Data Types

掩蔽的数组不是处理丢失数据的唯一方法,并且一些系统通过为丢失的数据定义特殊的“NA”值来处理该问题。这与NaN浮点值不同,NaN浮点值是不良浮点计算值的结果,但许多人使用NaN来实现此目的。

在IEEE浮点值的情况下,可以使用与NaN不同的“NA”的特定NaN值,其中存在许多NaN值。对于有符号整数,合理的方法是使用最小可存储值,其不具有对应的正值。对于无符号整数,最大存储值似乎最合理。

为了提供一个通用机制,一个参数化类型的机制比创建单独的nafloat32,nafloat64,naint64,nauint64等更有吸引力。如果这被看作是除了没有值保存之外的处理掩码的替代方式,则该参数化类型可以以特殊方式与掩码一起工作以在运行时产生值+掩码组合,并且使用与掩蔽数组系统。这允许避免为每个ufunc和每个na * dtype编写特殊情况代码的需要,这在为每个na * dtype构建单独的独立dtype实现时很难避免。

具有跨原语类型保留的NA位模式的可靠转换也需要考虑。即使在double - > float的简单情况下,这是由硬件支持的,NA值将丢失,因为NaN有效载荷通常不被保留。具有为相同底层类型指定的不同位掩码的能力也需要正确转换。通过定义良好的接口转换到/从(值,标志)对,这变得直接支持一般。

这种方法还为IEEE浮点的一些细微变化提供了一些机会。默认情况下,将使用一个精确的位模式,具有不会由硬件浮点运算产生的有效载荷的无声NaN。选择R已经可以是这个默认值。

另外,有时候将所有NaN作为缺失值也许是很好的。这需要稍微更复杂的映射以将浮点值转换为掩码/值组合,并且转换回将总是产生由NumPy使用的默认NaN。最后,将NaN和Infs作为缺失值处理将仅仅是NaN版本的轻微变化。

字符串需要稍微不同的处理,因为它们可以是任何大小。一种方法是使用由前32个ASCII / unicode值之一组成的单字符信号。这里有很多可能使用的值,例如0x15“否定确认”或0x10“数据链路转义”。

Object dtype有一个明显的信号,np.na单例本身。任何具有对象语义的dtype将不能具有这种自定义,因为指定位模式仅适用于纯二进制数据,而不是具有对象语义构造和破坏的数据。

结构类型更多是核心原始类型,以与参数化的NA能力类型相同的方式。不能将这些作为参数化的NA-dtype的参数。

dtype名称将被参数化,类似于datetime64如何由元数据单元参数化。使用什么名称可能需要一些辩论,但“NA”似乎是一个合理的选择。使用默认缺失值位模式,这些类型将类似于np.dtype('NA [float32]'),np.dtype('NA [f8]')或np.dtype('NA [i64]') 。

为了覆盖指示缺失值的位模式,可以给出十六进制无符号整数的格式的原始值,并且在上述用于浮点的特殊情况下,可以提供特殊字符串。在这种形式下明确写入的某些情况的默认值为:

np.dtype('NA[?,0x02]')
np.dtype('NA[i4,0x80000000]')
np.dtype('NA[u4,0xffffffff]')
np.dtype('NA[f4,0x7f8007a2')
np.dtype('NA[f8,0x7ff00000000007a2') (R-compatible bitpattern)
np.dtype('NA[S16,0x15]') (using the NAK character as the signal).

np.dtype('NA[f8,NaN]') (for any NaN)
np.dtype('NA[f8,InfNaN]') (for any NaN or Inf)

当没有指定参数时,将创建一个灵活的NA dtype,它本身不能容纳值,但会符合“np.astype”等函数中的输入类型。dtype'f8'映射到'NA [f8]',并且[('a','f4'),('b','i4' ),('b','NA [i4]')]。因此,要查看带有'NA [f8]'的'f8'数组'arr'的内存,你可以说arr.view(dtype ='NA')。

Future Expansion to multi-NA Payloads

包SAS和Stata都支持多个不同的“NA”值。这允许指定不同的原因为什么一个价值,例如家庭作业,没有做,因为狗吃了它或学生病了。在这些包中,不同的NA值具有指定不同NA值如何组合在一起的线性排序。

在C实现细节的部分中,掩码被设计为使得具有有效载荷的掩码是NumPy布尔类型的严格超集,并且布尔类型具有仅为零的有效载荷。不同的有效载荷与“min”操作相结合。

防止设计的未来的重要部分是确保C ABI级选择和Python API级选择有一个自然的过渡到多NA支持。这里是一种多NA支持可以看:

>>> a = np.array([np.NA(1), 3, np.NA(2)], maskna='multi')
>>> np.sum(a)
NA(1, dtype='<i4')
>>> np.sum(a[1:])
NA(2, dtype='<i4')
>>> b = np.array([np.NA, 2, 5], maskna=True)
>>> a + b
array([NA(0), 5, NA(2)], maskna='multi')

该NEP的设计不区分来自NA掩模的NA或来自NA dtype的NA。在计算中,这两个都被等价对待,掩模主要在NA数据类型上。

>>> a = np.array([np.NA, 2, 5], maskna=True)
>>> b = np.array([1, np.NA, 7], dtype='NA')
>>> a + b
array([NA, NA, 12], maskna=True)

多NA方法允许通过向不同类型分配不同的有效载荷来区分这些NA。如果我们扩展'skipna ='参数以接受除True / False之外的有效负载列表,可以这样做:

>>> a = np.array([np.NA(1), 2, 5], maskna='multi')
>>> b = np.array([1, np.NA(0), 7], dtype='NA[f4,multi]')
>>> a + b
array([NA(1), NA(0), 12], maskna='multi')
>>> np.sum(a, skipna=0)
NA(1, dtype='<i4')
>>> np.sum(a, skipna=1)
7
>>> np.sum(b, skipna=0)
8
>>> np.sum(b, skipna=1)
NA(0, dtype='<f4')
>>> np.sum(a+b, skipna=(0,1))
12

Differences with numpy.ma

numpy.ma使用的计算模型不严格遵守NA或IGNORE模型。本节展示了这些差异如何影响简单计算的一些例子。此信息对于帮助用户在系统之间导航非常重要,因此可能应将摘要放在文档中的表中。

>>> a = np.random.random((3, 2))
>>> mask = [[False, True], [True, True], [False, False]]
>>> b1 = np.ma.masked_array(a, mask=mask)
>>> b2 = a.view(maskna=True)
>>> b2[mask] = np.NA

>>> b1
masked_array(data =
 [[0.110804969841 --]
 [-- --]
 [0.955128477746 0.440430735546]],
             mask =
 [[False  True]
 [ True  True]
 [False False]],
       fill_value = 1e+20)
>>> b2
array([[0.110804969841, NA],
       [NA, NA],
       [0.955128477746, 0.440430735546]],
       maskna=True)

>>> b1.mean(axis=0)
masked_array(data = [0.532966723794 0.440430735546],
             mask = [False False],
       fill_value = 1e+20)

>>> b2.mean(axis=0)
array([NA, NA], dtype='<f8', maskna=True)
>>> b2.mean(axis=0, skipna=True)
array([0.532966723794 0.440430735546], maskna=True)

对于像np.mean这样的函数,当'skipna = True'时,所有NA的行为与空数组一致:

>>> b1.mean(axis=1)
masked_array(data = [0.110804969841 -- 0.697779606646],
             mask = [False  True False],
       fill_value = 1e+20)

>>> b2.mean(axis=1)
array([NA, NA, 0.697779606646], maskna=True)
>>> b2.mean(axis=1, skipna=True)
RuntimeWarning: invalid value encountered in double_scalars
array([0.110804969841, nan, 0.697779606646], maskna=True)

>>> np.mean([])
RuntimeWarning: invalid value encountered in double_scalars
nan

特别是,注意,numpy.ma通常跳过掩码值,除非当所有值都被掩码时返回掩码,而当所有值都为NA时,'skipna ='参数返回0,以与np.sum []):

>>> b1[1]
masked_array(data = [-- --],
             mask = [ True  True],
       fill_value = 1e+20)
>>> b2[1]
array([NA, NA], dtype='<f8', maskna=True)
>>> b1[1].sum()
masked
>>> b2[1].sum()
NA(dtype='<f8')
>>> b2[1].sum(skipna=True)
0.0

>>> np.sum([])
0.0

Boolean Indexing

使用包含NA的布尔数组的索引不具有根据NA抽象的一致解释。例如:

>>> a = np.array([1, 2])
>>> mask = np.array([np.NA, True], maskna=True)
>>> a[mask]
What should happen here?

由于NA表示有效但未知的值,它是一个布尔值,它有两个可能的基本值:

>>> a[np.array([True, True])]
array([1, 2])
>>> a[np.array([False, True])]
array([2])

改变的东西是输出数组的长度,没有什么可以替代NA。因此,至少在最初,NumPy将为这种情况引发异常。

另一种可能性是添加不一致,并遵循R使用的方法。也就是说,产生以下:

>>> a[mask]
array([NA, 2], maskna=True)

如果在用户测试中,出于实用原因发现这是必要的,则应该添加该特征,即使它是不一致的。

PEP 3118

PEP 3118没有任何掩码机制,因此具有掩码的数组将无法通过此接口访问。类似地,它不支持具有NA或IGNORE位模式的dtypes的规范,因此参数化的NA dtypes也将不能通过该接口访问。

如果NumPy允许通过PEP 3118访问,这将以非常有害的方式避开缺失值抽象。其他库将尝试使用掩码数组,并且默默地访问数据,而不会获取访问掩码或意识到掩码和数据一起丢失的抽象。

Cython

Cython使用PEP 3118来处理NumPy数组,因此目前它只是拒绝使用它们,如“PEP 3118”部分所述。

为了正确支持NumPy缺失值,Cython将需要以某种方式修改以添加此支持。可能的最好的方法是将它与支持np.nditer,这很可能会有一个增强,使编写缺失值算法更容易。

Hard Masks

numpy.ma实现有一个“硬掩码”特性,防止通过赋值来对值进行隐藏。这将是一个内部数组标志,命名为像'arr.flags.hardmask'。

如果实现硬掩码特性,布尔索引可以返回硬掩码数组,而不是一个扁平数组,可以任意选择C排序,因为它目前是这样。虽然这显着改进了数组的抽象,但它不是兼容的更改。

Shared Masks

numpy.ma的一个特性称为“共享掩码”。

http://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray.sharedmask

在不直接违反缺失值抽象的情况下,缺少值的掩蔽实现不能支持此功能。如果在两个数组'a'和'b'之间共享相同的掩码存储器,则向'a'中的被掩码的元素分配值将同时对具有在'b'中的匹配索引的元素进行掩码。因为这不是在同一时间为“b”中的那个元素分配一个有效的值,这违反了抽象。为此,基于掩码的缺失值实现将不支持共享掩码。

这与当对具有蒙版缺失值支持的数组的视图进行观察时发生的情况略有不同,其中同时获取掩码和数据的视图。结果是两个视图共享相同的掩码存储器和相同的数据存储器,其仍然保留丢失值抽象。

Interaction With Pre-existing C API Usage

使用C API确保现有代码,无论是用C,C ++还是Cython编写,做一些合理的事情是这个实现的一个重要目标。一般的策略是使现有的代码,不显式告诉numpy它支持NA掩码失败与异常说。有几种不同的访问模式,人们用来获取numpy数组数据,这里我们检查其中几个来看看numpy能做什么。这些例子是通过对numpy C API数组访问的google搜索找到的。

NumPy Documentation - How to extend NumPy

http://docs.scipy.org/doc/numpy/user/c-info.how-to-extend.html#dealing-with-array-objects

这个页面有一个“处理数组对象”的部分,其中有一些建议,如何从C访问numpy数组。接受数组时,第一步建议是使用PyArray_FromAny或一个宏建立在该函数,将给予一个NA掩码的数组,它不知道如何处理将正确失败。

处理方式是PyArray_FromAny需要一个特殊的标志NPY_ARRAY_ALLOWNA,它将允许NA掩码的数组流过。

http://docs.scipy.org/doc/numpy/reference/c-api.array.html#NPY_ARRAY_ALLOWNA

代码不遵循这个建议,而只是调用PyArray_Check()来验证它的一个ndarray并检查一些标志,将静默地产生不正确的结果。这种风格的代码没有提供任何机会,numpy说“嘿,这个数组是特别的”,所以也不兼容未来的想法懒惰评估,派生dtypes等。

Tutorial From Cython Website

http://docs.cython.org/src/tutorial/numpy.html

本教程提供了一个卷积示例,当给定包含NA值的输入时,所有示例都会失败并显示Python异常。

在引入任何Cython类型注释之前,代码的功能与解释器中的Python相同。

当引入类型信息时,它通过numpy.pxd来完成,它定义了ndarray声明和PyArrayObject *之间的映射。在引擎盖下,这映射到__Pyx_ArgTypeTest,它直接比较Py_TYPE(obj)和用于ndarray的PyTypeObject。

然后代码做一些dtype比较,并使用常规python索引访问数组元素。这个python索引仍然通过Python API,所以在numpy中的NA处理和错误检查仍然可以像正常一样工作,如果输入具有不适合输出数组的NAs,则失败。在这种情况下,当尝试将NA转换为输出中设置的整数时,它会失败。

下一版本的代码引入了更有效的索引。这基于Python的缓冲区协议。这使得Cython调用__Pyx_GetBufferAndValidate,它调用__Pyx_GetBuffer,它调用PyObject_GetBuffer。这个调用给numpy提供了一个机会,如果输入是带有NA掩码的数组,Python缓冲区协议不支持。

Numerical Python - JPL website

http://dsnra.jpl.nasa.gov/software/Python/numpydoc/numpy-13.html

这个文件是从2001年,所以不反映最近的numpy,但它是第二次在google上搜索“numpy c api example”。

有第一个例子,标题“一个简单的例子”,事实上已经无效的最近numpy即使没有NA支持。特别是,如果数据不对齐或以不同的字节顺序,它可能崩溃或产生不正确的结果。

下一个文档所做的是引入PyArray_ContiguousFromObject,这使得numpy有机会在使用NA掩码数组时引发异常,所以后面的代码将根据需要引发异常。

C Implementation Details

要实现的第一个版本是数组掩码,因为它是更通用的方法。掩码本身是一个数组,但由于它永远不能直接从Python访问,它不会是一个完整的ndarray本身。掩码始终具有与其附加的数组相同的形状,因此它不需要其自己的形状。然而,对于具有struct dtype的数组,掩码将具有不同于仅仅为直的布尔的dtype,因此它需要它自己的dtype。这给了我们对PyArrayObject的以下添加:

/*
 * Descriptor for the mask dtype.
 *   If no mask: NULL
 *   If mask   : bool/uint8/structured dtype of mask dtypes
 */
PyArray_Descr *maskna_dtype;
/*
 * Raw data buffer for mask. If the array has the flag
 * NPY_ARRAY_OWNMASKNA enabled, it owns this memory and
 * must call PyArray_free on it when destroyed.
 */
npy_mask *maskna_data;
/*
 * Just like dimensions and strides point into the same memory
 * buffer, we now just make the buffer 3x the nd instead of 2x
 * and use the same buffer.
 */
npy_intp *maskna_strides;

这些字段可以通过内联函数访问:

PyArray_Descr *
PyArray_MASKNA_DTYPE(PyArrayObject *arr);

npy_mask *
PyArray_MASKNA_DATA(PyArrayObject *arr);

npy_intp *
PyArray_MASKNA_STRIDES(PyArrayObject *arr);

npy_bool
PyArray_HASMASKNA(PyArrayObject *arr);

有2或3个标志必须添加到数组标志,这两个标志都用于请求NA掩码并用于测试它们:

NPY_ARRAY_MASKNA
NPY_ARRAY_OWNMASKNA
/* To possibly add in a later revision */
NPY_ARRAY_HARDMASKNA

为了允许轻松检测NA支持以及数组是否具有任何缺失值,我们添加以下函数:

PyDataType_HasNASupport(PyArray_Descr * dtype)
如果这是一个NA dtype或一个struct dtype,每个字段都有NA支持,则返回true。
PyArray_HasNASupport(PyArrayObject * obj)
如果数组dtype具有NA支持,或数组具有NA掩码,则返回true。
PyArray_ContainsNA(PyArrayObject * obj)
如果数组没有NA支持,则返回false。如果数组具有NA支持,并且在数组中的任何位置有NA,则返回true。
int PyArray_AllocateMaskNA(PyArrayObject * arr,npy_bool ownmaskna,npy_bool multina)
为数组分配一个NA掩码,确保所有权,如果请求,并使用NPY_MASK而不是NPY_BOOL为dtype如果multina为True。

Mask Binary Format

掩码本身的格式被设计为指示元素是否被掩码,以及包含有效载荷,使得将来可以使用具有不同有效载荷的多个不同的NA。最初,我们将简单地使用有效载荷0。

掩码具有类型npy_uint8,位0用于指示值是否被掩码。如果((m&0x01)== 0),元素被屏蔽,否则它被屏蔽。剩余的比特是有效载荷,其是(m >> 1)。将掩码与有效载荷组合的惯例是较小的有效载荷传播。该设计给予128个有效载荷值到掩蔽的元素,并且128个有效载荷值给未掩蔽的元素。

这种方法的最大好处是npy_bool也可以作为一个掩码,因为它取值0为False,1为True。此外,npy_bool的有效负载(始终为零)在所有其他可能的有效负载中占主导地位。

由于设计涉及给予掩码自己的dtype,我们可以区分具有单个NA值的掩码(npy_bool掩码)和使用多NA(npy_uint8掩码)的掩码。初始实现只支持npy_bool掩码。

丢弃的想法是允许掩码+有效负载的组合是一个简单的“最小”操作。这可以通过将有效负载置于位0到6来完成,使得有效负载是(m&0x7f),并且使用位7用于屏蔽标志,所以((m&0x80)== 0)意味着该元素被屏蔽。事实上,这使得掩码完全不同于布尔,而不是严格的超集,是这个选择被丢弃的主要原因。

C Iterator API Changes: Iteration With Masks

对于使用掩码的迭代和计算,在缺失值的上下文中,当使用掩码像ufuncs中的'where ='参数时,扩展nditer是公开此功能的最自然的方式。

掩码操作需要使用转换,对齐和其他任何导致值被复制到临时缓冲区的东西,这是由nditer很好地处理,但很难在该上下文之外。

首先我们描述迭代,设计用于在缺失值的上下文之外使用掩模,然后描述包括缺失值支持的特征。

Iterator Mask Features

我们添加了几个新的操作数标志:

NPY_ITER_WRITEMASKED

表示从缓冲区到数组的任何副本都被屏蔽。这是必要的,因为如果浮点数组像int数组一样被处理,则READWRITE模式可以破坏数据,因此复制到缓冲区并返回将截断为整数。没有提供类似的标志用于读取,因为可能不可能提前知道掩码,并且将所有内容复制到缓冲器中将不会破坏数据。

使用迭代器的代码只应该写入未被指定的掩码掩码的值,否则结果将根据缓冲是否启用而有所不同。

NPY_ITER_ARRAYMASK

表示此数组是将任何WRITEMASKED参数从缓冲区复制回数组时使用的布尔掩码。只能有一个这样的掩模,并且也不能有虚拟掩模。

作为特殊情况,如果同时指定标志NPY_ITER_USE_MASKNA,则使用操作数的掩码而不是操作数本身。如果操作数没有掩码,但基于NA dtype,则当从缓冲区复制到数组时,迭代器暴露的掩码转换为NA位模式。

NPY_ITER_VIRTUAL
表示此操作数不是数组,而是为内部迭代代码即时创建。这为代码读取/写入数据分配足够的缓冲区空间,但没有实际的数据组支持数据。当与NPY_ITER_ARRAYMASK组合时,允许创建“虚拟掩码”,指定哪些值被取消掩码,而无需创建完全掩码数组。

Iterator NA-array Features

我们添加了几个新的操作数标志:

NPY_ITER_USE_MASKNA
如果操作数具有NA dtype,NA掩码或两者,则将新的虚拟操作数添加到操作数列表的末尾,该操作数列表迭代特定操作数的掩码。
NPY_ITER_IGNORE_MASKNA

如果操作数具有NA掩码,则默认情况下迭代器将引发异常,除非指定了NPY_ITER_USE_MASKNA。该标志禁用该检查,并且用于其中首先使用PyArray_ContainsNA函数检查数组中的所有元素不为NA的情况。

如果dtype是NA dtype,这也从dtype剥离NA,显示不支持NA的dtype。

Rejected Alternative

Parameterized Data Type Which Adds Additional Memory for the NA Flag

将一个单独的掩码添加到数组的另一种方法是引入一个参数化类型,它将一个原始类型作为参数。dtype“i8”将变成“maybe [i8]”,并且字节标志将被附加到dtype以指示该值是否为NA。

这种方法增加了内存开销,大于或等于保持单独的掩码,但具有更好的局部性。为了保持dtype对齐,'i8'将需要具有16字节来保持正确的对齐,100%的开销相比于单独保持的掩码的12.5%开销。

Acknowledgments

除了Travis Oliphant和Enthought的其他人的反馈,这个NEP已经根据NumPy-Discussion邮件列表的大量反馈进行了修订。参与讨论的人员有:

Nathaniel Smith
Robert Kern
Charles Harris
Gael Varoquaux
Eric Firing
Keith Goodman
Pierre GM
Christopher Barker
Josef Perktold
Ben Root
Laurent Gautier
Neal Becker
Bruce Southey
Matthew Brett
Wes McKinney
Lluís
Olivier Delalleau
Alan G Isaac
E. Antero Tammi
Jason Grout
Dag Sverre Seljebotn
Joe Harrington
Gary Strangman
Chris Jordan-Squire
Peter

如果我错过任何人,我道歉。