布尔 pandas 之间的操作对称性破缺。具有不等索引的系列

2024-05-28

隐式索引匹配pandas用于不同之间的操作DataFrame/Series很棒,而且大多数时候,它都有效。

但是,我偶然发现了一个无法按预期工作的示例:

import pandas as pd # 0.21.0
import numpy as np # 1.13.3
x = pd.Series([True, False, True, True], index = range(4))
y = pd.Series([False, True, True, False], index = [2,4,3,5])

# logical AND: this works, symmetric as it should be
pd.concat([x, y, x & y, y & x], keys = ['x', 'y', 'x&y', 'y&x'], axis = 1)
#        x      y    x&y    y&x
# 0   True    NaN  False  False
# 1  False    NaN  False  False
# 2   True  False  False  False
# 3   True   True   True   True
# 4    NaN   True  False  False
# 5    NaN  False  False  False

# but logical OR is not symmetric anymore (same for XOR: x^y vs. y^x)
pd.concat([x, y, x | y, y | x], keys = ['x', 'y', 'x|y', 'y|x'], axis = 1)
#        x      y    x|y    y|x
# 0   True    NaN   True  False <-- INCONSISTENT!
# 1  False    NaN  False  False
# 2   True  False   True   True
# 3   True   True   True   True
# 4    NaN   True  False   True <-- INCONSISTENT!
# 5    NaN  False  False  False

经过一番研究,我发现了两点似乎相关:

  • bool(np.nan) equals True, cf. https://stackoverflow.com/a/15686477/2965879 https://stackoverflow.com/a/15686477/2965879
  • |决心np.bitwise_or, 而不是np.logical_or, cf. https://stackoverflow.com/a/37132854/2965879 https://stackoverflow.com/a/37132854/2965879

但最终,最令人兴奋的似乎是熊猫确实从nan to False 在某一点。看了上面的内容,似乎出现了这种情况after呼叫np.bitwise_or,虽然我认为这应该发生before?

特别是,使用np.logical_or没有帮助,因为它错过了索引对齐pandas是的,而且我也不想要np.nan or False等于True。 (换句话说,答案https://stackoverflow.com/a/37132854/2965879 https://stackoverflow.com/a/37132854/2965879没有帮助。)

我认为如果提供了这个美妙的语法糖,它应该尽可能一致*,所以|应该是对称的。当总是对称的东西突然不再对称时,调试真的很难(正如我所经历的那样)。

最后,问题是:是否有任何可行的解决方法(例如超载某些东西)来挽救x|y == y|x,并且理想情况下(宽松地说)nan | True == True == True | nan and nan | False == False == False | nan?

*即使德摩根定律失效,无论如何 -~(x&y)不能完全匹配~y|~x因为 NaN 仅出现在索引对齐处(因此不受先前否定的影响)。


在对 pandas 进行了一些探索之后,我发现有一个函数叫做pandas.core.ops._bool_method_SERIES这是包装 Series 对象的布尔运算符的几个工厂函数之一。

>>> f = pandas.Series.__or__
>>> f #the actual function you call when you do x|y
<function _bool_method_SERIES.<locals>.wrapper at 0x107436bf8>
>>> f.__closure__[0].cell_contents
    #it holds a reference to the other function defined in this factory na_op
<function _bool_method_SERIES.<locals>.na_op at 0x107436b70>
>>> f.__closure__[0].cell_contents.__closure__[0].cell_contents
    #and na_op has a reference to the built-in function or_
<built-in function or_>

这意味着我们理论上可以定义自己的方法来执行逻辑或正确的逻辑,首先让我们看看它实际上会做什么(请记住,如果无法执行操作,则运算符函数预计会引发 TypeError )

def test_logical_or(a,b):
    print("**** calling logical_or with ****")
    print(type(a), a)
    print(type(b), b)
    print("******")
    raise TypeError("my_logical_or isn't implemented")

#make the wrapper method
wrapper = pd.core.ops._bool_method_SERIES(test_logical_or, None,None)
pd.Series.logical_or = wrapper #insert method


x = pd.Series([True, False, True, True], index = range(4))
y = pd.Series([False, True, True, False], index = [2,4,3,5])

z = x.logical_or(y) #lets try it out!

print(x,y,z, sep="\n")

当它运行时(至少对于 pandas vs 0.19.1)

**** calling logical_or with ****
<class 'numpy.ndarray'> [True False True True nan nan]
<class 'numpy.ndarray'> [False False False  True  True False]
******
**** calling logical_or with ****
<class 'bool'> True
<class 'bool'> False
******
Traceback (most recent call last):
   ...

所以看起来它试图用两个 numpy 数组调用我们的方法,无论出于何种原因,第二个数组具有nan值已替换为False但不是第一个,这可能是我们对称性破缺的原因。然后当失败时它再次尝试我会假设元素明智。

因此,为了使其正常工作,您至少可以显式检查两个参数是否都是 numpy 数组,尝试转换所有nan第一个到的条目False then return np.logical_or(a,b)。我假设如果出现其他情况,我们只会提出错误。

def my_logical_or(a,b):
    if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
        a[np.isnan(a.astype(float))] = False
        b[np.isnan(b.astype(float))] = False
        return np.logical_or(a,b)
    else:
        raise TypeError("custom logical or is only implemented for numpy arrays")

wrapper = pd.core.ops._bool_method_SERIES(my_logical_or, None,None)
pd.Series.logical_or = wrapper


x = pd.Series([True, False, True, True], index = range(4))
y = pd.Series([False, True, True, False], index = [2,4,3,5])

z = pd.concat([x, y, x.logical_or(y), y.logical_or(x)], keys = ['x', 'y', 'x|y', 'y|x'], axis = 1)
print(z)
#        x      y    x|y    y|x
# 0   True    NaN   True   True
# 1  False    NaN  False  False <-- same!
# 2   True  False   True   True
# 3   True   True   True   True
# 4    NaN   True   True   True <-- same!
# 5    NaN  False  False  False

所以这可能是你的解决方法,我不建议修改Series.__or__因为我们不知道还有谁会使用它,并且不想破坏任何期望默认行为的代码。


或者,我们可以修改源代码pandas.core.ops943线 https://github.com/pandas-dev/pandas/blob/master/pandas/core/ops.py#L943填写NaN值为 False(或 0)的self在相同的它的方式other https://github.com/pandas-dev/pandas/blob/master/pandas/core/ops.py#L939,所以我们要改变这一行:

    return filler(self._constructor(na_op(self.values, other.values),
                                    index=self.index, name=name))

to use filler(self).values代替self.values:

    return filler(self._constructor(na_op(filler(self).values, other.values),
                                    index=self.index, name=name))

这也解决了这个问题or and xor不对称,但是,我不会推荐这样做,因为它可能会破坏其他代码,我个人对 pandas 没有足够的经验来确定这在不同情况下会发生什么变化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

布尔 pandas 之间的操作对称性破缺。具有不等索引的系列 的相关文章

随机推荐