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