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



由于我们讨论了围绕numpynan的特殊性,我发现了一些我也不明白的东西。我发布这个问题主要是作为MSeifert的扩展,因为似乎我们的两个观察可能都有一个共同的原因。

早些时候,我发布了一个解决方案,该解决方案涉及在包含nan值的序列上使用itertools.groupby

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

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

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

分析

  • 在所有四种组合中,只有使用原始函数检查列表才能按预期工作。
  • 修改后的函数(使用np.isnan)似乎以相同的方式处理列表和数组。
  • 原始函数在检查数组时似乎找不到任何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]]

另一方面,修改后的函数会将所有非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)如何工作的假设是错误的,并且修改后的函数不是原始函数的可行替代方案。

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

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 源代码中,第 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.

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

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

  • 我自己对keyfunc论点对groupby的误解.
  • 关于Python如何表示数组和列表中的nan值的(更有趣的)故事,在这个答案中得到了最好的解释。

解释keyfunc因素

groupby的文档中:

每次键函数的值更改时,它都会生成一个中断或新组

np.isnan的文档:

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

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

相比之下,原始函数(使用groupby(sequence) if key is nan)将使用标识函数进行keyfunc(其默认值)。这自然会导致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)

当我使用原始问题中定义的列表运行它时,我获得了以下输出(突出显示相同的元素):

4436731128[443673112844436731128, 44436731128, 4436730432, 4435753536, 4436730432, 4436730192, 4436730048, 4436730432, 4436730552,44436731128, 4436730432]

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

4343850232 [4357386696, 4357386720,4357386696, 4357386720, 4357386696,4357386720, 4357386696, 4357386720, 4357386696, ,43573867204357386696, 4357386720]

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

案例研究

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

带列表的原始函数

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

最新更新