NEP 24 — 缺失数据功能 - NEP 12 的替代方案#

作者:

Nathaniel J. Smith <njs@pobox.com>, Matthew Brett <matthew.brett@gmail.com>

状态:

Deferred

类型:

标准轨道

创建时间:

2011-06-30

摘要#

背景:此 NEP 是 NEP 12 的替代方案,在撰写本文时,NEP 12 的实现已合并到 NumPy 主分支。

此 NEP 的原则是根据以下内容分离掩码和缺失值的 API:

  • 当前掩码数组的实现(NEP 12)

  • 本提案。

本讨论仅涉及 API,不涉及实现。

详细描述#

理由#

此 NEP 的目的是定义两个接口——一个用于处理“缺失值”,一个用于处理“掩码数组”。

普通值是指整数或浮点数。缺失值是某个普通值的占位符,该值因某种原因不可用。例如,在处理统计数据时,我们经常构建表格,其中每一行代表一个项目,每一列代表该项目的属性。例如,我们可以选取一组人,并记录每个人的身高、年龄、教育程度和收入,然后将这些值放入表格中。但随后我们发现研究助理搞砸了,忘记记录其中一个人的年龄。我们可以丢弃他们其余的数据,但这很浪费;即使是这样不完整的行,对于某些分析来说也是完全可用的(例如,我们可以计算身高和收入的相关性)。处理这种情况的传统方法是为缺失数据插入某个无意义的值,例如,记录这个人的年龄为 0。但这非常容易出错;我们稍后可能会在进行其他分析时忘记这些特殊值,并惊讶地发现婴儿的收入高于青少年。(在这种情况下,解决方案是仅排除所有没有记录年龄的项目,但这并非通用解决方案;许多分析需要更巧妙的方法来处理缺失值。)因此,我们不使用 0 这样的普通值,而是定义一个特殊的“缺失”值,写为“NA”,意为“不可用”。

因此,缺失值具有以下属性:与其他任何值一样,它们必须被您的数组的 dtype 支持——您不能将浮点数存储在 dtype=int32 的数组中,也不能存储 NA。您需要一个 dtype=NAint32 或类似(确切语法待定)的数组。否则,它们的行为与其他任何值完全相同。特别是,您可以对它们应用算术函数等。默认情况下,任何以 NA 作为参数的函数都将始终返回 NA,而不管其他参数的值如何。这确保了如果我们尝试计算收入与年龄的相关性,我们会得到“NA”,表示“考虑到某些条目可能是任何值,结果也可能是任何值”。这提醒我们花点时间思考如何更具意义地重述我们的问题。另外,为了方便那些您确实想计算已知年龄与收入之间相关性的情况,您可以通过在函数调用中添加一个参数来启用此行为。

对于浮点数计算,NA 和 NaN 的行为(几乎?)相同。但它们代表不同的事物——NaN 代表无效计算,如 0/0;NA 代表不可用的值——区分这些事物很有用,因为在某些情况下它们应该被区别对待。(例如,插补程序应将 NA 替换为插补值,但很可能应保持 NaN 不变。)而且,我们不能将 NaN 用于整数、字符串或布尔值,所以我们无论如何都需要 NA,一旦我们对所有这些类型都支持 NA,我们不妨也为浮点数提供支持以保持一致性。

掩码数组在概念上是一个普通的矩形 numpy 数组,在其上放置了一个任意形状的掩码。结果本质上是一个矩形数组的非矩形视图。原则上,您使用掩码数组能完成的任何事情,都可以通过显式维护一个常规数组和一个布尔掩码数组,并使用 numpy 索引将它们组合起来进行每项操作来实现,但将它们组合成一个单一结构在您需要对数组的掩码视图执行复杂操作,同时仍然能够以常规方式操作掩码时,会更加方便。因此,掩码通过索引得以保留,并且函数通常将掩码掉的值视为仿佛它们根本不存在于数组中一样。(也许这是一个好的启发式方法:一个长度为 4 的数组,其中最后一个值被掩码掉了,它的行为就像一个普通的长度为 3 的数组,只要您不更改掩码。)当然,您可以随时以任意方式操作掩码;它只是一个标准的 numpy 数组。

在一些简单的情况下,您可以使用其中任何一个工具来完成工作——或者其他工具,如使用指定的代理值(年龄=0)、单独的掩码数组等。但缺失值旨在特别有助于处理缺失是数据内在特征的情况——即存在一个特定的值,如果存在,它将具有特定含义,但它不存在。掩码数组旨在特别有助于处理我们只想暂时忽略某些存在的数据的情况,或者通常在我们处理形状不规则的数据时(例如,如果您在覆盖圆形培养皿的网格上的每个点进行一些测量,那么落在培养皿外的点不是测量缺失,而是毫无意义的)。

初始化#

首先,缺失值可以设置为并显示为 np.NA, NA

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

由于初始化没有歧义,因此可以不带 NA dtype 来编写

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

掩码值可以设置为并显示为 np.IGNORE, IGNORE

>>> np.array([1.0, 2.0, np.IGNORE, 7.0], masked=True)
array([1., 2., IGNORE, 7.], masked=True)

由于初始化没有歧义,因此可以不带 masked=True 来编写

>>> np.array([1.0, 2.0, np.IGNORE, 7.0])
array([1., 2., IGNORE, 7.], masked=True)

通用函数#

默认情况下,NA 值会传播

>>> na_arr = np.array([1.0, 2.0, np.NA, 7.0])
>>> np.sum(na_arr)
NA('float64')

除非设置了 skipna 标志

>>> np.sum(na_arr, skipna=True)
10.0

默认情况下,掩码不会传播

>>> masked_arr = np.array([1.0, 2.0, np.IGNORE, 7.0])
>>> np.sum(masked_arr)
10.0

除非设置了 propmask 标志

>>> np.sum(masked_arr, propmask=True)
IGNORE

数组可以被掩码,并包含 NA 值

>>> both_arr = np.array([1.0, 2.0, np.IGNORE, np.NA, 7.0])

在默认情况下,行为是显而易见的

>>> np.sum(both_arr)
NA('float64')

对于 skipna=True 怎么做也是显而易见的

>>> np.sum(both_arr, skipna=True)
10.0
>>> np.sum(both_arr, skipna=True, propmask=True)
IGNORE

为了打破 NA 和 MSK 之间的联系,NA 的传播更加严格

>>> np.sum(both_arr, propmask=True)
NA('float64')

赋值#

在 NA 的情况下是显而易见的

>>> arr = np.array([1.0, 2.0, 7.0])
>>> arr[2] = np.NA
TypeError('dtype does not support NA')
>>> na_arr = np.array([1.0, 2.0, 7.0], dtype='NA[f8]')
>>> na_arr[2] = np.NA
>>> na_arr
array([1., 2., NA], dtype='NA[<f8]')

在掩码情况下直接赋值是魔法且令人困惑的,因此仅通过掩码进行

>>> masked_array = np.array([1.0, 2.0, 7.0], masked=True)
>>> masked_arr[2] = np.NA
TypeError('dtype does not support NA')
>>> masked_arr[2] = np.IGNORE
TypeError('float() argument must be a string or a number')
>>> masked_arr.visible[2] = False
>>> masked_arr
array([1., 2., IGNORE], masked=True)