Skip to content

Commit b106108

Browse files
jbrockmendeljreback
authored andcommitted
REF: implement arithmetic, comparison, logical ops on arrays (#28431)
1 parent 8f9a75c commit b106108

File tree

2 files changed

+264
-150
lines changed

2 files changed

+264
-150
lines changed

pandas/core/ops/__init__.py

+9-146
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@
99

1010
import numpy as np
1111

12-
from pandas._libs import Timedelta, Timestamp, lib, ops as libops
12+
from pandas._libs import Timedelta, Timestamp, lib
1313
from pandas.errors import NullFrequencyError
1414
from pandas.util._decorators import Appender
1515

16-
from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike
1716
from pandas.core.dtypes.common import (
18-
ensure_object,
19-
is_bool_dtype,
2017
is_datetime64_dtype,
2118
is_extension_array_dtype,
2219
is_integer_dtype,
@@ -27,30 +24,28 @@
2724
)
2825
from pandas.core.dtypes.generic import (
2926
ABCDataFrame,
30-
ABCDatetimeArray,
31-
ABCDatetimeIndex,
3227
ABCExtensionArray,
3328
ABCIndexClass,
3429
ABCSeries,
35-
ABCTimedeltaArray,
36-
ABCTimedeltaIndex,
3730
)
3831
from pandas.core.dtypes.missing import isna, notna
3932

4033
from pandas._typing import ArrayLike
4134
from pandas.core.construction import array, extract_array
4235
from pandas.core.ops.array_ops import (
43-
comp_method_OBJECT_ARRAY,
36+
arithmetic_op,
37+
comparison_op,
4438
define_na_arithmetic_op,
45-
na_arithmetic_op,
39+
logical_op,
4640
)
41+
from pandas.core.ops.array_ops import comp_method_OBJECT_ARRAY # noqa:F401
4742
from pandas.core.ops.docstrings import (
4843
_arith_doc_FRAME,
4944
_flex_comp_doc_FRAME,
5045
_make_flex_doc,
5146
_op_descriptions,
5247
)
53-
from pandas.core.ops.invalid import invalid_comparison
48+
from pandas.core.ops.invalid import invalid_comparison # noqa:F401
5449
from pandas.core.ops.methods import ( # noqa:F401
5550
add_flex_arithmetic_methods,
5651
add_special_arithmetic_methods,
@@ -643,30 +638,8 @@ def wrapper(left, right):
643638
left, right = _align_method_SERIES(left, right)
644639
res_name = get_op_result_name(left, right)
645640

646-
keep_null_freq = isinstance(
647-
right,
648-
(
649-
ABCDatetimeIndex,
650-
ABCDatetimeArray,
651-
ABCTimedeltaIndex,
652-
ABCTimedeltaArray,
653-
Timestamp,
654-
),
655-
)
656-
657641
lvalues = extract_array(left, extract_numpy=True)
658-
rvalues = extract_array(right, extract_numpy=True)
659-
660-
rvalues = maybe_upcast_for_op(rvalues, lvalues.shape)
661-
662-
if should_extension_dispatch(left, rvalues) or isinstance(
663-
rvalues, (ABCTimedeltaArray, ABCDatetimeArray, Timestamp)
664-
):
665-
result = dispatch_to_extension_op(op, lvalues, rvalues, keep_null_freq)
666-
667-
else:
668-
with np.errstate(all="ignore"):
669-
result = na_arithmetic_op(lvalues, rvalues, op, str_rep, eval_kwargs)
642+
result = arithmetic_op(lvalues, right, op, str_rep, eval_kwargs)
670643

671644
# We do not pass dtype to ensure that the Series constructor
672645
# does inference in the case where `result` has object-dtype.
@@ -702,46 +675,10 @@ def wrapper(self, other):
702675
if isinstance(other, ABCSeries) and not self._indexed_same(other):
703676
raise ValueError("Can only compare identically-labeled Series objects")
704677

705-
other = lib.item_from_zerodim(other)
706-
if isinstance(other, list):
707-
# TODO: same for tuples?
708-
other = np.asarray(other)
709-
710-
if isinstance(other, (np.ndarray, ABCExtensionArray, ABCIndexClass)):
711-
# TODO: make this treatment consistent across ops and classes.
712-
# We are not catching all listlikes here (e.g. frozenset, tuple)
713-
# The ambiguous case is object-dtype. See GH#27803
714-
if len(self) != len(other):
715-
raise ValueError("Lengths must match to compare")
716-
717678
lvalues = extract_array(self, extract_numpy=True)
718679
rvalues = extract_array(other, extract_numpy=True)
719680

720-
if should_extension_dispatch(lvalues, rvalues):
721-
res_values = dispatch_to_extension_op(op, lvalues, rvalues)
722-
723-
elif is_scalar(rvalues) and isna(rvalues):
724-
# numpy does not like comparisons vs None
725-
if op is operator.ne:
726-
res_values = np.ones(len(lvalues), dtype=bool)
727-
else:
728-
res_values = np.zeros(len(lvalues), dtype=bool)
729-
730-
elif is_object_dtype(lvalues.dtype):
731-
res_values = comp_method_OBJECT_ARRAY(op, lvalues, rvalues)
732-
733-
else:
734-
op_name = "__{op}__".format(op=op.__name__)
735-
method = getattr(lvalues, op_name)
736-
with np.errstate(all="ignore"):
737-
res_values = method(rvalues)
738-
739-
if res_values is NotImplemented:
740-
res_values = invalid_comparison(lvalues, rvalues, op)
741-
if is_scalar(res_values):
742-
raise TypeError(
743-
"Could not compare {typ} type with Series".format(typ=type(rvalues))
744-
)
681+
res_values = comparison_op(lvalues, rvalues, op)
745682

746683
result = self._constructor(res_values, index=self.index)
747684
result = finalizer(result)
@@ -762,58 +699,7 @@ def _bool_method_SERIES(cls, op, special):
762699
"""
763700
op_name = _get_op_name(op, special)
764701

765-
def na_op(x, y):
766-
try:
767-
result = op(x, y)
768-
except TypeError:
769-
assert not isinstance(y, (list, ABCSeries, ABCIndexClass))
770-
if isinstance(y, np.ndarray):
771-
# bool-bool dtype operations should be OK, should not get here
772-
assert not (is_bool_dtype(x.dtype) and is_bool_dtype(y.dtype))
773-
x = ensure_object(x)
774-
y = ensure_object(y)
775-
result = libops.vec_binop(x, y, op)
776-
else:
777-
# let null fall thru
778-
assert lib.is_scalar(y)
779-
if not isna(y):
780-
y = bool(y)
781-
try:
782-
result = libops.scalar_binop(x, y, op)
783-
except (
784-
TypeError,
785-
ValueError,
786-
AttributeError,
787-
OverflowError,
788-
NotImplementedError,
789-
):
790-
raise TypeError(
791-
"cannot compare a dtyped [{dtype}] array "
792-
"with a scalar of type [{typ}]".format(
793-
dtype=x.dtype, typ=type(y).__name__
794-
)
795-
)
796-
797-
return result
798-
799-
fill_int = lambda x: x
800-
801-
def fill_bool(x, left=None):
802-
# if `left` is specifically not-boolean, we do not cast to bool
803-
if x.dtype.kind in ["c", "f", "O"]:
804-
# dtypes that can hold NA
805-
mask = isna(x)
806-
if mask.any():
807-
x = x.astype(object)
808-
x[mask] = False
809-
810-
if left is None or is_bool_dtype(left.dtype):
811-
x = x.astype(bool)
812-
return x
813-
814702
def wrapper(self, other):
815-
is_self_int_dtype = is_integer_dtype(self.dtype)
816-
817703
self, other = _align_method_SERIES(self, other, align_asobject=True)
818704
res_name = get_op_result_name(self, other)
819705

@@ -829,33 +715,10 @@ def wrapper(self, other):
829715
# Defer to DataFrame implementation; fail early
830716
return NotImplemented
831717

832-
other = lib.item_from_zerodim(other)
833-
if is_list_like(other) and not hasattr(other, "dtype"):
834-
# e.g. list, tuple
835-
other = construct_1d_object_array_from_listlike(other)
836-
837718
lvalues = extract_array(self, extract_numpy=True)
838719
rvalues = extract_array(other, extract_numpy=True)
839720

840-
if should_extension_dispatch(self, rvalues):
841-
res_values = dispatch_to_extension_op(op, lvalues, rvalues)
842-
843-
else:
844-
if isinstance(rvalues, (ABCSeries, ABCIndexClass, np.ndarray)):
845-
is_other_int_dtype = is_integer_dtype(rvalues.dtype)
846-
rvalues = rvalues if is_other_int_dtype else fill_bool(rvalues, lvalues)
847-
848-
else:
849-
# i.e. scalar
850-
is_other_int_dtype = lib.is_integer(rvalues)
851-
852-
# For int vs int `^`, `|`, `&` are bitwise operators and return
853-
# integer dtypes. Otherwise these are boolean ops
854-
filler = fill_int if is_self_int_dtype and is_other_int_dtype else fill_bool
855-
856-
res_values = na_op(lvalues, rvalues)
857-
res_values = filler(res_values)
858-
721+
res_values = logical_op(lvalues, rvalues, op)
859722
result = self._constructor(res_values, index=self.index, name=res_name)
860723
return finalizer(result)
861724

0 commit comments

Comments
 (0)