Skip to content

Commit 5b91feb

Browse files
REF: move common arithmetic ops from Integer/FloatingArray to NumericArray (#38290)
1 parent fb5e57f commit 5b91feb

File tree

3 files changed

+99
-143
lines changed

3 files changed

+99
-143
lines changed

pandas/core/arrays/floating.py

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
from pandas.core.dtypes.common import (
1414
is_bool_dtype,
1515
is_datetime64_dtype,
16-
is_float,
1716
is_float_dtype,
18-
is_integer,
1917
is_integer_dtype,
2018
is_list_like,
2119
is_object_dtype,
@@ -28,7 +26,8 @@
2826
from pandas.core.ops import invalid_comparison
2927
from pandas.core.tools.numeric import to_numeric
3028

31-
from .masked import BaseMaskedArray, BaseMaskedDtype
29+
from .masked import BaseMaskedDtype
30+
from .numeric import NumericArray
3231

3332
if TYPE_CHECKING:
3433
import pyarrow
@@ -199,7 +198,7 @@ def coerce_to_array(
199198
return values, mask
200199

201200

202-
class FloatingArray(BaseMaskedArray):
201+
class FloatingArray(NumericArray):
203202
"""
204203
Array of floating (optional missing) values.
205204
@@ -478,71 +477,6 @@ def _maybe_mask_result(self, result, mask, other, op_name: str):
478477

479478
return type(self)(result, mask, copy=False)
480479

481-
def _arith_method(self, other, op):
482-
from pandas.arrays import IntegerArray
483-
484-
omask = None
485-
486-
if getattr(other, "ndim", 0) > 1:
487-
raise NotImplementedError("can only perform ops with 1-d structures")
488-
489-
if isinstance(other, (IntegerArray, FloatingArray)):
490-
other, omask = other._data, other._mask
491-
492-
elif is_list_like(other):
493-
other = np.asarray(other)
494-
if other.ndim > 1:
495-
raise NotImplementedError("can only perform ops with 1-d structures")
496-
if len(self) != len(other):
497-
raise ValueError("Lengths must match")
498-
if not (is_float_dtype(other) or is_integer_dtype(other)):
499-
raise TypeError("can only perform ops with numeric values")
500-
501-
else:
502-
if not (is_float(other) or is_integer(other) or other is libmissing.NA):
503-
raise TypeError("can only perform ops with numeric values")
504-
505-
if omask is None:
506-
mask = self._mask.copy()
507-
if other is libmissing.NA:
508-
mask |= True
509-
else:
510-
mask = self._mask | omask
511-
512-
if op.__name__ == "pow":
513-
# 1 ** x is 1.
514-
mask = np.where((self._data == 1) & ~self._mask, False, mask)
515-
# x ** 0 is 1.
516-
if omask is not None:
517-
mask = np.where((other == 0) & ~omask, False, mask)
518-
elif other is not libmissing.NA:
519-
mask = np.where(other == 0, False, mask)
520-
521-
elif op.__name__ == "rpow":
522-
# 1 ** x is 1.
523-
if omask is not None:
524-
mask = np.where((other == 1) & ~omask, False, mask)
525-
elif other is not libmissing.NA:
526-
mask = np.where(other == 1, False, mask)
527-
# x ** 0 is 1.
528-
mask = np.where((self._data == 0) & ~self._mask, False, mask)
529-
530-
if other is libmissing.NA:
531-
result = np.ones_like(self._data)
532-
else:
533-
with np.errstate(all="ignore"):
534-
result = op(self._data, other)
535-
536-
# divmod returns a tuple
537-
if op.__name__ == "divmod":
538-
div, mod = result
539-
return (
540-
self._maybe_mask_result(div, mask, other, "floordiv"),
541-
self._maybe_mask_result(mod, mask, other, "mod"),
542-
)
543-
544-
return self._maybe_mask_result(result, mask, other, op.__name__)
545-
546480

547481
_dtype_docstring = """
548482
An ExtensionDtype for {dtype} data.

pandas/core/arrays/integer.py

Lines changed: 4 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from datetime import timedelta
21
import numbers
32
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
43
import warnings
54

65
import numpy as np
76

8-
from pandas._libs import Timedelta, iNaT, lib, missing as libmissing
7+
from pandas._libs import iNaT, lib, missing as libmissing
98
from pandas._typing import ArrayLike, DtypeObj
109
from pandas.compat.numpy import function as nv
1110
from pandas.util._decorators import cache_readonly
@@ -16,7 +15,6 @@
1615
is_datetime64_dtype,
1716
is_float,
1817
is_float_dtype,
19-
is_integer,
2018
is_integer_dtype,
2119
is_list_like,
2220
is_object_dtype,
@@ -29,6 +27,7 @@
2927
from pandas.core.tools.numeric import to_numeric
3028

3129
from .masked import BaseMaskedArray, BaseMaskedDtype
30+
from .numeric import NumericArray
3231

3332
if TYPE_CHECKING:
3433
import pyarrow
@@ -263,7 +262,7 @@ def coerce_to_array(
263262
return values, mask
264263

265264

266-
class IntegerArray(BaseMaskedArray):
265+
class IntegerArray(NumericArray):
267266
"""
268267
Array of integer (optional missing) values.
269268
@@ -494,7 +493,7 @@ def _values_for_argsort(self) -> np.ndarray:
494493
return data
495494

496495
def _cmp_method(self, other, op):
497-
from pandas.core.arrays import BaseMaskedArray, BooleanArray
496+
from pandas.core.arrays import BooleanArray
498497

499498
mask = None
500499

@@ -538,75 +537,6 @@ def _cmp_method(self, other, op):
538537

539538
return BooleanArray(result, mask)
540539

541-
def _arith_method(self, other, op):
542-
from pandas.core.arrays import FloatingArray
543-
544-
op_name = op.__name__
545-
omask = None
546-
547-
if getattr(other, "ndim", 0) > 1:
548-
raise NotImplementedError("can only perform ops with 1-d structures")
549-
550-
if isinstance(other, (IntegerArray, FloatingArray)):
551-
other, omask = other._data, other._mask
552-
553-
elif is_list_like(other):
554-
other = np.asarray(other)
555-
if other.ndim > 1:
556-
raise NotImplementedError("can only perform ops with 1-d structures")
557-
if len(self) != len(other):
558-
raise ValueError("Lengths must match")
559-
if not (is_float_dtype(other) or is_integer_dtype(other)):
560-
raise TypeError("can only perform ops with numeric values")
561-
562-
elif isinstance(other, (timedelta, np.timedelta64)):
563-
other = Timedelta(other)
564-
565-
else:
566-
if not (is_float(other) or is_integer(other) or other is libmissing.NA):
567-
raise TypeError("can only perform ops with numeric values")
568-
569-
if omask is None:
570-
mask = self._mask.copy()
571-
if other is libmissing.NA:
572-
mask |= True
573-
else:
574-
mask = self._mask | omask
575-
576-
if op_name == "pow":
577-
# 1 ** x is 1.
578-
mask = np.where((self._data == 1) & ~self._mask, False, mask)
579-
# x ** 0 is 1.
580-
if omask is not None:
581-
mask = np.where((other == 0) & ~omask, False, mask)
582-
elif other is not libmissing.NA:
583-
mask = np.where(other == 0, False, mask)
584-
585-
elif op_name == "rpow":
586-
# 1 ** x is 1.
587-
if omask is not None:
588-
mask = np.where((other == 1) & ~omask, False, mask)
589-
elif other is not libmissing.NA:
590-
mask = np.where(other == 1, False, mask)
591-
# x ** 0 is 1.
592-
mask = np.where((self._data == 0) & ~self._mask, False, mask)
593-
594-
if other is libmissing.NA:
595-
result = np.ones_like(self._data)
596-
else:
597-
with np.errstate(all="ignore"):
598-
result = op(self._data, other)
599-
600-
# divmod returns a tuple
601-
if op_name == "divmod":
602-
div, mod = result
603-
return (
604-
self._maybe_mask_result(div, mask, other, "floordiv"),
605-
self._maybe_mask_result(mod, mask, other, "mod"),
606-
)
607-
608-
return self._maybe_mask_result(result, mask, other, op_name)
609-
610540
def sum(self, *, skipna=True, min_count=0, **kwargs):
611541
nv.validate_sum((), kwargs)
612542
return super()._reduce("sum", skipna=skipna, min_count=min_count)

pandas/core/arrays/numeric.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import datetime
2+
3+
import numpy as np
4+
5+
from pandas._libs import Timedelta, missing as libmissing
6+
from pandas.errors import AbstractMethodError
7+
8+
from pandas.core.dtypes.common import (
9+
is_float,
10+
is_float_dtype,
11+
is_integer,
12+
is_integer_dtype,
13+
is_list_like,
14+
)
15+
16+
from .masked import BaseMaskedArray
17+
18+
19+
class NumericArray(BaseMaskedArray):
20+
"""
21+
Base class for IntegerArray and FloatingArray.
22+
"""
23+
24+
def _maybe_mask_result(self, result, mask, other, op_name: str):
25+
raise AbstractMethodError(self)
26+
27+
def _arith_method(self, other, op):
28+
op_name = op.__name__
29+
omask = None
30+
31+
if getattr(other, "ndim", 0) > 1:
32+
raise NotImplementedError("can only perform ops with 1-d structures")
33+
34+
if isinstance(other, NumericArray):
35+
other, omask = other._data, other._mask
36+
37+
elif is_list_like(other):
38+
other = np.asarray(other)
39+
if other.ndim > 1:
40+
raise NotImplementedError("can only perform ops with 1-d structures")
41+
if len(self) != len(other):
42+
raise ValueError("Lengths must match")
43+
if not (is_float_dtype(other) or is_integer_dtype(other)):
44+
raise TypeError("can only perform ops with numeric values")
45+
46+
elif isinstance(other, (datetime.timedelta, np.timedelta64)):
47+
other = Timedelta(other)
48+
49+
else:
50+
if not (is_float(other) or is_integer(other) or other is libmissing.NA):
51+
raise TypeError("can only perform ops with numeric values")
52+
53+
if omask is None:
54+
mask = self._mask.copy()
55+
if other is libmissing.NA:
56+
mask |= True
57+
else:
58+
mask = self._mask | omask
59+
60+
if op_name == "pow":
61+
# 1 ** x is 1.
62+
mask = np.where((self._data == 1) & ~self._mask, False, mask)
63+
# x ** 0 is 1.
64+
if omask is not None:
65+
mask = np.where((other == 0) & ~omask, False, mask)
66+
elif other is not libmissing.NA:
67+
mask = np.where(other == 0, False, mask)
68+
69+
elif op_name == "rpow":
70+
# 1 ** x is 1.
71+
if omask is not None:
72+
mask = np.where((other == 1) & ~omask, False, mask)
73+
elif other is not libmissing.NA:
74+
mask = np.where(other == 1, False, mask)
75+
# x ** 0 is 1.
76+
mask = np.where((self._data == 0) & ~self._mask, False, mask)
77+
78+
if other is libmissing.NA:
79+
result = np.ones_like(self._data)
80+
else:
81+
with np.errstate(all="ignore"):
82+
result = op(self._data, other)
83+
84+
# divmod returns a tuple
85+
if op_name == "divmod":
86+
div, mod = result
87+
return (
88+
self._maybe_mask_result(div, mask, other, "floordiv"),
89+
self._maybe_mask_result(mod, mask, other, "mod"),
90+
)
91+
92+
return self._maybe_mask_result(result, mask, other, op_name)

0 commit comments

Comments
 (0)