Почему модуле itertools.groupby группирует NaNs в списках, но не в массивах numpy

мне трудно отладить проблему, в которой float nan на list и nan на numpy.array обрабатываются по-разному, когда они используются в 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)

когда я перебираю список смежных 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 вообще?

3 ответов


списки 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 массив с плавающими значениями (при a for-петля или groupby level), Python должен поместить эти байты в правильный объект Python. Он создает новый объект Python в памяти для каждого отдельного значения в массив, как это проходит.

например, вы можете видеть, что это разные объекты для каждого nan значения создаются, когда .tolist() называется:

>>> [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. Потому что эти указатели на nan все точки На одном np.nan объект, группировка возможна.

однако итерация по массиву NumPy не позволяет этой первоначальной проверке идентичности успешно, поэтому Python возвращается к проверке равенства и nan != nan как вы говорите.


ответы tobias_k и ajcr правильно, потому что nans в списке имеют то же самое id пока они имеют разные идентификаторы, когда они "перебираются" в массиве numpy.

этот ответ предназначен в качестве дополнения для этих ответов.

>>> 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), который проверяет идентичность объекта первый и до == проверен.

это можно проверить с помощью небольшого фрагмента цитона:

%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); is используется и упоминается в документация:

Примечание :

если o1 и o2-один и тот же объект,PyObject_RichCompareBool() всегда будет возвращать 1 для Py_EQ и 0 для Py_NE.


я не уверен, что это причина, но я только что заметил это о nan на lst и arr:

>>> 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) все то же самое экземпляр, в то время как nan на arr are разные экземпляры типа numpy.float64). Так что я предполагаю, что если нет дали, groupby будет тест на идентичность, прежде чем делать более дорогим проверка равенства.

это также согласуется с наблюдением, которое не группируется в arr.tolist() либо, потому что хоть те nan теперь float опять же, они больше не являются одним и тем же экземпляром.

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