【问题标题】:Why can itertools.groupby group the NaNs in lists but not in numpy arrays为什么 itertools.groupby 可以将 NaN 分组在列表中而不是 numpy 数组中
【发布时间】:2017-06-03 01:37:25
【问题描述】:

我很难调试list 中的浮点nannumpy.array 中的nanitertools.groupby 中使用时处理方式不同的问题:

给定以下列表和数组:

from itertools import groupby
import numpy as np

lst = [np.nan, np.nan, np.nan, 0.16, 1, 0.16, 0.9999, 0.0001, 0.16, 0.101, np.nan, 0.16]
arr = np.array(lst)

当我遍历列表时,连续的nans 被分组:

>>> for key, group in groupby(lst):
...     if np.isnan(key):
...         print(key, list(group), type(key))
nan [nan, nan, nan] <class 'float'>
nan [nan] <class 'float'>

但是,如果我使用数组,它会将连续的 nans 放在不同的组中:

>>> for key, group in groupby(arr):
...     if np.isnan(key):
...         print(key, list(group), type(key))
nan [nan] <class 'numpy.float64'>
nan [nan] <class 'numpy.float64'>
nan [nan] <class 'numpy.float64'>
nan [nan] <class 'numpy.float64'>

即使我将数组转换回列表:

>>> for key, group in groupby(arr.tolist()):
...     if np.isnan(key):
...         print(key, list(group), type(key))
nan [nan] <class 'float'>
nan [nan] <class 'float'>
nan [nan] <class 'float'>
nan [nan] <class 'float'>

我正在使用:

numpy 1.11.3
python 3.5

我知道一般nan != nan 那么为什么这些操作会给出不同的结果呢? groupby 怎么可能对nans 进行分组?

【问题讨论】:

  • 嗯,我想知道是什么引发了这个问题。 :)
  • 这里的问题到底是什么:为什么groupby 表现出这种不一致的行为,或者如何解决它?在后一种情况下,您是否要对nans 进行分组?
  • 如果其他人感兴趣,当我试图找出为什么 an answer 在应用于列表时有效但在应用到 np.array 时无效时,会提示该问题。
  • @tobias_k 我有兴趣解释为什么这些不同。
  • groupby 应用于 float dtype 数组可能接近无用,除非将 np.isclose 用作 key

标签: python arrays list numpy nan


【解决方案1】:

Python 列表只是指向内存中对象的指针数组。特别是lst 持有指向对象np.nan 的指针:

>>> [id(x) for x in lst]
[139832272211880, # nan
 139832272211880, # nan
 139832272211880, # nan
 139832133974296,
 139832270325408,
 139832133974296,
 139832133974464,
 139832133974320,
 139832133974296,
 139832133974440,
 139832272211880, # nan
 139832133974296]

np.nan 在我的电脑上是 139832272211880。)

另一方面,NumPy 数组只是内存的连续区域。它们是位和字节的区域,被 NumPy 解释为一系列值(浮点数、整数等)。

问题在于,当您要求 Python 遍历包含浮动值的 NumPy 数组时(在 for-loop 或 groupby 级别),Python 需要将这些字节装箱到适当的 Python 对象中。它在迭代时为数组中的每个单个值在内存中创建一个全新的 Python 对象。

例如,您可以看到在调用 .tolist() 时为每个 nan 值创建了不同的对象:

>>> [id(x) for x in arr.tolist()]
[4355054616, # nan
 4355054640, # nan
 4355054664, # nan
 4355054688,
 4355054712,
 4355054736,
 4355054760,
 4355054784,
 4355054808,
 4355054832,
 4355054856, # nan
 4355054880]

itertools.groupby 能够在np.nan 上对 Python 列表进行分组,因为它在比较 Python 对象时首先检查 identity。因为这些指向nan 的指针都指向同一个np.nan 对象,所以可以进行分组。

但是,NumPy 数组上的迭代不允许此初始身份检查成功,因此 Python 退回到检查相等性和 nan != nan,如您所说。

【讨论】:

    【解决方案2】:

    我不确定这是否是原因,但我刚刚注意到lstarr 中的nan

    >>> lst[0] == lst[1], arr[0] == arr[1]
    (False, False)
    >>> lst[0] is lst[1], arr[0] is arr[1]
    (True, False)
    

    即,虽然所有nan 都不相等,但常规np.nanfloat 类型)都是相同 实例,而arr 中的nan 是不同类型的numpy.float64实例)。所以我的猜测是,如果没有给出key 函数,groupby 将在进行更昂贵的相等检查之前测试身份。

    这也与没有在arr.tolist() 中分组的观察结果一致,因为即使那些nan 现在又是float,它们不再是同一个实例。

    >>> atl = arr.tolist()
    >>> atl[0] is atl[1]
    False
    

    【讨论】:

    • 所以math.nan 是单例,np.nan 是单例,但访问np.array 会创建一个不是单例的临时浮点数?大吃一惊。
    • @MarkRansom 不仅np.array.__getitem__ 在您想要访问它时为 nan 创建了一个不同的对象。但是每次您手动创建nan(例如float('nan'))时,都会创建一个新的not-singleton nan。有趣的事实:np.nan is not math.nan!
    【解决方案3】:

    tobias_kajcr 的答案是正确的,这是因为列表中的nans 具有相同的id,而它们在numpy-array 中“迭代”时具有不同的ID。

    此答案旨在作为这些答案的补充。

    >>> from itertools import groupby
    >>> import numpy as np
    
    >>> lst = [np.nan, np.nan, np.nan, 0.16, 1, 0.16, 0.9999, 0.0001, 0.16, 0.101, np.nan, 0.16]
    >>> arr = np.array(lst)
    
    >>> for key, group in groupby(lst):
    ...     if np.isnan(key):
    ...         print(key, id(key), [id(item) for item in group])
    nan 1274500321192 [1274500321192, 1274500321192, 1274500321192]
    nan 1274500321192 [1274500321192]
    
    >>> for key, group in groupby(arr):
    ...     if np.isnan(key):
    ...         print(key, id(key), [id(item) for item in group])
    nan 1274537130480 [1274537130480]
    nan 1274537130504 [1274537130504]
    nan 1274537130480 [1274537130480]
    nan 1274537130480 [1274537130480]  # same id as before but these are not consecutive
    
    >>> for key, group in groupby(arr.tolist()):
    ...     if np.isnan(key):
    ...         print(key, id(key), [id(item) for item in group])
    nan 1274537130336 [1274537130336]
    nan 1274537130408 [1274537130408]
    nan 1274500320904 [1274500320904]
    nan 1274537130168 [1274537130168]
    

    问题在于 Python 在比较值时使用了PyObject_RichCompare-操作,该操作仅在 == 因未实现而失败时测试对象身份。另一方面,itertools.groupby 使用 PyObject_RichCompareBool(参见来源:12)来测试对象身份首先和之前测试 ==

    这可以用一个小cython sn-p来验证:

    %load_ext cython
    %%cython
    
    from cpython.object cimport PyObject_RichCompareBool, PyObject_RichCompare, Py_EQ
    
    def compare(a, b):
        return PyObject_RichCompare(a, b, Py_EQ), PyObject_RichCompareBool(a, b, Py_EQ)
    
    >>> compare(np.nan, np.nan)
    (False, True)
    

    PyObject_RichCompareBool 的源代码如下所示:

    /* Perform a rich comparison with object result.  This wraps do_richcompare()
       with a check for NULL arguments and a recursion check. */
    
    /* Perform a rich comparison with integer result.  This wraps
       PyObject_RichCompare(), returning -1 for error, 0 for false, 1 for true. */
    int
    PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
    {
        PyObject *res;
        int ok;
    
        /* Quick result when objects are the same.
           Guarantees that identity implies equality. */
        /**********************That's the difference!****************/
        if (v == w) {
            if (op == Py_EQ)
                return 1;
            else if (op == Py_NE)
                return 0;
        }
    
        res = PyObject_RichCompare(v, w, op);
        if (res == NULL)
            return -1;
        if (PyBool_Check(res))
            ok = (res == Py_True);
        else
            ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        return ok;
    }
    

    对象身份测试(if (v == w) )确实在正常的python比较之前完成了PyObject_RichCompare(v, w, op);使用并在its documentation中提到:

    注意:

    如果 o1 和 o2 是同一个对象,PyObject_RichCompareBool() 将始终为 Py_EQ 返回 1,为 Py_NE 返回 0。

    【讨论】:

      猜你喜欢
      • 2013-12-17
      • 2020-03-30
      • 2022-01-16
      • 1970-01-01
      • 2021-09-10
      • 1970-01-01
      • 2011-03-27
      • 1970-01-01
      相关资源
      最近更新 更多