为啥 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
中的浮点nan
和numpy.array
中的nan
在itertools.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)
当我遍历列表时,连续的nan
s 被分组:
>>> 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'>
但是,如果我使用数组,它会将连续的 nan
s 放在不同的组中:
>>> 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
怎么可能对nan
s 进行分组?
【问题讨论】:
嗯,我想知道是什么引发了这个问题。 :) 这里的问题到底是什么:为什么groupby
表现出这种不一致的行为,或者如何解决它?在后一种情况下,您是否要对nan
s 进行分组?
如果其他人感兴趣,当我试图找出为什么 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】:我不确定这是否是原因,但我刚刚注意到lst
和arr
中的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.nan
(float
类型)都是相同 实例,而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 的答案是正确的,这是因为列表中的nan
s 具有相同的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() 生成的迭代器被意外消耗