NEP 24 — 遺失資料功能 - NEP 12 的替代方案 1#

作者:

Nathaniel J. Smith <njs@pobox.com>、Matthew Brett <matthew.brett@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,一旦我們為所有這些類型提供 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)

Ufuncs#

預設情況下,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)