Skip to content

Commit e5a99c6

Browse files
jbrockmendeljreback
authored andcommitted
Fix Series v Index bool ops (#22173)
1 parent f67b90d commit e5a99c6

File tree

3 files changed

+70
-51
lines changed

3 files changed

+70
-51
lines changed

doc/source/whatsnew/v0.24.0.txt

-2
Original file line numberDiff line numberDiff line change
@@ -819,5 +819,3 @@ Other
819819
- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly.
820820
- Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`)
821821
-
822-
-
823-
-

pandas/core/ops.py

+33-25
Original file line numberDiff line numberDiff line change
@@ -1525,23 +1525,22 @@ def _bool_method_SERIES(cls, op, special):
15251525
Wrapper function for Series arithmetic operations, to avoid
15261526
code duplication.
15271527
"""
1528+
op_name = _get_op_name(op, special)
15281529

15291530
def na_op(x, y):
15301531
try:
15311532
result = op(x, y)
15321533
except TypeError:
1533-
if isinstance(y, list):
1534-
y = construct_1d_object_array_from_listlike(y)
1535-
1536-
if isinstance(y, (np.ndarray, ABCSeries, ABCIndexClass)):
1537-
if (is_bool_dtype(x.dtype) and is_bool_dtype(y.dtype)):
1538-
result = op(x, y) # when would this be hit?
1539-
else:
1540-
x = ensure_object(x)
1541-
y = ensure_object(y)
1542-
result = libops.vec_binop(x, y, op)
1534+
assert not isinstance(y, (list, ABCSeries, ABCIndexClass))
1535+
if isinstance(y, np.ndarray):
1536+
# bool-bool dtype operations should be OK, should not get here
1537+
assert not (is_bool_dtype(x) and is_bool_dtype(y))
1538+
x = ensure_object(x)
1539+
y = ensure_object(y)
1540+
result = libops.vec_binop(x, y, op)
15431541
else:
15441542
# let null fall thru
1543+
assert lib.is_scalar(y)
15451544
if not isna(y):
15461545
y = bool(y)
15471546
try:
@@ -1561,33 +1560,42 @@ def wrapper(self, other):
15611560
is_self_int_dtype = is_integer_dtype(self.dtype)
15621561

15631562
self, other = _align_method_SERIES(self, other, align_asobject=True)
1563+
res_name = get_op_result_name(self, other)
15641564

15651565
if isinstance(other, ABCDataFrame):
15661566
# Defer to DataFrame implementation; fail early
15671567
return NotImplemented
15681568

1569-
elif isinstance(other, ABCSeries):
1570-
name = get_op_result_name(self, other)
1569+
elif isinstance(other, (ABCSeries, ABCIndexClass)):
15711570
is_other_int_dtype = is_integer_dtype(other.dtype)
15721571
other = fill_int(other) if is_other_int_dtype else fill_bool(other)
15731572

1574-
filler = (fill_int if is_self_int_dtype and is_other_int_dtype
1575-
else fill_bool)
1576-
1577-
res_values = na_op(self.values, other.values)
1578-
unfilled = self._constructor(res_values,
1579-
index=self.index, name=name)
1580-
return filler(unfilled)
1573+
ovalues = other.values
1574+
finalizer = lambda x: x
15811575

15821576
else:
15831577
# scalars, list, tuple, np.array
1584-
filler = (fill_int if is_self_int_dtype and
1585-
is_integer_dtype(np.asarray(other)) else fill_bool)
1586-
1587-
res_values = na_op(self.values, other)
1588-
unfilled = self._constructor(res_values, index=self.index)
1589-
return filler(unfilled).__finalize__(self)
1578+
is_other_int_dtype = is_integer_dtype(np.asarray(other))
1579+
if is_list_like(other) and not isinstance(other, np.ndarray):
1580+
# TODO: Can we do this before the is_integer_dtype check?
1581+
# could the is_integer_dtype check be checking the wrong
1582+
# thing? e.g. other = [[0, 1], [2, 3], [4, 5]]?
1583+
other = construct_1d_object_array_from_listlike(other)
1584+
1585+
ovalues = other
1586+
finalizer = lambda x: x.__finalize__(self)
1587+
1588+
# For int vs int `^`, `|`, `&` are bitwise operators and return
1589+
# integer dtypes. Otherwise these are boolean ops
1590+
filler = (fill_int if is_self_int_dtype and is_other_int_dtype
1591+
else fill_bool)
1592+
res_values = na_op(self.values, ovalues)
1593+
unfilled = self._constructor(res_values,
1594+
index=self.index, name=res_name)
1595+
filled = filler(unfilled)
1596+
return finalizer(filled)
15901597

1598+
wrapper.__name__ = op_name
15911599
return wrapper
15921600

15931601

pandas/tests/series/test_operators.py

+37-24
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
NaT, date_range, timedelta_range, Categorical)
1515
from pandas.core.indexes.datetimes import Timestamp
1616
import pandas.core.nanops as nanops
17+
from pandas.core import ops
1718

1819
from pandas.compat import range
1920
from pandas import compat
@@ -425,30 +426,6 @@ def test_comparison_flex_alignment_fill(self):
425426
exp = pd.Series([True, True, False, False], index=list('abcd'))
426427
assert_series_equal(left.gt(right, fill_value=0), exp)
427428

428-
def test_logical_ops_with_index(self):
429-
# GH22092
430-
ser = Series([True, True, False, False])
431-
idx1 = Index([True, False, True, False])
432-
idx2 = Index([1, 0, 1, 0])
433-
434-
expected = Series([True, False, False, False])
435-
result1 = ser & idx1
436-
assert_series_equal(result1, expected)
437-
result2 = ser & idx2
438-
assert_series_equal(result2, expected)
439-
440-
expected = Series([True, True, True, False])
441-
result1 = ser | idx1
442-
assert_series_equal(result1, expected)
443-
result2 = ser | idx2
444-
assert_series_equal(result2, expected)
445-
446-
expected = Series([False, True, True, False])
447-
result1 = ser ^ idx1
448-
assert_series_equal(result1, expected)
449-
result2 = ser ^ idx2
450-
assert_series_equal(result2, expected)
451-
452429
def test_ne(self):
453430
ts = Series([3, 4, 5, 6, 7], [3, 4, 5, 6, 7], dtype=float)
454431
expected = [True, True, False, True, True]
@@ -627,6 +604,42 @@ def test_ops_datetimelike_align(self):
627604
result = (dt2.to_frame() - dt.to_frame())[0]
628605
assert_series_equal(result, expected)
629606

607+
@pytest.mark.parametrize('op', [
608+
operator.and_,
609+
operator.or_,
610+
operator.xor,
611+
pytest.param(ops.rand_,
612+
marks=pytest.mark.xfail(reason="GH#22092 Index "
613+
"implementation returns "
614+
"Index",
615+
raises=AssertionError,
616+
strict=True)),
617+
pytest.param(ops.ror_,
618+
marks=pytest.mark.xfail(reason="GH#22092 Index "
619+
"implementation raises",
620+
raises=ValueError, strict=True)),
621+
pytest.param(ops.rxor,
622+
marks=pytest.mark.xfail(reason="GH#22092 Index "
623+
"implementation raises",
624+
raises=TypeError, strict=True))
625+
])
626+
def test_bool_ops_with_index(self, op):
627+
# GH#22092, GH#19792
628+
ser = Series([True, True, False, False])
629+
idx1 = Index([True, False, True, False])
630+
idx2 = Index([1, 0, 1, 0])
631+
632+
expected = Series([op(ser[n], idx1[n]) for n in range(len(ser))])
633+
634+
result = op(ser, idx1)
635+
assert_series_equal(result, expected)
636+
637+
expected = Series([op(ser[n], idx2[n]) for n in range(len(ser))],
638+
dtype=bool)
639+
640+
result = op(ser, idx2)
641+
assert_series_equal(result, expected)
642+
630643
def test_operators_bitwise(self):
631644
# GH 9016: support bitwise op for integer types
632645
index = list('bca')

0 commit comments

Comments
 (0)