关于 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])
    

    此类函数的文档字符串应该清楚地说明这种行为。

  • 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-infnan 更像常规浮点值。

  • 可以将 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 在很多情况下可以被视为有用的浮点值。

不忽略 inf 也与 NumPy 的 nan 函数保持一致

>>> 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 转换为掩码值,而我们不希望这样做(见上文)。它还意味着,如果不注意,返回值将是掩码数组,如果用户传入的是常规数组,这很可能会让他们感到意外。

脚注