1"""
2Functions for arithmetic and comparison operations on NumPy arrays and
3ExtensionArrays.
4"""
5
6from __future__ import annotations
7
8import datetime
9from functools import partial
10import operator
11from typing import (
12 TYPE_CHECKING,
13 Any,
14)
15
16import numpy as np
17
18from pandas._libs import (
19 NaT,
20 Timedelta,
21 Timestamp,
22 lib,
23 ops as libops,
24)
25from pandas._libs.tslibs import (
26 BaseOffset,
27 get_supported_dtype,
28 is_supported_dtype,
29 is_unitless,
30)
31
32from pandas.core.dtypes.cast import (
33 construct_1d_object_array_from_listlike,
34 find_common_type,
35)
36from pandas.core.dtypes.common import (
37 ensure_object,
38 is_bool_dtype,
39 is_list_like,
40 is_numeric_v_string_like,
41 is_object_dtype,
42 is_scalar,
43)
44from pandas.core.dtypes.generic import (
45 ABCExtensionArray,
46 ABCIndex,
47 ABCSeries,
48)
49from pandas.core.dtypes.missing import (
50 isna,
51 notna,
52)
53
54from pandas.core import roperator
55from pandas.core.computation import expressions
56from pandas.core.construction import (
57 ensure_wrapped_if_datetimelike,
58 sanitize_array,
59)
60from pandas.core.ops import missing
61from pandas.core.ops.dispatch import should_extension_dispatch
62from pandas.core.ops.invalid import invalid_comparison
63
64if TYPE_CHECKING:
65 from pandas._typing import (
66 ArrayLike,
67 Shape,
68 )
69
70# -----------------------------------------------------------------------------
71# Masking NA values and fallbacks for operations numpy does not support
72
73
74def fill_binop(left, right, fill_value):
75 """
76 If a non-None fill_value is given, replace null entries in left and right
77 with this value, but only in positions where _one_ of left/right is null,
78 not both.
79
80 Parameters
81 ----------
82 left : array-like
83 right : array-like
84 fill_value : object
85
86 Returns
87 -------
88 left : array-like
89 right : array-like
90
91 Notes
92 -----
93 Makes copies if fill_value is not None and NAs are present.
94 """
95 if fill_value is not None:
96 left_mask = isna(left)
97 right_mask = isna(right)
98
99 # one but not both
100 mask = left_mask ^ right_mask
101
102 if left_mask.any():
103 # Avoid making a copy if we can
104 left = left.copy()
105 left[left_mask & mask] = fill_value
106
107 if right_mask.any():
108 # Avoid making a copy if we can
109 right = right.copy()
110 right[right_mask & mask] = fill_value
111
112 return left, right
113
114
115def comp_method_OBJECT_ARRAY(op, x, y):
116 if isinstance(y, list):
117 # e.g. test_tuple_categories
118 y = construct_1d_object_array_from_listlike(y)
119
120 if isinstance(y, (np.ndarray, ABCSeries, ABCIndex)):
121 if not is_object_dtype(y.dtype):
122 y = y.astype(np.object_)
123
124 if isinstance(y, (ABCSeries, ABCIndex)):
125 y = y._values
126
127 if x.shape != y.shape:
128 raise ValueError("Shapes must match", x.shape, y.shape)
129 result = libops.vec_compare(x.ravel(), y.ravel(), op)
130 else:
131 result = libops.scalar_compare(x.ravel(), y, op)
132 return result.reshape(x.shape)
133
134
135def _masked_arith_op(x: np.ndarray, y, op) -> np.ndarray:
136 """
137 If the given arithmetic operation fails, attempt it again on
138 only the non-null elements of the input array(s).
139
140 Parameters
141 ----------
142 x : np.ndarray
143 y : np.ndarray, Series, Index
144 op : binary operator
145 """
146 # For Series `x` is 1D so ravel() is a no-op; calling it anyway makes
147 # the logic valid for both Series and DataFrame ops.
148 xrav = x.ravel()
149
150 if isinstance(y, np.ndarray):
151 dtype = find_common_type([x.dtype, y.dtype])
152 result = np.empty(x.size, dtype=dtype)
153
154 if len(x) != len(y):
155 raise ValueError(x.shape, y.shape)
156 ymask = notna(y)
157
158 # NB: ravel() is only safe since y is ndarray; for e.g. PeriodIndex
159 # we would get int64 dtype, see GH#19956
160 yrav = y.ravel()
161 mask = notna(xrav) & ymask.ravel()
162
163 # See GH#5284, GH#5035, GH#19448 for historical reference
164 if mask.any():
165 result[mask] = op(xrav[mask], yrav[mask])
166
167 else:
168 if not is_scalar(y):
169 raise TypeError(
170 f"Cannot broadcast np.ndarray with operand of type {type(y)}"
171 )
172
173 # mask is only meaningful for x
174 result = np.empty(x.size, dtype=x.dtype)
175 mask = notna(xrav)
176
177 # 1 ** np.nan is 1. So we have to unmask those.
178 if op is pow:
179 mask = np.where(x == 1, False, mask)
180 elif op is roperator.rpow:
181 mask = np.where(y == 1, False, mask)
182
183 if mask.any():
184 result[mask] = op(xrav[mask], y)
185
186 np.putmask(result, ~mask, np.nan)
187 result = result.reshape(x.shape) # 2D compat
188 return result
189
190
191def _na_arithmetic_op(left: np.ndarray, right, op, is_cmp: bool = False):
192 """
193 Return the result of evaluating op on the passed in values.
194
195 If native types are not compatible, try coercion to object dtype.
196
197 Parameters
198 ----------
199 left : np.ndarray
200 right : np.ndarray or scalar
201 Excludes DataFrame, Series, Index, ExtensionArray.
202 is_cmp : bool, default False
203 If this a comparison operation.
204
205 Returns
206 -------
207 array-like
208
209 Raises
210 ------
211 TypeError : invalid operation
212 """
213 if isinstance(right, str):
214 # can never use numexpr
215 func = op
216 else:
217 func = partial(expressions.evaluate, op)
218
219 try:
220 result = func(left, right)
221 except TypeError:
222 if not is_cmp and (
223 left.dtype == object or getattr(right, "dtype", None) == object
224 ):
225 # For object dtype, fallback to a masked operation (only operating
226 # on the non-missing values)
227 # Don't do this for comparisons, as that will handle complex numbers
228 # incorrectly, see GH#32047
229 result = _masked_arith_op(left, right, op)
230 else:
231 raise
232
233 if is_cmp and (is_scalar(result) or result is NotImplemented):
234 # numpy returned a scalar instead of operating element-wise
235 # e.g. numeric array vs str
236 # TODO: can remove this after dropping some future numpy version?
237 return invalid_comparison(left, right, op)
238
239 return missing.dispatch_fill_zeros(op, left, right, result)
240
241
242def arithmetic_op(left: ArrayLike, right: Any, op):
243 """
244 Evaluate an arithmetic operation `+`, `-`, `*`, `/`, `//`, `%`, `**`, ...
245
246 Note: the caller is responsible for ensuring that numpy warnings are
247 suppressed (with np.errstate(all="ignore")) if needed.
248
249 Parameters
250 ----------
251 left : np.ndarray or ExtensionArray
252 right : object
253 Cannot be a DataFrame or Index. Series is *not* excluded.
254 op : {operator.add, operator.sub, ...}
255 Or one of the reversed variants from roperator.
256
257 Returns
258 -------
259 ndarray or ExtensionArray
260 Or a 2-tuple of these in the case of divmod or rdivmod.
261 """
262 # NB: We assume that extract_array and ensure_wrapped_if_datetimelike
263 # have already been called on `left` and `right`,
264 # and `maybe_prepare_scalar_for_op` has already been called on `right`
265 # We need to special-case datetime64/timedelta64 dtypes (e.g. because numpy
266 # casts integer dtypes to timedelta64 when operating with timedelta64 - GH#22390)
267 if isinstance(right, list):
268 # GH#62423
269 right = sanitize_array(right, None)
270 right = ensure_wrapped_if_datetimelike(right)
271
272 if (
273 should_extension_dispatch(left, right)
274 or isinstance(right, (Timedelta, BaseOffset, Timestamp))
275 or right is NaT
276 ):
277 # Timedelta/Timestamp and other custom scalars are included in the check
278 # because numexpr will fail on it, see GH#31457
279 res_values = op(left, right)
280 else:
281 # TODO we should handle EAs consistently and move this check before the if/else
282 # (https://github.com/pandas-dev/pandas/issues/41165)
283 # error: Argument 2 to "_bool_arith_check" has incompatible type
284 # "Union[ExtensionArray, ndarray[Any, Any]]"; expected "ndarray[Any, Any]"
285 _bool_arith_check(op, left, right) # type: ignore[arg-type]
286
287 # error: Argument 1 to "_na_arithmetic_op" has incompatible type
288 # "Union[ExtensionArray, ndarray[Any, Any]]"; expected "ndarray[Any, Any]"
289 res_values = _na_arithmetic_op(left, right, op) # type: ignore[arg-type]
290
291 return res_values
292
293
294def comparison_op(left: ArrayLike, right: Any, op) -> ArrayLike:
295 """
296 Evaluate a comparison operation `=`, `!=`, `>=`, `>`, `<=`, or `<`.
297
298 Note: the caller is responsible for ensuring that numpy warnings are
299 suppressed (with np.errstate(all="ignore")) if needed.
300
301 Parameters
302 ----------
303 left : np.ndarray or ExtensionArray
304 right : object
305 Cannot be a DataFrame, Series, or Index.
306 op : {operator.eq, operator.ne, operator.gt, operator.ge, operator.lt, operator.le}
307
308 Returns
309 -------
310 ndarray or ExtensionArray
311 """
312 # NB: We assume extract_array has already been called on left and right
313 lvalues = ensure_wrapped_if_datetimelike(left)
314 rvalues = ensure_wrapped_if_datetimelike(right)
315
316 rvalues = lib.item_from_zerodim(rvalues)
317
318 # Special handling needed if rvalues is a zerodim np.ndarray subclass, see GH#63205
319 rvalues_is_zerodim: bool = getattr(rvalues, "ndim", None) == 0
320
321 if isinstance(rvalues, list):
322 # We don't catch tuple here bc we may be comparing e.g. MultiIndex
323 # to a tuple that represents a single entry, see test_compare_tuple_strs
324 rvalues = sanitize_array(rvalues, None)
325 rvalues = ensure_wrapped_if_datetimelike(rvalues)
326
327 if isinstance(rvalues, (np.ndarray, ABCExtensionArray)) and not rvalues_is_zerodim:
328 # TODO: make this treatment consistent across ops and classes.
329 # We are not catching all listlikes here (e.g. frozenset, tuple)
330 # The ambiguous case is object-dtype. See GH#27803
331 if len(lvalues) != len(rvalues):
332 raise ValueError(
333 "Lengths must match to compare", lvalues.shape, rvalues.shape
334 )
335
336 if should_extension_dispatch(lvalues, rvalues) or (
337 (isinstance(rvalues, (Timedelta, BaseOffset, Timestamp)) or right is NaT)
338 and lvalues.dtype != object
339 ):
340 # Call the method on lvalues
341 res_values = op(lvalues, rvalues)
342
343 # TODO: but not pd.NA?
344 elif (is_scalar(rvalues) or rvalues_is_zerodim) and isna(rvalues):
345 # numpy does not like comparisons vs None
346 if op is operator.ne:
347 res_values = np.ones(lvalues.shape, dtype=bool)
348 else:
349 res_values = np.zeros(lvalues.shape, dtype=bool)
350
351 elif is_numeric_v_string_like(lvalues, rvalues):
352 # GH#36377 going through the numexpr path would incorrectly raise
353 return invalid_comparison(lvalues, rvalues, op)
354
355 elif lvalues.dtype == object or isinstance(rvalues, str):
356 res_values = comp_method_OBJECT_ARRAY(op, lvalues, rvalues)
357
358 else:
359 res_values = _na_arithmetic_op(lvalues, rvalues, op, is_cmp=True)
360
361 return res_values
362
363
364def na_logical_op(x: np.ndarray, y, op):
365 try:
366 # For exposition, write:
367 # yarr = isinstance(y, np.ndarray)
368 # yint = is_integer(y) or (yarr and y.dtype.kind == "i")
369 # ybool = is_bool(y) or (yarr and y.dtype.kind == "b")
370 # xint = x.dtype.kind == "i"
371 # xbool = x.dtype.kind == "b"
372 # Then Cases where this goes through without raising include:
373 # (xint or xbool) and (yint or bool)
374 result = op(x, y)
375 except TypeError:
376 if isinstance(y, np.ndarray):
377 # bool-bool dtype operations should be OK, should not get here
378 assert not (x.dtype.kind == "b" and y.dtype.kind == "b")
379 x = ensure_object(x)
380 y = ensure_object(y)
381 result = libops.vec_binop(x.ravel(), y.ravel(), op)
382 else:
383 # let null fall thru
384 assert lib.is_scalar(y)
385 if not isna(y):
386 y = bool(y)
387 try:
388 result = libops.scalar_binop(x, y, op)
389 except (
390 TypeError,
391 ValueError,
392 AttributeError,
393 OverflowError,
394 NotImplementedError,
395 ) as err:
396 typ = type(y).__name__
397 raise TypeError(
398 f"Cannot perform '{op.__name__}' with a dtyped [{x.dtype}] array "
399 f"and scalar of type [{typ}]"
400 ) from err
401
402 return result.reshape(x.shape)
403
404
405def logical_op(left: ArrayLike, right: Any, op) -> ArrayLike:
406 """
407 Evaluate a logical operation `|`, `&`, or `^`.
408
409 Parameters
410 ----------
411 left : np.ndarray or ExtensionArray
412 right : object
413 Cannot be a DataFrame, Series, or Index.
414 op : {operator.and_, operator.or_, operator.xor}
415 Or one of the reversed variants from roperator.
416
417 Returns
418 -------
419 ndarray or ExtensionArray
420 """
421
422 def fill_bool(x, left=None):
423 # if `left` is specifically not-boolean, we do not cast to bool
424 if x.dtype.kind in "cfO":
425 # dtypes that can hold NA
426 mask = isna(x)
427 if mask.any():
428 x = x.astype(object)
429 x[mask] = False
430
431 if left is None or left.dtype.kind == "b":
432 x = x.astype(bool)
433 return x
434
435 right = lib.item_from_zerodim(right)
436 if is_list_like(right) and not hasattr(right, "dtype"):
437 # e.g. list, tuple
438 raise TypeError(
439 # GH#52264
440 "Logical ops (and, or, xor) between Pandas objects and dtype-less "
441 "sequences (e.g. list, tuple) are no longer supported. "
442 "Wrap the object in a Series, Index, or np.array "
443 "before operating instead.",
444 )
445
446 # NB: We assume extract_array has already been called on left and right
447 lvalues = ensure_wrapped_if_datetimelike(left)
448 rvalues = right
449
450 if should_extension_dispatch(lvalues, rvalues):
451 # Call the method on lvalues
452 res_values = op(lvalues, rvalues)
453
454 else:
455 if isinstance(rvalues, np.ndarray):
456 is_other_int_dtype = rvalues.dtype.kind in "iu"
457 if not is_other_int_dtype:
458 rvalues = fill_bool(rvalues, lvalues)
459
460 else:
461 # i.e. scalar
462 is_other_int_dtype = lib.is_integer(rvalues)
463
464 res_values = na_logical_op(lvalues, rvalues, op)
465
466 # For int vs int `^`, `|`, `&` are bitwise operators and return
467 # integer dtypes. Otherwise these are boolean ops
468 if not (left.dtype.kind in "iu" and is_other_int_dtype):
469 res_values = fill_bool(res_values)
470
471 return res_values
472
473
474def get_array_op(op):
475 """
476 Return a binary array operation corresponding to the given operator op.
477
478 Parameters
479 ----------
480 op : function
481 Binary operator from operator or roperator module.
482
483 Returns
484 -------
485 functools.partial
486 """
487 if isinstance(op, partial):
488 # We get here via dispatch_to_series in DataFrame case
489 # e.g. test_rolling_consistency_var_debiasing_factors
490 return op
491
492 op_name = op.__name__.strip("_").lstrip("r")
493 if op_name == "arith_op":
494 # Reached via DataFrame._combine_frame i.e. flex methods
495 # e.g. test_df_add_flex_filled_mixed_dtypes
496 return op
497
498 if op_name in {"eq", "ne", "lt", "le", "gt", "ge"}:
499 return partial(comparison_op, op=op)
500 elif op_name in {"and", "or", "xor", "rand", "ror", "rxor"}:
501 return partial(logical_op, op=op)
502 elif op_name in {
503 "add",
504 "sub",
505 "mul",
506 "truediv",
507 "floordiv",
508 "mod",
509 "divmod",
510 "pow",
511 }:
512 return partial(arithmetic_op, op=op)
513 else:
514 raise NotImplementedError(op_name)
515
516
517def maybe_prepare_scalar_for_op(obj, shape: Shape):
518 """
519 Cast non-pandas objects to pandas types to unify behavior of arithmetic
520 and comparison operations.
521
522 Parameters
523 ----------
524 obj: object
525 shape : tuple[int]
526
527 Returns
528 -------
529 out : object
530
531 Notes
532 -----
533 Be careful to call this *after* determining the `name` attribute to be
534 attached to the result of the arithmetic operation.
535 """
536 if type(obj) is datetime.timedelta:
537 # GH#22390 cast up to Timedelta to rely on Timedelta
538 # implementation; otherwise operation against numeric-dtype
539 # raises TypeError
540 return Timedelta(obj)
541 elif type(obj) is datetime.datetime:
542 # cast up to Timestamp to rely on Timestamp implementation, see Timedelta above
543 return Timestamp(obj)
544 elif isinstance(obj, np.datetime64):
545 # GH#28080 numpy casts integer-dtype to datetime64 when doing
546 # array[int] + datetime64, which we do not allow
547 if isna(obj):
548 from pandas.core.arrays import DatetimeArray
549
550 # Avoid possible ambiguities with pd.NaT
551 # GH 52295
552 if is_unitless(obj.dtype):
553 # Use second resolution to ensure that the result of e.g.
554 # `left - np.datetime64("NaT")` retains the unit of left.unit
555 obj = obj.astype("datetime64[s]")
556 elif not is_supported_dtype(obj.dtype):
557 new_dtype = get_supported_dtype(obj.dtype)
558 obj = obj.astype(new_dtype)
559 right = np.broadcast_to(obj, shape)
560 return DatetimeArray._simple_new(right, dtype=right.dtype)
561
562 return Timestamp(obj)
563
564 elif isinstance(obj, np.timedelta64):
565 if isna(obj):
566 from pandas.core.arrays import TimedeltaArray
567
568 # wrapping timedelta64("NaT") in Timedelta returns NaT,
569 # which would incorrectly be treated as a datetime-NaT, so
570 # we broadcast and wrap in a TimedeltaArray
571 # GH 52295
572 if is_unitless(obj.dtype):
573 # Use second resolution to ensure that the result of e.g.
574 # `left + np.timedelta64("NaT")` retains the unit of left.unit
575 obj = obj.astype("timedelta64[s]")
576 elif not is_supported_dtype(obj.dtype):
577 new_dtype = get_supported_dtype(obj.dtype)
578 obj = obj.astype(new_dtype)
579 right = np.broadcast_to(obj, shape)
580 return TimedeltaArray._simple_new(right, dtype=right.dtype)
581
582 # In particular non-nanosecond timedelta64 needs to be cast to
583 # nanoseconds, or else we get undesired behavior like
584 # np.timedelta64(3, 'D') / 2 == np.timedelta64(1, 'D')
585 return Timedelta(obj)
586
587 # We want NumPy numeric scalars to behave like Python scalars
588 # post NEP 50
589 elif isinstance(obj, np.integer):
590 return int(obj)
591
592 elif isinstance(obj, np.floating):
593 return float(obj)
594
595 return obj
596
597
598_BOOL_OP_NOT_ALLOWED = {
599 operator.truediv,
600 roperator.rtruediv,
601 operator.floordiv,
602 roperator.rfloordiv,
603 operator.pow,
604 roperator.rpow,
605 divmod,
606 roperator.rdivmod,
607}
608
609
610def _bool_arith_check(op, a: np.ndarray, b) -> None:
611 """
612 In contrast to numpy, pandas raises an error for certain operations
613 with booleans.
614 """
615 if op in _BOOL_OP_NOT_ALLOWED:
616 if a.dtype.kind == "b" and (is_bool_dtype(b) or lib.is_bool(b)):
617 op_name = op.__name__.strip("_").lstrip("r")
618 raise NotImplementedError(
619 f"operator '{op_name}' not implemented for bool dtypes"
620 )