Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/ops/__init__.py: 21%
181 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Arithmetic operations for PandasObjects
4This is not a public API.
5"""
6from __future__ import annotations
8import operator
9from typing import TYPE_CHECKING
10import warnings
12import numpy as np
14from pandas._libs.ops_dispatch import maybe_dispatch_ufunc_to_dunder_op
15from pandas._typing import Level
16from pandas.util._decorators import Appender
17from pandas.util._exceptions import find_stack_level
19from pandas.core.dtypes.common import (
20 is_array_like,
21 is_list_like,
22)
23from pandas.core.dtypes.generic import (
24 ABCDataFrame,
25 ABCSeries,
26)
27from pandas.core.dtypes.missing import isna
29from pandas.core import (
30 algorithms,
31 roperator,
32)
33from pandas.core.ops.array_ops import (
34 arithmetic_op,
35 comp_method_OBJECT_ARRAY,
36 comparison_op,
37 get_array_op,
38 logical_op,
39 maybe_prepare_scalar_for_op,
40)
41from pandas.core.ops.common import (
42 get_op_result_name,
43 unpack_zerodim_and_defer,
44)
45from pandas.core.ops.docstrings import (
46 _flex_comp_doc_FRAME,
47 _op_descriptions,
48 make_flex_doc,
49)
50from pandas.core.ops.invalid import invalid_comparison
51from pandas.core.ops.mask_ops import (
52 kleene_and,
53 kleene_or,
54 kleene_xor,
55)
56from pandas.core.ops.methods import add_flex_arithmetic_methods
57from pandas.core.roperator import (
58 radd,
59 rand_,
60 rdiv,
61 rdivmod,
62 rfloordiv,
63 rmod,
64 rmul,
65 ror_,
66 rpow,
67 rsub,
68 rtruediv,
69 rxor,
70)
72if TYPE_CHECKING: 72 ↛ 73line 72 didn't jump to line 73, because the condition on line 72 was never true
73 from pandas import (
74 DataFrame,
75 Series,
76 )
78# -----------------------------------------------------------------------------
79# constants
80ARITHMETIC_BINOPS: set[str] = {
81 "add",
82 "sub",
83 "mul",
84 "pow",
85 "mod",
86 "floordiv",
87 "truediv",
88 "divmod",
89 "radd",
90 "rsub",
91 "rmul",
92 "rpow",
93 "rmod",
94 "rfloordiv",
95 "rtruediv",
96 "rdivmod",
97}
100COMPARISON_BINOPS: set[str] = {"eq", "ne", "lt", "gt", "le", "ge"}
103# -----------------------------------------------------------------------------
104# Masking NA values and fallbacks for operations numpy does not support
107def fill_binop(left, right, fill_value):
108 """
109 If a non-None fill_value is given, replace null entries in left and right
110 with this value, but only in positions where _one_ of left/right is null,
111 not both.
113 Parameters
114 ----------
115 left : array-like
116 right : array-like
117 fill_value : object
119 Returns
120 -------
121 left : array-like
122 right : array-like
124 Notes
125 -----
126 Makes copies if fill_value is not None and NAs are present.
127 """
128 if fill_value is not None:
129 left_mask = isna(left)
130 right_mask = isna(right)
132 # one but not both
133 mask = left_mask ^ right_mask
135 if left_mask.any():
136 # Avoid making a copy if we can
137 left = left.copy()
138 left[left_mask & mask] = fill_value
140 if right_mask.any():
141 # Avoid making a copy if we can
142 right = right.copy()
143 right[right_mask & mask] = fill_value
145 return left, right
148# -----------------------------------------------------------------------------
149# Series
152def align_method_SERIES(left: Series, right, align_asobject: bool = False):
153 """align lhs and rhs Series"""
154 # ToDo: Different from align_method_FRAME, list, tuple and ndarray
155 # are not coerced here
156 # because Series has inconsistencies described in #13637
158 if isinstance(right, ABCSeries):
159 # avoid repeated alignment
160 if not left.index.equals(right.index):
162 if align_asobject:
163 # to keep original value's dtype for bool ops
164 left = left.astype(object)
165 right = right.astype(object)
167 left, right = left.align(right, copy=False)
169 return left, right
172def flex_method_SERIES(op):
173 name = op.__name__.strip("_")
174 doc = make_flex_doc(name, "series")
176 @Appender(doc)
177 def flex_wrapper(self, other, level=None, fill_value=None, axis=0):
178 # validate axis
179 if axis is not None:
180 self._get_axis_number(axis)
182 res_name = get_op_result_name(self, other)
184 if isinstance(other, ABCSeries):
185 return self._binop(other, op, level=level, fill_value=fill_value)
186 elif isinstance(other, (np.ndarray, list, tuple)):
187 if len(other) != len(self):
188 raise ValueError("Lengths must be equal")
189 other = self._constructor(other, self.index)
190 result = self._binop(other, op, level=level, fill_value=fill_value)
191 result.name = res_name
192 return result
193 else:
194 if fill_value is not None:
195 self = self.fillna(fill_value)
197 return op(self, other)
199 flex_wrapper.__name__ = name
200 return flex_wrapper
203# -----------------------------------------------------------------------------
204# DataFrame
207def align_method_FRAME(
208 left, right, axis, flex: bool | None = False, level: Level = None
209):
210 """
211 Convert rhs to meet lhs dims if input is list, tuple or np.ndarray.
213 Parameters
214 ----------
215 left : DataFrame
216 right : Any
217 axis : int, str, or None
218 flex : bool or None, default False
219 Whether this is a flex op, in which case we reindex.
220 None indicates not to check for alignment.
221 level : int or level name, default None
223 Returns
224 -------
225 left : DataFrame
226 right : Any
227 """
229 def to_series(right):
230 msg = "Unable to coerce to Series, length must be {req_len}: given {given_len}"
231 if axis is not None and left._get_axis_name(axis) == "index":
232 if len(left.index) != len(right):
233 raise ValueError(
234 msg.format(req_len=len(left.index), given_len=len(right))
235 )
236 right = left._constructor_sliced(right, index=left.index)
237 else:
238 if len(left.columns) != len(right):
239 raise ValueError(
240 msg.format(req_len=len(left.columns), given_len=len(right))
241 )
242 right = left._constructor_sliced(right, index=left.columns)
243 return right
245 if isinstance(right, np.ndarray):
247 if right.ndim == 1:
248 right = to_series(right)
250 elif right.ndim == 2:
251 if right.shape == left.shape:
252 right = left._constructor(right, index=left.index, columns=left.columns)
254 elif right.shape[0] == left.shape[0] and right.shape[1] == 1:
255 # Broadcast across columns
256 right = np.broadcast_to(right, left.shape)
257 right = left._constructor(right, index=left.index, columns=left.columns)
259 elif right.shape[1] == left.shape[1] and right.shape[0] == 1:
260 # Broadcast along rows
261 right = to_series(right[0, :])
263 else:
264 raise ValueError(
265 "Unable to coerce to DataFrame, shape "
266 f"must be {left.shape}: given {right.shape}"
267 )
269 elif right.ndim > 2:
270 raise ValueError(
271 "Unable to coerce to Series/DataFrame, "
272 f"dimension must be <= 2: {right.shape}"
273 )
275 elif is_list_like(right) and not isinstance(right, (ABCSeries, ABCDataFrame)):
276 # GH 36702. Raise when attempting arithmetic with list of array-like.
277 if any(is_array_like(el) for el in right):
278 raise ValueError(
279 f"Unable to coerce list of {type(right[0])} to Series/DataFrame"
280 )
281 # GH17901
282 right = to_series(right)
284 if flex is not None and isinstance(right, ABCDataFrame):
285 if not left._indexed_same(right):
286 if flex:
287 left, right = left.align(right, join="outer", level=level, copy=False)
288 else:
289 raise ValueError(
290 "Can only compare identically-labeled DataFrame objects"
291 )
292 elif isinstance(right, ABCSeries):
293 # axis=1 is default for DataFrame-with-Series op
294 axis = left._get_axis_number(axis) if axis is not None else 1
296 if not flex:
297 if not left.axes[axis].equals(right.index):
298 warnings.warn(
299 "Automatic reindexing on DataFrame vs Series comparisons "
300 "is deprecated and will raise ValueError in a future version. "
301 "Do `left, right = left.align(right, axis=1, copy=False)` "
302 "before e.g. `left == right`",
303 FutureWarning,
304 stacklevel=find_stack_level(),
305 )
307 left, right = left.align(
308 right, join="outer", axis=axis, level=level, copy=False
309 )
310 right = _maybe_align_series_as_frame(left, right, axis)
312 return left, right
315def should_reindex_frame_op(
316 left: DataFrame, right, op, axis, default_axis, fill_value, level
317) -> bool:
318 """
319 Check if this is an operation between DataFrames that will need to reindex.
320 """
321 assert isinstance(left, ABCDataFrame)
323 if op is operator.pow or op is roperator.rpow:
324 # GH#32685 pow has special semantics for operating with null values
325 return False
327 if not isinstance(right, ABCDataFrame):
328 return False
330 if fill_value is None and level is None and axis is default_axis:
331 # TODO: any other cases we should handle here?
333 # Intersection is always unique so we have to check the unique columns
334 left_uniques = left.columns.unique()
335 right_uniques = right.columns.unique()
336 cols = left_uniques.intersection(right_uniques)
337 if len(cols) and not (
338 len(cols) == len(left_uniques) and len(cols) == len(right_uniques)
339 ):
340 # TODO: is there a shortcut available when len(cols) == 0?
341 return True
343 return False
346def frame_arith_method_with_reindex(left: DataFrame, right: DataFrame, op) -> DataFrame:
347 """
348 For DataFrame-with-DataFrame operations that require reindexing,
349 operate only on shared columns, then reindex.
351 Parameters
352 ----------
353 left : DataFrame
354 right : DataFrame
355 op : binary operator
357 Returns
358 -------
359 DataFrame
360 """
361 # GH#31623, only operate on shared columns
362 cols, lcols, rcols = left.columns.join(
363 right.columns, how="inner", level=None, return_indexers=True
364 )
366 new_left = left.iloc[:, lcols]
367 new_right = right.iloc[:, rcols]
368 result = op(new_left, new_right)
370 # Do the join on the columns instead of using align_method_FRAME
371 # to avoid constructing two potentially large/sparse DataFrames
372 join_columns, _, _ = left.columns.join(
373 right.columns, how="outer", level=None, return_indexers=True
374 )
376 if result.columns.has_duplicates:
377 # Avoid reindexing with a duplicate axis.
378 # https://github.com/pandas-dev/pandas/issues/35194
379 indexer, _ = result.columns.get_indexer_non_unique(join_columns)
380 indexer = algorithms.unique1d(indexer)
381 result = result._reindex_with_indexers(
382 {1: [join_columns, indexer]}, allow_dups=True
383 )
384 else:
385 result = result.reindex(join_columns, axis=1)
387 return result
390def _maybe_align_series_as_frame(frame: DataFrame, series: Series, axis: int):
391 """
392 If the Series operand is not EA-dtype, we can broadcast to 2D and operate
393 blockwise.
394 """
395 rvalues = series._values
396 if not isinstance(rvalues, np.ndarray):
397 # TODO(EA2D): no need to special-case with 2D EAs
398 if rvalues.dtype == "datetime64[ns]" or rvalues.dtype == "timedelta64[ns]":
399 # We can losslessly+cheaply cast to ndarray
400 rvalues = np.asarray(rvalues)
401 else:
402 return series
404 if axis == 0:
405 rvalues = rvalues.reshape(-1, 1)
406 else:
407 rvalues = rvalues.reshape(1, -1)
409 rvalues = np.broadcast_to(rvalues, frame.shape)
410 return type(frame)(rvalues, index=frame.index, columns=frame.columns)
413def flex_arith_method_FRAME(op):
414 op_name = op.__name__.strip("_")
415 default_axis = "columns"
417 na_op = get_array_op(op)
418 doc = make_flex_doc(op_name, "dataframe")
420 @Appender(doc)
421 def f(self, other, axis=default_axis, level=None, fill_value=None):
423 if should_reindex_frame_op(
424 self, other, op, axis, default_axis, fill_value, level
425 ):
426 return frame_arith_method_with_reindex(self, other, op)
428 if isinstance(other, ABCSeries) and fill_value is not None:
429 # TODO: We could allow this in cases where we end up going
430 # through the DataFrame path
431 raise NotImplementedError(f"fill_value {fill_value} not supported.")
433 axis = self._get_axis_number(axis) if axis is not None else 1
435 other = maybe_prepare_scalar_for_op(other, self.shape)
436 self, other = align_method_FRAME(self, other, axis, flex=True, level=level)
438 if isinstance(other, ABCDataFrame):
439 # Another DataFrame
440 new_data = self._combine_frame(other, na_op, fill_value)
442 elif isinstance(other, ABCSeries):
443 new_data = self._dispatch_frame_op(other, op, axis=axis)
444 else:
445 # in this case we always have `np.ndim(other) == 0`
446 if fill_value is not None:
447 self = self.fillna(fill_value)
449 new_data = self._dispatch_frame_op(other, op)
451 return self._construct_result(new_data)
453 f.__name__ = op_name
455 return f
458def flex_comp_method_FRAME(op):
459 op_name = op.__name__.strip("_")
460 default_axis = "columns" # because we are "flex"
462 doc = _flex_comp_doc_FRAME.format(
463 op_name=op_name, desc=_op_descriptions[op_name]["desc"]
464 )
466 @Appender(doc)
467 def f(self, other, axis=default_axis, level=None):
468 axis = self._get_axis_number(axis) if axis is not None else 1
470 self, other = align_method_FRAME(self, other, axis, flex=True, level=level)
472 new_data = self._dispatch_frame_op(other, op, axis=axis)
473 return self._construct_result(new_data)
475 f.__name__ = op_name
477 return f
480__all__ = [
481 "add_flex_arithmetic_methods",
482 "align_method_FRAME",
483 "align_method_SERIES",
484 "ARITHMETIC_BINOPS",
485 "arithmetic_op",
486 "COMPARISON_BINOPS",
487 "comparison_op",
488 "comp_method_OBJECT_ARRAY",
489 "fill_binop",
490 "flex_arith_method_FRAME",
491 "flex_comp_method_FRAME",
492 "flex_method_SERIES",
493 "frame_arith_method_with_reindex",
494 "invalid_comparison",
495 "kleene_and",
496 "kleene_or",
497 "kleene_xor",
498 "logical_op",
499 "maybe_dispatch_ufunc_to_dunder_op",
500 "radd",
501 "rand_",
502 "rdiv",
503 "rdivmod",
504 "rfloordiv",
505 "rmod",
506 "rmul",
507 "ror_",
508 "rpow",
509 "rsub",
510 "rtruediv",
511 "rxor",
512 "should_reindex_frame_op",
513 "unpack_zerodim_and_defer",
514]