From ac2f7469e166d55d33c4c735a82fe227e0aab092 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 27 Feb 2022 12:01:14 -0800 Subject: [PATCH] ENH: IntegerArray bitwise operations (#46104) --- doc/source/whatsnew/v1.5.0.rst | 2 +- pandas/core/arrays/boolean.py | 8 +++--- pandas/core/arrays/masked.py | 2 ++ .../tests/arrays/floating/test_arithmetic.py | 12 +++++++++ .../tests/arrays/integer/test_arithmetic.py | 27 +++++++++++++++++++ 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 7e470a51858ce..b472067e28328 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -39,7 +39,7 @@ Other enhancements - :meth:`.GroupBy.min` and :meth:`.GroupBy.max` now supports `Numba `_ execution with the ``engine`` keyword (:issue:`45428`) - Implemented a ``bool``-dtype :class:`Index`, passing a bool-dtype array-like to ``pd.Index`` will now retain ``bool`` dtype instead of casting to ``object`` (:issue:`45061`) - Implemented a complex-dtype :class:`Index`, passing a complex-dtype array-like to ``pd.Index`` will now retain complex dtype instead of casting to ``object`` (:issue:`45845`) - +- :class:`Series` and :class:`DataFrame` with ``IntegerDtype`` now supports bitwise operations (:issue:`34463`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/arrays/boolean.py b/pandas/core/arrays/boolean.py index 10cb7683e57d4..10469f2aef9ea 100644 --- a/pandas/core/arrays/boolean.py +++ b/pandas/core/arrays/boolean.py @@ -375,9 +375,9 @@ def _logical_method(self, other, op): result, mask = ops.kleene_or(self._data, other, self._mask, mask) elif op.__name__ in {"and_", "rand_"}: result, mask = ops.kleene_and(self._data, other, self._mask, mask) - elif op.__name__ in {"xor", "rxor"}: + else: + # i.e. xor, rxor result, mask = ops.kleene_xor(self._data, other, self._mask, mask) - # error: Argument 2 to "BooleanArray" has incompatible type "Optional[Any]"; - # expected "ndarray" - return BooleanArray(result, mask) # type: ignore[arg-type] + # i.e. BooleanArray + return self._maybe_mask_result(result, mask) diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 248eef0bb4430..ffa65577e4d5e 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -675,6 +675,8 @@ def _arith_method(self, other, op): return self._maybe_mask_result(result, mask) + _logical_method = _arith_method + def _cmp_method(self, other, op) -> BooleanArray: from pandas.core.arrays import BooleanArray diff --git a/pandas/tests/arrays/floating/test_arithmetic.py b/pandas/tests/arrays/floating/test_arithmetic.py index d0f25d2cd5026..ec7419d6346a8 100644 --- a/pandas/tests/arrays/floating/test_arithmetic.py +++ b/pandas/tests/arrays/floating/test_arithmetic.py @@ -218,3 +218,15 @@ def test_unary_float_operators(float_ea_dtype, source, neg_target, abs_target): tm.assert_extension_array_equal(pos_result, arr) assert not tm.shares_memory(pos_result, arr) tm.assert_extension_array_equal(abs_result, abs_target) + + +def test_bitwise(dtype): + left = pd.array([1, None, 3, 4], dtype=dtype) + right = pd.array([None, 3, 5, 4], dtype=dtype) + + with pytest.raises(TypeError, match="unsupported operand type"): + left | right + with pytest.raises(TypeError, match="unsupported operand type"): + left & right + with pytest.raises(TypeError, match="unsupported operand type"): + left ^ right diff --git a/pandas/tests/arrays/integer/test_arithmetic.py b/pandas/tests/arrays/integer/test_arithmetic.py index dc5071e7585a9..e6a085ceb4d29 100644 --- a/pandas/tests/arrays/integer/test_arithmetic.py +++ b/pandas/tests/arrays/integer/test_arithmetic.py @@ -323,3 +323,30 @@ def test_values_multiplying_large_series_by_NA(): expected = pd.Series([pd.NA] * 10001) tm.assert_series_equal(result, expected) + + +def test_bitwise(dtype): + left = pd.array([1, None, 3, 4], dtype=dtype) + right = pd.array([None, 3, 5, 4], dtype=dtype) + + result = left | right + expected = pd.array([None, None, 3 | 5, 4 | 4], dtype=dtype) + tm.assert_extension_array_equal(result, expected) + + result = left & right + expected = pd.array([None, None, 3 & 5, 4 & 4], dtype=dtype) + tm.assert_extension_array_equal(result, expected) + + result = left ^ right + expected = pd.array([None, None, 3 ^ 5, 4 ^ 4], dtype=dtype) + tm.assert_extension_array_equal(result, expected) + + # TODO: desired behavior when operating with boolean? defer? + + floats = right.astype("Float64") + with pytest.raises(TypeError, match="unsupported operand type"): + left | floats + with pytest.raises(TypeError, match="unsupported operand type"): + left & floats + with pytest.raises(TypeError, match="unsupported operand type"): + left ^ floats