Skip to content

Commit 7c59260

Browse files
authored
BUG: BooleanArray match non-masked behavior div/pow/mod (#46063)
1 parent 1efa4fb commit 7c59260

File tree

5 files changed

+108
-47
lines changed

5 files changed

+108
-47
lines changed

doc/source/whatsnew/v1.5.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ Time Zones
328328
Numeric
329329
^^^^^^^
330330
- Bug in operations with array-likes with ``dtype="boolean"`` and :attr:`NA` incorrectly altering the array in-place (:issue:`45421`)
331+
- Bug in division, ``pow`` and ``mod`` operations on array-likes with ``dtype="boolean"`` not being like their ``np.bool_`` counterparts (:issue:`46063`)
331332
- Bug in multiplying a :class:`Series` with ``IntegerDtype`` or ``FloatingDtype`` by an array-like with ``timedelta64[ns]`` dtype incorrectly raising (:issue:`45622`)
332333
-
333334

pandas/core/arrays/masked.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -626,10 +626,21 @@ def _arith_method(self, other, op):
626626
if other is libmissing.NA:
627627
result = np.ones_like(self._data)
628628
if self.dtype.kind == "b":
629-
if op_name in {"floordiv", "rfloordiv", "mod", "rmod", "pow", "rpow"}:
629+
if op_name in {
630+
"floordiv",
631+
"rfloordiv",
632+
"pow",
633+
"rpow",
634+
"truediv",
635+
"rtruediv",
636+
}:
637+
# GH#41165 Try to match non-masked Series behavior
638+
# This is still imperfect GH#46043
639+
raise NotImplementedError(
640+
f"operator '{op_name}' not implemented for bool dtypes"
641+
)
642+
elif op_name in {"mod", "rmod"}:
630643
dtype = "int8"
631-
elif op_name in {"truediv", "rtruediv"}:
632-
dtype = "float64"
633644
else:
634645
dtype = "bool"
635646
result = result.astype(dtype)
@@ -646,12 +657,6 @@ def _arith_method(self, other, op):
646657
# types with respect to floordiv-by-zero
647658
pd_op = op
648659

649-
elif self.dtype.kind == "b" and (
650-
"div" in op_name or "pow" in op_name or "mod" in op_name
651-
):
652-
# TODO(GH#41165): should these be disallowed?
653-
pd_op = op
654-
655660
with np.errstate(all="ignore"):
656661
result = pd_op(self._data, other)
657662

pandas/tests/arrays/boolean/test_arithmetic.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import pandas as pd
77
import pandas._testing as tm
8-
from pandas.arrays import FloatingArray
98

109

1110
@pytest.fixture
@@ -55,15 +54,13 @@ def test_sub(left_array, right_array):
5554

5655

5756
def test_div(left_array, right_array):
58-
result = left_array / right_array
59-
expected = FloatingArray(
60-
np.array(
61-
[1.0, np.inf, np.nan, 0.0, np.nan, np.nan, np.nan, np.nan, np.nan],
62-
dtype="float64",
63-
),
64-
np.array([False, False, True, False, False, True, True, True, True]),
65-
)
66-
tm.assert_extension_array_equal(result, expected)
57+
msg = "operator '.*' not implemented for bool dtypes"
58+
with pytest.raises(NotImplementedError, match=msg):
59+
# check that we are matching the non-masked Series behavior
60+
pd.Series(left_array._data) / pd.Series(right_array._data)
61+
62+
with pytest.raises(NotImplementedError, match=msg):
63+
left_array / right_array
6764

6865

6966
@pytest.mark.parametrize(
@@ -76,6 +73,11 @@ def test_div(left_array, right_array):
7673
)
7774
def test_op_int8(left_array, right_array, opname):
7875
op = getattr(operator, opname)
76+
if opname != "mod":
77+
msg = "operator '.*' not implemented for bool dtypes"
78+
with pytest.raises(NotImplementedError, match=msg):
79+
result = op(left_array, right_array)
80+
return
7981
result = op(left_array, right_array)
8082
expected = op(left_array.astype("Int8"), right_array.astype("Int8"))
8183
tm.assert_extension_array_equal(result, expected)

pandas/tests/arrays/masked/test_arithmetic.py

+63-24
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ def check_skip(data, op_name):
2929
pytest.skip("subtract not implemented for boolean")
3030

3131

32+
def is_bool_not_implemented(data, op_name):
33+
# match non-masked behavior
34+
return data.dtype.kind == "b" and op_name.strip("_").lstrip("r") in [
35+
"pow",
36+
"truediv",
37+
"floordiv",
38+
]
39+
40+
3241
# Test equivalence of scalars, numpy arrays with array ops
3342
# -----------------------------------------------------------------------------
3443

@@ -42,9 +51,16 @@ def test_array_scalar_like_equivalence(data, all_arithmetic_operators):
4251

4352
# TODO also add len-1 array (np.array([scalar], dtype=data.dtype.numpy_dtype))
4453
for scalar in [scalar, data.dtype.type(scalar)]:
45-
result = op(data, scalar)
46-
expected = op(data, scalar_array)
47-
tm.assert_extension_array_equal(result, expected)
54+
if is_bool_not_implemented(data, all_arithmetic_operators):
55+
msg = "operator '.*' not implemented for bool dtypes"
56+
with pytest.raises(NotImplementedError, match=msg):
57+
op(data, scalar)
58+
with pytest.raises(NotImplementedError, match=msg):
59+
op(data, scalar_array)
60+
else:
61+
result = op(data, scalar)
62+
expected = op(data, scalar_array)
63+
tm.assert_extension_array_equal(result, expected)
4864

4965

5066
def test_array_NA(data, all_arithmetic_operators):
@@ -56,6 +72,15 @@ def test_array_NA(data, all_arithmetic_operators):
5672
scalar_array = pd.array([pd.NA] * len(data), dtype=data.dtype)
5773

5874
mask = data._mask.copy()
75+
76+
if is_bool_not_implemented(data, all_arithmetic_operators):
77+
msg = "operator '.*' not implemented for bool dtypes"
78+
with pytest.raises(NotImplementedError, match=msg):
79+
op(data, scalar)
80+
# GH#45421 check op doesn't alter data._mask inplace
81+
tm.assert_numpy_array_equal(mask, data._mask)
82+
return
83+
5984
result = op(data, scalar)
6085
# GH#45421 check op doesn't alter data._mask inplace
6186
tm.assert_numpy_array_equal(mask, data._mask)
@@ -74,6 +99,14 @@ def test_numpy_array_equivalence(data, all_arithmetic_operators):
7499
numpy_array = np.array([scalar] * len(data), dtype=data.dtype.numpy_dtype)
75100
pd_array = pd.array(numpy_array, dtype=data.dtype)
76101

102+
if is_bool_not_implemented(data, all_arithmetic_operators):
103+
msg = "operator '.*' not implemented for bool dtypes"
104+
with pytest.raises(NotImplementedError, match=msg):
105+
op(data, numpy_array)
106+
with pytest.raises(NotImplementedError, match=msg):
107+
op(data, pd_array)
108+
return
109+
77110
result = op(data, numpy_array)
78111
expected = op(data, pd_array)
79112
tm.assert_extension_array_equal(result, expected)
@@ -91,6 +124,14 @@ def test_frame(data, all_arithmetic_operators):
91124
# DataFrame with scalar
92125
df = pd.DataFrame({"A": data})
93126

127+
if is_bool_not_implemented(data, all_arithmetic_operators):
128+
msg = "operator '.*' not implemented for bool dtypes"
129+
with pytest.raises(NotImplementedError, match=msg):
130+
op(df, scalar)
131+
with pytest.raises(NotImplementedError, match=msg):
132+
op(data, scalar)
133+
return
134+
94135
result = op(df, scalar)
95136
expected = pd.DataFrame({"A": op(data, scalar)})
96137
tm.assert_frame_equal(result, expected)
@@ -101,30 +142,25 @@ def test_series(data, all_arithmetic_operators):
101142
op = tm.get_op_from_name(all_arithmetic_operators)
102143
check_skip(data, all_arithmetic_operators)
103144

104-
s = pd.Series(data)
145+
ser = pd.Series(data)
105146

106-
# Series with scalar
107-
result = op(s, scalar)
108-
expected = pd.Series(op(data, scalar))
109-
tm.assert_series_equal(result, expected)
147+
others = [
148+
scalar,
149+
np.array([scalar] * len(data), dtype=data.dtype.numpy_dtype),
150+
pd.array([scalar] * len(data), dtype=data.dtype),
151+
pd.Series([scalar] * len(data), dtype=data.dtype),
152+
]
110153

111-
# Series with np.ndarray
112-
other = np.array([scalar] * len(data), dtype=data.dtype.numpy_dtype)
113-
result = op(s, other)
114-
expected = pd.Series(op(data, other))
115-
tm.assert_series_equal(result, expected)
154+
for other in others:
155+
if is_bool_not_implemented(data, all_arithmetic_operators):
156+
msg = "operator '.*' not implemented for bool dtypes"
157+
with pytest.raises(NotImplementedError, match=msg):
158+
op(ser, other)
116159

117-
# Series with pd.array
118-
other = pd.array([scalar] * len(data), dtype=data.dtype)
119-
result = op(s, other)
120-
expected = pd.Series(op(data, other))
121-
tm.assert_series_equal(result, expected)
122-
123-
# Series with Series
124-
other = pd.Series([scalar] * len(data), dtype=data.dtype)
125-
result = op(s, other)
126-
expected = pd.Series(op(data, other.array))
127-
tm.assert_series_equal(result, expected)
160+
else:
161+
result = op(ser, other)
162+
expected = pd.Series(op(data, other))
163+
tm.assert_series_equal(result, expected)
128164

129165

130166
# Test generic characteristics / errors
@@ -169,6 +205,9 @@ def test_error_len_mismatch(data, all_arithmetic_operators):
169205
r"numpy boolean subtract, the `\-` operator, is not supported, use "
170206
r"the bitwise_xor, the `\^` operator, or the logical_xor function instead"
171207
)
208+
elif is_bool_not_implemented(data, all_arithmetic_operators):
209+
msg = "operator '.*' not implemented for bool dtypes"
210+
err = NotImplementedError
172211

173212
for other in [other, np.array(other)]:
174213
with pytest.raises(err, match=msg):

pandas/tests/extension/test_boolean.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ class TestArithmeticOps(base.BaseArithmeticOpsTests):
108108

109109
def check_opname(self, s, op_name, other, exc=None):
110110
# overwriting to indicate ops don't raise an error
111-
super().check_opname(s, op_name, other, exc=None)
111+
exc = None
112+
if op_name.strip("_").lstrip("r") in ["pow", "truediv", "floordiv"]:
113+
# match behavior with non-masked bool dtype
114+
exc = NotImplementedError
115+
super().check_opname(s, op_name, other, exc=exc)
112116

113117
def _check_op(self, obj, op, other, op_name, exc=NotImplementedError):
114118
if exc is None:
@@ -144,9 +148,19 @@ def _check_op(self, obj, op, other, op_name, exc=NotImplementedError):
144148
with pytest.raises(exc):
145149
op(obj, other)
146150

147-
def _check_divmod_op(self, s, op, other, exc=None):
148-
# override to not raise an error
149-
super()._check_divmod_op(s, op, other, None)
151+
@pytest.mark.xfail(
152+
reason="Inconsistency between floordiv and divmod; we raise for floordiv "
153+
"but not for divmod. This matches what we do for non-masked bool dtype."
154+
)
155+
def test_divmod_series_array(self, data, data_for_twos):
156+
super().test_divmod_series_array(data, data_for_twos)
157+
158+
@pytest.mark.xfail(
159+
reason="Inconsistency between floordiv and divmod; we raise for floordiv "
160+
"but not for divmod. This matches what we do for non-masked bool dtype."
161+
)
162+
def test_divmod(self, data):
163+
super().test_divmod(data)
150164

151165

152166
class TestComparisonOps(base.BaseComparisonOpsTests):

0 commit comments

Comments
 (0)