Skip to content

Commit b2d54d9

Browse files
authored
BUG: IntegerArray/FloatingArray ufunc with 'out' kwd (#45122)
1 parent 45113f8 commit b2d54d9

File tree

4 files changed

+46
-4
lines changed

4 files changed

+46
-4
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ ExtensionArray
980980
- Bug in :func:`array` failing to preserve :class:`PandasArray` (:issue:`43887`)
981981
- NumPy ufuncs ``np.abs``, ``np.positive``, ``np.negative`` now correctly preserve dtype when called on ExtensionArrays that implement ``__abs__, __pos__, __neg__``, respectively. In particular this is fixed for :class:`TimedeltaArray` (:issue:`43899`, :issue:`23316`)
982982
- NumPy ufuncs ``np.minimum.reduce`` ``np.maximum.reduce``, ``np.add.reduce``, and ``np.prod.reduce`` now work correctly instead of raising ``NotImplementedError`` on :class:`Series` with ``IntegerDtype`` or ``FloatDtype`` (:issue:`43923`, :issue:`44793`)
983+
- NumPy ufuncs with ``out`` keyword are now supported by arrays with ``IntegerDtype`` and ``FloatingDtype`` (:issue:`??`)
983984
- Avoid raising ``PerformanceWarning`` about fragmented DataFrame when using many columns with an extension dtype (:issue:`44098`)
984985
- Bug in :class:`IntegerArray` and :class:`FloatingArray` construction incorrectly coercing mismatched NA values (e.g. ``np.timedelta64("NaT")``) to numeric NA (:issue:`44514`)
985986
- Bug in :meth:`BooleanArray.__eq__` and :meth:`BooleanArray.__ne__` raising ``TypeError`` on comparison with an incompatible type (like a string). This caused :meth:`DataFrame.replace` to sometimes raise a ``TypeError`` if a nullable boolean column was included (:issue:`44499`)

pandas/core/arrays/masked.py

+6
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,12 @@ def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
433433
if result is not NotImplemented:
434434
return result
435435

436+
if "out" in kwargs:
437+
# e.g. test_ufunc_with_out
438+
return arraylike.dispatch_ufunc_with_out(
439+
self, ufunc, method, *inputs, **kwargs
440+
)
441+
436442
if method == "reduce":
437443
result = arraylike.dispatch_reduction_ufunc(
438444
self, ufunc, method, *inputs, **kwargs

pandas/core/missing.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,15 @@ def mask_missing(arr: ArrayLike, values_to_mask) -> npt.NDArray[np.bool_]:
9292
# GH#29553 prevent numpy deprecation warnings
9393
pass
9494
else:
95-
mask |= arr == x
95+
new_mask = arr == x
96+
if not isinstance(new_mask, np.ndarray):
97+
# usually BooleanArray
98+
new_mask = new_mask.to_numpy(dtype=bool, na_value=False)
99+
mask |= new_mask
96100

97101
if na_mask.any():
98102
mask |= isna(arr)
99103

100-
if not isinstance(mask, np.ndarray):
101-
# e.g. if arr is IntegerArray, then mask is BooleanArray
102-
mask = mask.to_numpy(dtype=bool, na_value=False)
103104
return mask
104105

105106

pandas/tests/arrays/masked_shared.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Tests shared by MaskedArray subclasses.
33
"""
4+
import numpy as np
5+
import pytest
46

57
import pandas as pd
68
import pandas._testing as tm
@@ -102,3 +104,35 @@ def test_compare_to_string(self, dtype):
102104
expected = pd.Series([False, pd.NA], dtype="boolean")
103105

104106
self.assert_series_equal(result, expected)
107+
108+
def test_ufunc_with_out(self, dtype):
109+
arr = pd.array([1, 2, 3], dtype=dtype)
110+
arr2 = pd.array([1, 2, pd.NA], dtype=dtype)
111+
112+
mask = arr == arr
113+
mask2 = arr2 == arr2
114+
115+
result = np.zeros(3, dtype=bool)
116+
result |= mask
117+
# If MaskedArray.__array_ufunc__ handled "out" appropriately,
118+
# `result` should still be an ndarray.
119+
assert isinstance(result, np.ndarray)
120+
assert result.all()
121+
122+
# result |= mask worked because mask could be cast lossslessly to
123+
# boolean ndarray. mask2 can't, so this raises
124+
result = np.zeros(3, dtype=bool)
125+
msg = "Specify an appropriate 'na_value' for this dtype"
126+
with pytest.raises(ValueError, match=msg):
127+
result |= mask2
128+
129+
# addition
130+
res = np.add(arr, arr2)
131+
expected = pd.array([2, 4, pd.NA], dtype=dtype)
132+
tm.assert_extension_array_equal(res, expected)
133+
134+
# when passing out=arr, we will modify 'arr' inplace.
135+
res = np.add(arr, arr2, out=arr)
136+
assert res is arr
137+
tm.assert_extension_array_equal(res, expected)
138+
tm.assert_extension_array_equal(arr, expected)

0 commit comments

Comments
 (0)