nan_policy 的設計規範#

scipy.stats 中的許多函數都有一個名為 nan_policy 的參數,用於決定函數如何處理包含 nan 的資料。在本節中,我們提供 SciPy 開發者指南,說明 nan_policy 的預期使用方式,以確保在將此參數新增至新函數時,我們能維持一致的 API。

基本 API#

nan_policy 參數接受三個可能的字串:'omit''raise''propagate'。其含義如下:

  • nan_policy='omit':忽略輸入中出現的 nan。如果輸入包含 nan,則不產生警告 (除非移除 nan 值後的等效輸入會產生警告)。例如,對於接受單一陣列並傳回純量值的簡單函數案例 (且暫時忽略可能使用的 axis)

    func([1.0, 3.0, np.nan, 5.0], nan_policy='omit')
    

    應與以下行為相同

    func([1.0, 3.0, 5.0])
    

    更廣泛地說,對於傳回純量值的函數,func(a, nan_policy='omit') 的行為應與 func(a[~np.isnan(a)]) 相同。

    對於將向量轉換為相同大小的新向量,且輸出陣列中的每個條目都取決於輸入陣列中不只對應值的函數[1] (例如 scipy.stats.zscorescipy.stats.boxcox lmbda 為 None 時),

    y = func(a, nan_policy='omit')
    

    應與以下行為相同

    nan_mask = np.isnan(a)
    y = np.empty(a.shape, dtype=np.float64)
    y[~nan_mask] = func(a[~nan_mask])
    y[nan_mask] = np.nan
    

    (一般來說,y 的 dtype 可能取決於 a 以及 func 的預期行為)。換句話說,輸入中的 nan 會在輸出中產生對應的 nan,但該 nan 的存在不會影響非 nan 值的計算。

    用於測試此屬性的單元測試應被用於測試處理 nan_policy 的函數。

    對於傳回純量值且接受兩個或多個引數,但其值不相關的函數 (例如 scipy.stats.ansariscipy.stats.f_oneway),相同的概念適用於每個輸入陣列。因此

    func(a, b, nan_policy='omit')
    

    應與以下行為相同

    func(a[~np.isnan(a)], b[~np.isnan(b)])
    

    對於具有相關成對值的輸入 (例如 scipy.stats.pearsonrscipy.stats.ttest_rel),建議的行為是省略所有相關值中任何值為 nan 的值。對於具有兩個相關陣列輸入的函數,這表示

    y = func(a, b, nan_policy='omit')
    

    應與以下行為相同

    hasnan = np.isnan(a) | np.isnan(b)  # Union of the isnan masks.
    y = func(a[~hasnan], b[~hasnan])
    

    此類函數的 docstring 應清楚說明此行為。

  • nan_policy='raise':引發 ValueError

  • nan_policy='propagate':將 nan 值傳播到輸出。通常,這表示直接執行函數而不檢查 nan,但請參閱

    以取得可能導致意外輸出的範例。

nan_policyaxis 參數結合使用#

這裡沒有什麼令人驚訝的地方——當函數具有 axis 參數時,上述原則仍然適用。假設,例如,func 將一維陣列縮減為純量值,並將 n 維陣列處理為一維陣列的集合,其中 axis 參數指定要沿其應用縮減的軸。如果,例如

func([1, 3, 4])     -> 10.0
func([2, -3, 8, 2]) ->  4.2
func([7, 8])        ->  9.5
func([])            -> -inf

func([[  1, nan,   3,   4],
      [  2,  -3,   8,   2],
      [nan,   7, nan,   8],
      [nan, nan, nan, nan]], nan_policy='omit', axis=-1)

必須給出結果

np.array([10.0, 4.2, 9.5, -inf])

邊緣情況#

實作 nan_policy 參數的函數應優雅地處理輸入陣列中所有值都是 nan 的情況。上述基本原則仍然適用

func([nan, nan, nan], nan_policy='omit')

應與以下行為相同

func([])

實際上,在將 nan_policy 新增至現有函數時,發現該函數尚未以明確定義的方式處理這種情況並不少見,並且可能需要應用一些思考和設計來確保其正常運作。正確的行為 (無論是傳回 nan、傳回其他值、引發例外,還是其他) 將根據具體情況而定。

為什麼 nan_policy 不適用於 inf#

雖然我們在小學學到「無限不是一個數字」,但浮點數值 naninf 在性質上是不同的。inf-inf 值的行為更像是一般的浮點數值,而不是 nan

  • 人們可以將 inf 與其他浮點數值進行比較,它的行為符合預期,例如 3 < inf 為 True。

  • 在大多數情況下,算術運算在 inf 的情況下「如預期般」運作,例如 inf + inf = inf-2*inf = -inf1/inf = 0 等。

  • 許多現有的函數在 inf 的情況下「如預期般」運作:np.log(inf) = infnp.exp(-inf) = 0np.array([1.0, -1.0, np.inf]).min() = -1.0 等。

因此,雖然 nan 幾乎總是表示「發生了錯誤」或「遺失了某些東西」,但在許多情況下,inf 可以被視為有用的浮點數值。

這也與 NumPy 的 nan 函數不忽略 inf 的行為一致

>>> np.nanmax([1, 2, 3, np.inf, np.nan])
inf
>>> np.nansum([1, 2, 3, np.inf, np.nan])
inf
>>> np.nanmean([8, -np.inf, 9, 1, np.nan])
-inf

如何實作 nan_policy#

過去 (以及可能目前),某些 stats 函數透過使用遮罩陣列來遮罩 nan 值,然後使用 mstats 子套件中的函數計算結果來處理 nan_policy。這種方法的問題在於,遮罩陣列程式碼可能會將 inf 轉換為遮罩值,而這不是我們想要做的 (請參閱上文)。這也表示,如果沒有謹慎處理,傳回值將會是遮罩陣列,如果使用者傳入的是常規陣列,這可能會讓他們感到驚訝。

腳註