Skip to content

Commit 9159513

Browse files
REF: move __array_ufunc__ to base NumericArray (#38412)
1 parent dba7641 commit 9159513

File tree

4 files changed

+64
-106
lines changed

4 files changed

+64
-106
lines changed

pandas/core/arrays/floating.py

-50
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import numbers
21
from typing import List, Optional, Tuple, Type
32
import warnings
43

@@ -22,7 +21,6 @@
2221
from pandas.core.dtypes.dtypes import ExtensionDtype, register_extension_dtype
2322
from pandas.core.dtypes.missing import isna
2423

25-
from pandas.core import ops
2624
from pandas.core.ops import invalid_comparison
2725
from pandas.core.tools.numeric import to_numeric
2826

@@ -255,54 +253,6 @@ def _from_sequence_of_strings(
255253
scalars = to_numeric(strings, errors="raise")
256254
return cls._from_sequence(scalars, dtype=dtype, copy=copy)
257255

258-
_HANDLED_TYPES = (np.ndarray, numbers.Number)
259-
260-
def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
261-
# For FloatingArray inputs, we apply the ufunc to ._data
262-
# and mask the result.
263-
if method == "reduce":
264-
# Not clear how to handle missing values in reductions. Raise.
265-
raise NotImplementedError("The 'reduce' method is not supported.")
266-
out = kwargs.get("out", ())
267-
268-
for x in inputs + out:
269-
if not isinstance(x, self._HANDLED_TYPES + (FloatingArray,)):
270-
return NotImplemented
271-
272-
# for binary ops, use our custom dunder methods
273-
result = ops.maybe_dispatch_ufunc_to_dunder_op(
274-
self, ufunc, method, *inputs, **kwargs
275-
)
276-
if result is not NotImplemented:
277-
return result
278-
279-
mask = np.zeros(len(self), dtype=bool)
280-
inputs2 = []
281-
for x in inputs:
282-
if isinstance(x, FloatingArray):
283-
mask |= x._mask
284-
inputs2.append(x._data)
285-
else:
286-
inputs2.append(x)
287-
288-
def reconstruct(x):
289-
# we don't worry about scalar `x` here, since we
290-
# raise for reduce up above.
291-
292-
# TODO
293-
if is_float_dtype(x.dtype):
294-
m = mask.copy()
295-
return FloatingArray(x, m)
296-
else:
297-
x[mask] = np.nan
298-
return x
299-
300-
result = getattr(ufunc, method)(*inputs2, **kwargs)
301-
if isinstance(result, tuple):
302-
tuple(reconstruct(x) for x in result)
303-
else:
304-
return reconstruct(result)
305-
306256
def _coerce_to_array(self, value) -> Tuple[np.ndarray, np.ndarray]:
307257
return coerce_to_array(value, dtype=self.dtype)
308258

pandas/core/arrays/integer.py

-49
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import numbers
21
from typing import Dict, List, Optional, Tuple, Type
32
import warnings
43

@@ -22,7 +21,6 @@
2221
)
2322
from pandas.core.dtypes.missing import isna
2423

25-
from pandas.core import ops
2624
from pandas.core.ops import invalid_comparison
2725
from pandas.core.tools.numeric import to_numeric
2826

@@ -316,53 +314,6 @@ def _from_sequence_of_strings(
316314
scalars = to_numeric(strings, errors="raise")
317315
return cls._from_sequence(scalars, dtype=dtype, copy=copy)
318316

319-
_HANDLED_TYPES = (np.ndarray, numbers.Number)
320-
321-
def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
322-
# For IntegerArray inputs, we apply the ufunc to ._data
323-
# and mask the result.
324-
if method == "reduce":
325-
# Not clear how to handle missing values in reductions. Raise.
326-
raise NotImplementedError("The 'reduce' method is not supported.")
327-
out = kwargs.get("out", ())
328-
329-
for x in inputs + out:
330-
if not isinstance(x, self._HANDLED_TYPES + (IntegerArray,)):
331-
return NotImplemented
332-
333-
# for binary ops, use our custom dunder methods
334-
result = ops.maybe_dispatch_ufunc_to_dunder_op(
335-
self, ufunc, method, *inputs, **kwargs
336-
)
337-
if result is not NotImplemented:
338-
return result
339-
340-
mask = np.zeros(len(self), dtype=bool)
341-
inputs2 = []
342-
for x in inputs:
343-
if isinstance(x, IntegerArray):
344-
mask |= x._mask
345-
inputs2.append(x._data)
346-
else:
347-
inputs2.append(x)
348-
349-
def reconstruct(x):
350-
# we don't worry about scalar `x` here, since we
351-
# raise for reduce up above.
352-
353-
if is_integer_dtype(x.dtype):
354-
m = mask.copy()
355-
return IntegerArray(x, m)
356-
else:
357-
x[mask] = np.nan
358-
return x
359-
360-
result = getattr(ufunc, method)(*inputs2, **kwargs)
361-
if isinstance(result, tuple):
362-
return tuple(reconstruct(x) for x in result)
363-
else:
364-
return reconstruct(result)
365-
366317
def _coerce_to_array(self, value) -> Tuple[np.ndarray, np.ndarray]:
367318
return coerce_to_array(value, dtype=self.dtype)
368319

pandas/core/arrays/numeric.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
2-
from typing import TYPE_CHECKING, Union
2+
import numbers
3+
from typing import TYPE_CHECKING, Any, List, Union
34

45
import numpy as np
56

@@ -14,6 +15,8 @@
1415
is_list_like,
1516
)
1617

18+
from pandas.core import ops
19+
1720
from .masked import BaseMaskedArray, BaseMaskedDtype
1821

1922
if TYPE_CHECKING:
@@ -130,3 +133,57 @@ def _arith_method(self, other, op):
130133
)
131134

132135
return self._maybe_mask_result(result, mask, other, op_name)
136+
137+
_HANDLED_TYPES = (np.ndarray, numbers.Number)
138+
139+
def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
140+
# For NumericArray inputs, we apply the ufunc to ._data
141+
# and mask the result.
142+
if method == "reduce":
143+
# Not clear how to handle missing values in reductions. Raise.
144+
raise NotImplementedError("The 'reduce' method is not supported.")
145+
out = kwargs.get("out", ())
146+
147+
for x in inputs + out:
148+
if not isinstance(x, self._HANDLED_TYPES + (NumericArray,)):
149+
return NotImplemented
150+
151+
# for binary ops, use our custom dunder methods
152+
result = ops.maybe_dispatch_ufunc_to_dunder_op(
153+
self, ufunc, method, *inputs, **kwargs
154+
)
155+
if result is not NotImplemented:
156+
return result
157+
158+
mask = np.zeros(len(self), dtype=bool)
159+
inputs2: List[Any] = []
160+
for x in inputs:
161+
if isinstance(x, NumericArray):
162+
mask |= x._mask
163+
inputs2.append(x._data)
164+
else:
165+
inputs2.append(x)
166+
167+
def reconstruct(x):
168+
# we don't worry about scalar `x` here, since we
169+
# raise for reduce up above.
170+
171+
if is_integer_dtype(x.dtype):
172+
from pandas.core.arrays import IntegerArray
173+
174+
m = mask.copy()
175+
return IntegerArray(x, m)
176+
elif is_float_dtype(x.dtype):
177+
from pandas.core.arrays import FloatingArray
178+
179+
m = mask.copy()
180+
return FloatingArray(x, m)
181+
else:
182+
x[mask] = np.nan
183+
return x
184+
185+
result = getattr(ufunc, method)(*inputs2, **kwargs)
186+
if isinstance(result, tuple):
187+
return tuple(reconstruct(x) for x in result)
188+
else:
189+
return reconstruct(result)

pandas/tests/arrays/integer/test_function.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import pandas as pd
55
import pandas._testing as tm
6+
from pandas.core.arrays import FloatingArray
67

78

89
@pytest.mark.parametrize("ufunc", [np.abs, np.sign])
@@ -25,13 +26,13 @@ def test_ufuncs_single_float(ufunc):
2526
a = pd.array([1, 2, -3, np.nan])
2627
with np.errstate(invalid="ignore"):
2728
result = ufunc(a)
28-
expected = ufunc(a.astype(float))
29-
tm.assert_numpy_array_equal(result, expected)
29+
expected = FloatingArray(ufunc(a.astype(float)), mask=a._mask)
30+
tm.assert_extension_array_equal(result, expected)
3031

3132
s = pd.Series(a)
3233
with np.errstate(invalid="ignore"):
3334
result = ufunc(s)
34-
expected = ufunc(s.astype(float))
35+
expected = pd.Series(expected)
3536
tm.assert_series_equal(result, expected)
3637

3738

@@ -67,14 +68,13 @@ def test_ufunc_binary_output():
6768
a = pd.array([1, 2, np.nan])
6869
result = np.modf(a)
6970
expected = np.modf(a.to_numpy(na_value=np.nan, dtype="float"))
71+
expected = (pd.array(expected[0]), pd.array(expected[1]))
7072

7173
assert isinstance(result, tuple)
7274
assert len(result) == 2
7375

7476
for x, y in zip(result, expected):
75-
# TODO(FloatArray): This will return an extension array.
76-
# y = pd.array(y)
77-
tm.assert_numpy_array_equal(x, y)
77+
tm.assert_extension_array_equal(x, y)
7878

7979

8080
@pytest.mark.parametrize("values", [[0, 1], [0, None]])

0 commit comments

Comments
 (0)