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

作者

纳撒尼尔·史密斯 < njs @ pobox com >,马修·布雷特 <马修.布雷特@gmail ​ com >

地位

延期

类型

标准轨道

创建

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,一旦我们有了不支持所有这些类型,为了保持一致性,我们也可能支持浮点。

从概念上讲,屏蔽数组是一个普通的矩形 numpy 数组,上面放置了一个任意形状的屏蔽。结果本质上是矩形阵列的非矩形视图。原则上,使用掩码数组可以完成的任何事情也可以通过显式保留常规数组和布尔掩码数组并使用 numpy 索引将它们组合到每个操作中来完成,但是当您将它们组合成单个结构时会更方便需要对数组的屏蔽视图执行复杂的操作,同时仍然能够以通常的方式操作屏蔽。因此,掩码是通过索引保留的,并且函数通常将掩码值视为一开始就不是数组的一部分。 (也许这是一个很好的启发式方法:只要不更改掩码,最后一个值已被屏蔽掉的 length-4 数组的行为就像普通的 length-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)

Ufunc #

默认情况下,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)