Skip to content

Commit 6b05ed7

Browse files
authored
REF: BooleanArray/NumericArray share __array_ufunc__ (#43848)
1 parent 47d20ee commit 6b05ed7

File tree

3 files changed

+64
-99
lines changed

3 files changed

+64
-99
lines changed

pandas/core/arrays/boolean.py

-45
Original file line numberDiff line numberDiff line change
@@ -365,51 +365,6 @@ def map_string(s):
365365

366366
_HANDLED_TYPES = (np.ndarray, numbers.Number, bool, np.bool_)
367367

368-
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
369-
# For BooleanArray inputs, we apply the ufunc to ._data
370-
# and mask the result.
371-
if method == "reduce":
372-
# Not clear how to handle missing values in reductions. Raise.
373-
raise NotImplementedError("The 'reduce' method is not supported.")
374-
out = kwargs.get("out", ())
375-
376-
for x in inputs + out:
377-
if not isinstance(x, self._HANDLED_TYPES + (BooleanArray,)):
378-
return NotImplemented
379-
380-
# for binary ops, use our custom dunder methods
381-
result = ops.maybe_dispatch_ufunc_to_dunder_op(
382-
self, ufunc, method, *inputs, **kwargs
383-
)
384-
if result is not NotImplemented:
385-
return result
386-
387-
mask = np.zeros(len(self), dtype=bool)
388-
inputs2 = []
389-
for x in inputs:
390-
if isinstance(x, BooleanArray):
391-
mask |= x._mask
392-
inputs2.append(x._data)
393-
else:
394-
inputs2.append(x)
395-
396-
def reconstruct(x):
397-
# we don't worry about scalar `x` here, since we
398-
# raise for reduce up above.
399-
400-
if is_bool_dtype(x.dtype):
401-
m = mask.copy()
402-
return BooleanArray(x, m)
403-
else:
404-
x[mask] = np.nan
405-
return x
406-
407-
result = getattr(ufunc, method)(*inputs2, **kwargs)
408-
if isinstance(result, tuple):
409-
tuple(reconstruct(x) for x in result)
410-
else:
411-
return reconstruct(result)
412-
413368
def _coerce_to_array(self, value) -> tuple[np.ndarray, np.ndarray]:
414369
return coerce_to_array(value)
415370

pandas/core/arrays/masked.py

+64
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@
3434

3535
from pandas.core.dtypes.base import ExtensionDtype
3636
from pandas.core.dtypes.common import (
37+
is_bool_dtype,
3738
is_dtype_equal,
39+
is_float_dtype,
3840
is_integer,
41+
is_integer_dtype,
3942
is_object_dtype,
4043
is_scalar,
4144
is_string_dtype,
@@ -50,6 +53,7 @@
5053
from pandas.core import (
5154
missing,
5255
nanops,
56+
ops,
5357
)
5458
from pandas.core.algorithms import (
5559
factorize_array,
@@ -64,6 +68,7 @@
6468
if TYPE_CHECKING:
6569
from pandas import Series
6670
from pandas.core.arrays import BooleanArray
71+
6772
from pandas.compat.numpy import function as nv
6873

6974
BaseMaskedArrayT = TypeVar("BaseMaskedArrayT", bound="BaseMaskedArray")
@@ -360,6 +365,65 @@ def __array__(self, dtype: NpDtype | None = None) -> np.ndarray:
360365
"""
361366
return self.to_numpy(dtype=dtype)
362367

368+
_HANDLED_TYPES: tuple[type, ...]
369+
370+
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
371+
# For MaskedArray inputs, we apply the ufunc to ._data
372+
# and mask the result.
373+
if method == "reduce":
374+
# Not clear how to handle missing values in reductions. Raise.
375+
raise NotImplementedError("The 'reduce' method is not supported.")
376+
377+
out = kwargs.get("out", ())
378+
379+
for x in inputs + out:
380+
if not isinstance(x, self._HANDLED_TYPES + (BaseMaskedArray,)):
381+
return NotImplemented
382+
383+
# for binary ops, use our custom dunder methods
384+
result = ops.maybe_dispatch_ufunc_to_dunder_op(
385+
self, ufunc, method, *inputs, **kwargs
386+
)
387+
if result is not NotImplemented:
388+
return result
389+
390+
mask = np.zeros(len(self), dtype=bool)
391+
inputs2 = []
392+
for x in inputs:
393+
if isinstance(x, BaseMaskedArray):
394+
mask |= x._mask
395+
inputs2.append(x._data)
396+
else:
397+
inputs2.append(x)
398+
399+
def reconstruct(x):
400+
# we don't worry about scalar `x` here, since we
401+
# raise for reduce up above.
402+
from pandas.core.arrays import (
403+
BooleanArray,
404+
FloatingArray,
405+
IntegerArray,
406+
)
407+
408+
if is_bool_dtype(x.dtype):
409+
m = mask.copy()
410+
return BooleanArray(x, m)
411+
elif is_integer_dtype(x.dtype):
412+
m = mask.copy()
413+
return IntegerArray(x, m)
414+
elif is_float_dtype(x.dtype):
415+
m = mask.copy()
416+
return FloatingArray(x, m)
417+
else:
418+
x[mask] = np.nan
419+
return x
420+
421+
result = getattr(ufunc, method)(*inputs2, **kwargs)
422+
if isinstance(result, tuple):
423+
return tuple(reconstruct(x) for x in result)
424+
else:
425+
return reconstruct(result)
426+
363427
def __arrow_array__(self, type=None):
364428
"""
365429
Convert myself into a pyarrow Array.

pandas/core/arrays/numeric.py

-54
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import numbers
55
from typing import (
66
TYPE_CHECKING,
7-
Any,
87
TypeVar,
98
)
109

@@ -25,7 +24,6 @@
2524
is_list_like,
2625
)
2726

28-
from pandas.core import ops
2927
from pandas.core.arrays.masked import (
3028
BaseMaskedArray,
3129
BaseMaskedDtype,
@@ -154,58 +152,6 @@ def _arith_method(self, other, op):
154152

155153
_HANDLED_TYPES = (np.ndarray, numbers.Number)
156154

157-
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
158-
# For NumericArray inputs, we apply the ufunc to ._data
159-
# and mask the result.
160-
if method == "reduce":
161-
# Not clear how to handle missing values in reductions. Raise.
162-
raise NotImplementedError("The 'reduce' method is not supported.")
163-
out = kwargs.get("out", ())
164-
165-
for x in inputs + out:
166-
if not isinstance(x, self._HANDLED_TYPES + (NumericArray,)):
167-
return NotImplemented
168-
169-
# for binary ops, use our custom dunder methods
170-
result = ops.maybe_dispatch_ufunc_to_dunder_op(
171-
self, ufunc, method, *inputs, **kwargs
172-
)
173-
if result is not NotImplemented:
174-
return result
175-
176-
mask = np.zeros(len(self), dtype=bool)
177-
inputs2: list[Any] = []
178-
for x in inputs:
179-
if isinstance(x, NumericArray):
180-
mask |= x._mask
181-
inputs2.append(x._data)
182-
else:
183-
inputs2.append(x)
184-
185-
def reconstruct(x):
186-
# we don't worry about scalar `x` here, since we
187-
# raise for reduce up above.
188-
189-
if is_integer_dtype(x.dtype):
190-
from pandas.core.arrays import IntegerArray
191-
192-
m = mask.copy()
193-
return IntegerArray(x, m)
194-
elif is_float_dtype(x.dtype):
195-
from pandas.core.arrays import FloatingArray
196-
197-
m = mask.copy()
198-
return FloatingArray(x, m)
199-
else:
200-
x[mask] = np.nan
201-
return x
202-
203-
result = getattr(ufunc, method)(*inputs2, **kwargs)
204-
if isinstance(result, tuple):
205-
return tuple(reconstruct(x) for x in result)
206-
else:
207-
return reconstruct(result)
208-
209155
def __neg__(self):
210156
return type(self)(-self._data, self._mask.copy())
211157

0 commit comments

Comments
 (0)