为啥 itertools.groupby 可以将 NaN 分组在列表中而不是 numpy 数组中

Posted

技术标签:

【中文标题】为啥 itertools.groupby 可以将 NaN 分组在列表中而不是 numpy 数组中【英文标题】: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 【参考方案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_k 和ajcr 的答案是正确的,这是因为列表中的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(参见来源:1、2)来测试对象身份首先和之前测试 ==

这可以用一个小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。

【讨论】:

以上是关于为啥 itertools.groupby 可以将 NaN 分组在列表中而不是 numpy 数组中的主要内容,如果未能解决你的问题,请参考以下文章

为啥 itertools.groupby() 不起作用? [复制]

由 itertools.groupby() 生成的迭代器被意外消耗

itertools.groupby 返回错误的结果(这与排序无关)[重复]

如何将 itertools “grouper”对象变成列表

python中的itertools.groupby()

python itertools groupby 返回元组