为啥'groupby(x,np.isnan)'的行为与'groupby(x)如果键是nan'不同?

Posted

技术标签:

【中文标题】为啥\'groupby(x,np.isnan)\'的行为与\'groupby(x)如果键是nan\'不同?【英文标题】:Why does 'groupby(x, np.isnan)' behave differently to 'groupby(x) if key is nan'?为什么'groupby(x,np.isnan)'的行为与'groupby(x)如果键是nan'不同? 【发布时间】:2017-06-03 02:29:57 【问题描述】:

Since we're on the topic of peculiarities surrounding numpy's nan,我发现了一些我也不明白的东西。我发布这个问题主要是作为 MSeifert 的延伸,因为我们的观察似乎有一个共同的原因。

早些时候,I posted a solution 涉及在包含 nan 值的序列上使用 itertools.groupby

return max((sum(1 for _ in group) for key, group in groupby(sequence) if key is nan), default=0)

但是,我在上面链接的 MSeifert 的问题上看到了this answer,它显示了我可能制定此算法的另一种方法:

return max((sum(1 for _ in group) for key, group in groupby(sequence, np.isnan)), default=0)

实验

我已经使用列表和 numpy 数组测试了这两种变体。代码和结果如下:

from itertools import groupby

from numpy import nan
import numpy as np


def longest_nan_run(sequence):
    return max((sum(1 for _ in group) for key, group in groupby(sequence) if key is nan), default=0)


def longest_nan_run_2(sequence):
    return max((sum(1 for _ in group) for key, group in groupby(sequence, np.isnan)), default=0)


if __name__ == '__main__':
    nan_list = [nan, nan, nan, 0.16, 1, 0.16, 0.9999, 0.0001, 0.16, 0.101, nan, 0.16]
    nan_array = np.array(nan_list)

    print(longest_nan_run(nan_list))  # 3 - correct
    print(longest_nan_run_2(nan_list))  # 7 - incorrect
    print(longest_nan_run(nan_array))  # 0 - incorrect
    print(longest_nan_run_2(nan_array))  # 7 - incorrect

分析

在所有四种组合中,只有使用 original 函数检查 lists 才能正常工作。 modified 函数(使用np.isnan)似乎对列表和数组都以相同的方式工作。 检查数组时,original函数似乎找不到任何nan值。

谁能解释这些结果?同样,由于这个问题与 MSeifert 的问题有关,因此对他的结果的解释也可能解释我的问题(反之亦然)。


进一步调查

为了更好地了解正在发生的事情,我尝试打印出groupby 生成的组:

def longest_nan_run(sequence):
    print(list(list(group) for key, group in groupby(sequence) if key is nan))
    return max((sum(1 for _ in group) for key, group in groupby(sequence) if key is nan), default=0)


def longest_nan_run_2(sequence):
    print(list(list(group) for _, group in groupby(sequence, np.isnan)))
    return max((sum(1 for _ in group) for key, group in groupby(sequence, np.isnan)), default=0)

一个根本的区别(回想起来是有道理的)是​​原始函数(使用if key is nan)将过滤掉所有除了nan值,所以所有生成的组将仅包含 nan 值,如下所示:

[[nan, nan, nan], [nan]]

另一方面,modified 函数会将所有非nan 值分组到它们自己的组中,如下所示:

[[nan, nan, nan], [0.16, 1.0, 0.16, 0.99990000000000001, 0.0001, 0.16, 0.10100000000000001], [nan], [0.16]]

这解释了为什么修改后的函数在两种情况下都返回 7 - 它考虑值是“nan”或“不是nan”,并返回最长的连续序列。

这也意味着我对 groupby(sequence, keyfunc) 工作原理的假设是错误的,并且修改后的函数不是原始函数的可行替代方案。

不过,我仍然不确定在列表和数组上运行原始函数时结果的差异。

【问题讨论】:

【参考方案1】:

numpy 数组中的项目访问行为与列表中的不同:

nan_list[0] == nan_list[1]
# False
nan_list[0] is nan_list[1]
# True

nan_array[0] == nan_array[1]
# False
nan_array[0] is nan_array[1]
# False

x = np.array([1])
x[0] == x[0]
# True
x[0] is x[0]
# False

虽然列表包含对同一对象的引用,但 numpy 数组“包含”仅一个内存区域,并在每次访问元素时动态创建新的 Python 对象。 (感谢 user2357112 指出措辞不准确的地方。)

有道理,对吧?列表返回相同的对象,数组返回不同的对象 - 显然groupby 内部使用is 进行比较......但是等等,这并不容易!为什么groupby(np.array([1, 1, 1, 2, 3])) 可以正常工作?

答案隐藏在itertools C source中,第90行显示函数PyObject_RichCompareBool用于比较两个键。

rcmp = PyObject_RichCompareBool(gbo->tgtkey, gbo->currkey, Py_EQ);

虽然这基本上等同于在 Python 中使用 ==,但文档指出了一个特点:

注意如果 o1 和 o2 是同一个对象,PyObject_RichCompareBool() 将始终返回 1 用于 Py_EQ0 用于 Py_NE

这意味着实际上执行了这种比较(等效代码):

if o1 is o2:
    return True
else:
    return o1 == o2

所以对于列表,我们有相同的nan 对象,它们被标识为相等。相比之下,数组为我们提供了不同的对象,其值为 nan,它们与 == 进行比较 - 但 nan == nan 总是评估为 False

【讨论】:

"np.array 包含不同的对象(具有相同的值)" - 不,它根本不包含对象。这些对象是在元素访问时创建的,因此您甚至会为 x[0] is x[0] 获得 False。 @kazemakase 感谢您的回答。 :) 我看到你在我打字的时候把它贴出来了。您的回答提供了我没有的、我不知道的细节,所以我会仔细阅读。 @Tagc 欢迎您。这是一个有趣的问题。不要忘记刷新页面 - 我已经编辑了答案以改进叙述并合并 user23... 的评论。【参考方案2】:

好吧,我想我已经为自己描绘了一幅足够清晰的画面。

这里有两个因素在起作用:

我自己对 keyfunc 参数对 groupby 的作用的误解。 关于 Python 如何在数组和列表中表示 nan 值的(更有趣的)故事,最好在 this answer 中进行解释。

解释keyfunc 因素

来自documentation on groupby

每次key函数的值发生变化时都会产生一个break或者new group

来自documentation on np.isnan

对于标量输入,如果输入为 NaN,则结果是一个值为 True 的新布尔值;否则值为 False。

基于这两件事,我们推断当我们将keyfunc设置为np.isnan时,传递给groupyby的序列中的每个元素将映射到TrueFalse,这取决于是否是否为nan。这意味着键函数只会在nan 元素和非nan 元素之间的边界处发生变化,因此groupby 只会将序列拆分为nan 和非nan 元素的连续块.

相比之下,original 函数(使用groupby(sequence) if key is nan)将使用keyfuncidentity 函数(其默认值)。这自然会导致nan 身份的细微差别,这将在下面解释(以及上面的链接答案),但这里的重点是if key is nan 将过滤掉所有以非nan 元素为键的组。

解释nan 身份的细微差别

正如我在上面链接的答案中更好地解释的那样,在 Python 的内置列表中出现的所有 nan 实例似乎都是同一个实例。换句话说,列表中所有出现的nan 都指向内存中的同一个位置。与此相反,nan 元素是在使用 numpy 数组时动态生成的,因此都是单独的对象。

使用以下代码演示:

def longest_nan_run(sequence):
    print(id(nan))
    print([id(x) for x in sequence])
    return max((sum(1 for _ in group) for key, group in groupby(sequence) if key is nan), default=0)

当我使用原始问题中定义的 list 运行它时,我得到了这个输出(相同的元素被突出显示):

4436731128 [ 4436731128, 44436731128, 44436731128,4436730432,403575353,4036730432,4036730192,4436730048,4036730432,4436730432,4036730552, 44436731128 , 4436730432]

另一方面,数组元素在内存中的处理方式似乎大不相同:

4343850232 [4357386696、4357386720、4357386696、4357386720、4357386696、4357386720、4357386696、4357386720、696435738 /b>,, 4357386720, 4357386696, 4357386720]

该函数似乎在内存中用于存储这些值的两个不同位置之间交替。请注意,没有一个元素与过滤条件中使用的 nan 相同。

案例研究

我们现在可以将收集到的所有这些信息应用到实验中使用的四个不同案例中来解释我们的观察结果。

带列表的原始函数

在这种情况下,我们使用默认的identity 函数作为keyfunc,我们已经看到nan 在列表中的每次出现实际上都是同一个实例。过滤条件if key is nan 中使用的nan与列表中的nan 元素相同,导致groupby 在适当的位置打破列表并仅保留包含@ 的组987654359@。这就是为什么这个变体有效并且我们得到了3的正确结果。

带有数组的原始函数

同样,我们使用默认的identity 函数作为keyfunc,但这次所有出现的nan——包括条件过滤器中的一个——都指向不同的对象。这意味着条件过滤器if key is nan 对于所有 组将失败。由于我们找不到空集合的最大值,所以我们使用默认值0

带有列表和数组的修改函数

在这两种情况下,我们都将np.isnan 用作keyfunc。这将导致groupby 将序列拆分为nan 和非nan 元素的连续序列。

对于我们实验使用的列表/数组,nan元素的最长序列是[nan, nan, nan],它有三个元素,非nan元素的最长序列是[0.16, 1, 0.16, 0.9999, 0.0001, 0.16, 0.101],它有7 个元素。

max 将选择这两个序列中较长的一个,并在两种情况下都返回7

【讨论】:

“出现在 Python 内置列表中的所有 nan 实例似乎都是同一个实例” - 不,那只是因为您为您的所有 nan 使用了特定的 numpy.nan 对象列表。比如说,如果你为每个 nan 分别调用 float('nan'),你就会得到不同的对象。 另请注意,这已经在groupby 中发挥作用。 list(groupby(nan_list)) 给出的结果与 list(groupby(nan_array)) 不同。 @user2357112 所以说内置列表将简单地 point 指向用于构造它们的对象是正确的,而 numpy 数组将它们视为“模板”并且(例如)在每个元素“x”访问它时调用copy(x) @Tagc:列表总是引用作为元素给出的对象。 NumPy 数组将数值直接复制到非面向对象的后备存储中,并且仅在您尝试检索它们时构造对象来表示元素。 (除非你使用对象数组,这通常是个坏主意。)

以上是关于为啥'groupby(x,np.isnan)'的行为与'groupby(x)如果键是nan'不同?的主要内容,如果未能解决你的问题,请参考以下文章

np.isnan() == False,但 np.isnan() 不是 False

numpy通用函数

为啥groupby这么快?

为啥我的 MySQL 组这么慢?

Numpy:检查值是不是为 NaT

在 pandas 中删除 nan 行的更好方法