Почему itertools.groupby группирует NaN в списках, но не в массивах 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)

Когда я перебираю список, смежные nan сгруппированы:

>>> 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 в разные группы:

>>> 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 вообще?

Ответ 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 являются просто смежными областями памяти; это области битов и байтов, которые интерпретируются как последовательность значений (float, ints и т.д.) с помощью NumPy.

Проблема заключается в том, что когда вы запрашиваете Python для итерации по массиву NumPy, содержащему плавающие значения (на уровне for -loop или groupby), 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, как вы говорите.

Ответ 2

Ответы tobias_k и ajcr верны, потому что nan в списке имеют те же 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), который проверяет идентификацию объекта сначала и до ==.

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

%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 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); и указано в его документации

Примечание:

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

Ответ 3

Я не уверен, является ли это причиной, но я просто заметил это о 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 являются разными экземплярами типа numpy.float64), Поэтому я предполагаю, что если функция key указана, groupby проверит идентификацию перед тем, как выполнить более дорогостоящую проверку равенства.

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

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