Skip to content

Commit 4c5e6fa

Browse files
authored
ENH: Implement unary operators for FloatingArray class (#39916)
1 parent 879cd22 commit 4c5e6fa

File tree

7 files changed

+78
-73
lines changed

7 files changed

+78
-73
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Other enhancements
137137
- :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`)
138138
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
139139
- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)
140+
- Add support for unary operators in :class:`FloatingArray` (:issue:`38749`)
140141

141142
.. ---------------------------------------------------------------------------
142143

pandas/core/arrays/integer.py

-9
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,6 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False):
315315
)
316316
super().__init__(values, mask, copy=copy)
317317

318-
def __neg__(self):
319-
return type(self)(-self._data, self._mask.copy())
320-
321-
def __pos__(self):
322-
return self
323-
324-
def __abs__(self):
325-
return type(self)(np.abs(self._data), self._mask.copy())
326-
327318
@classmethod
328319
def _from_sequence(
329320
cls, scalars, *, dtype: Optional[Dtype] = None, copy: bool = False

pandas/core/arrays/numeric.py

+9
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,12 @@ def reconstruct(x):
199199
return tuple(reconstruct(x) for x in result)
200200
else:
201201
return reconstruct(result)
202+
203+
def __neg__(self):
204+
return type(self)(-self._data, self._mask.copy())
205+
206+
def __pos__(self):
207+
return self
208+
209+
def __abs__(self):
210+
return type(self)(abs(self._data), self._mask.copy())

pandas/tests/arrays/floating/test_arithmetic.py

+21
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,24 @@ def test_cross_type_arithmetic():
180180
result = df.A + df.B
181181
expected = pd.Series([2, np.nan, np.nan], dtype="Float64")
182182
tm.assert_series_equal(result, expected)
183+
184+
185+
@pytest.mark.parametrize(
186+
"source, neg_target, abs_target",
187+
[
188+
([1.1, 2.2, 3.3], [-1.1, -2.2, -3.3], [1.1, 2.2, 3.3]),
189+
([1.1, 2.2, None], [-1.1, -2.2, None], [1.1, 2.2, None]),
190+
([-1.1, 0.0, 1.1], [1.1, 0.0, -1.1], [1.1, 0.0, 1.1]),
191+
],
192+
)
193+
def test_unary_float_operators(float_ea_dtype, source, neg_target, abs_target):
194+
# GH38794
195+
dtype = float_ea_dtype
196+
arr = pd.array(source, dtype=dtype)
197+
neg_result, pos_result, abs_result = -arr, +arr, abs(arr)
198+
neg_target = pd.array(neg_target, dtype=dtype)
199+
abs_target = pd.array(abs_target, dtype=dtype)
200+
201+
tm.assert_extension_array_equal(neg_result, neg_target)
202+
tm.assert_extension_array_equal(pos_result, arr)
203+
tm.assert_extension_array_equal(abs_result, abs_target)

pandas/tests/arrays/integer/test_arithmetic.py

+13-27
Original file line numberDiff line numberDiff line change
@@ -284,36 +284,22 @@ def test_reduce_to_float(op):
284284

285285

286286
@pytest.mark.parametrize(
287-
"source, target",
287+
"source, neg_target, abs_target",
288288
[
289-
([1, 2, 3], [-1, -2, -3]),
290-
([1, 2, None], [-1, -2, None]),
291-
([-1, 0, 1], [1, 0, -1]),
289+
([1, 2, 3], [-1, -2, -3], [1, 2, 3]),
290+
([1, 2, None], [-1, -2, None], [1, 2, None]),
291+
([-1, 0, 1], [1, 0, -1], [1, 0, 1]),
292292
],
293293
)
294-
def test_unary_minus_nullable_int(any_signed_nullable_int_dtype, source, target):
294+
def test_unary_int_operators(
295+
any_signed_nullable_int_dtype, source, neg_target, abs_target
296+
):
295297
dtype = any_signed_nullable_int_dtype
296298
arr = pd.array(source, dtype=dtype)
297-
result = -arr
298-
expected = pd.array(target, dtype=dtype)
299-
tm.assert_extension_array_equal(result, expected)
300-
301-
302-
@pytest.mark.parametrize("source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]])
303-
def test_unary_plus_nullable_int(any_signed_nullable_int_dtype, source):
304-
dtype = any_signed_nullable_int_dtype
305-
expected = pd.array(source, dtype=dtype)
306-
result = +expected
307-
tm.assert_extension_array_equal(result, expected)
299+
neg_result, pos_result, abs_result = -arr, +arr, abs(arr)
300+
neg_target = pd.array(neg_target, dtype=dtype)
301+
abs_target = pd.array(abs_target, dtype=dtype)
308302

309-
310-
@pytest.mark.parametrize(
311-
"source, target",
312-
[([1, 2, 3], [1, 2, 3]), ([1, -2, None], [1, 2, None]), ([-1, 0, 1], [1, 0, 1])],
313-
)
314-
def test_abs_nullable_int(any_signed_nullable_int_dtype, source, target):
315-
dtype = any_signed_nullable_int_dtype
316-
s = pd.array(source, dtype=dtype)
317-
result = abs(s)
318-
expected = pd.array(target, dtype=dtype)
319-
tm.assert_extension_array_equal(result, expected)
303+
tm.assert_extension_array_equal(neg_result, neg_target)
304+
tm.assert_extension_array_equal(pos_result, arr)
305+
tm.assert_extension_array_equal(abs_result, abs_target)

pandas/tests/arrays/masked/test_arithmetic.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,14 @@ def test_error_len_mismatch(data, all_arithmetic_operators):
165165

166166

167167
@pytest.mark.parametrize("op", ["__neg__", "__abs__", "__invert__"])
168-
@pytest.mark.parametrize(
169-
"values, dtype", [([1, 2, 3], "Int64"), ([True, False, True], "boolean")]
170-
)
171-
def test_unary_op_does_not_propagate_mask(op, values, dtype):
168+
def test_unary_op_does_not_propagate_mask(data, op, request):
172169
# https://github.com/pandas-dev/pandas/issues/39943
173-
s = pd.Series(values, dtype=dtype)
170+
data, _ = data
171+
if data.dtype in ["Float32", "Float64"] and op == "__invert__":
172+
request.node.add_marker(
173+
pytest.mark.xfail(reason="invert is not implemented for float ea dtypes")
174+
)
175+
s = pd.Series(data)
174176
result = getattr(s, op)()
175177
expected = result.copy(deep=True)
176178
s[0] = None

pandas/tests/series/test_unary.py

+27-32
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,35 @@ def test_invert(self):
1818
tm.assert_series_equal(-(ser < 0), ~(ser < 0))
1919

2020
@pytest.mark.parametrize(
21-
"source, target",
21+
"source, neg_target, abs_target",
2222
[
23-
([1, 2, 3], [-1, -2, -3]),
24-
([1, 2, None], [-1, -2, None]),
25-
([-1, 0, 1], [1, 0, -1]),
23+
([1, 2, 3], [-1, -2, -3], [1, 2, 3]),
24+
([1, 2, None], [-1, -2, None], [1, 2, None]),
2625
],
2726
)
28-
def test_unary_minus_nullable_int(
29-
self, any_signed_nullable_int_dtype, source, target
27+
def test_all_numeric_unary_operators(
28+
self, any_nullable_numeric_dtype, source, neg_target, abs_target
3029
):
31-
dtype = any_signed_nullable_int_dtype
30+
# GH38794
31+
dtype = any_nullable_numeric_dtype
3232
ser = Series(source, dtype=dtype)
33-
result = -ser
34-
expected = Series(target, dtype=dtype)
35-
tm.assert_series_equal(result, expected)
36-
37-
@pytest.mark.parametrize("source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]])
38-
def test_unary_plus_nullable_int(self, any_signed_nullable_int_dtype, source):
39-
dtype = any_signed_nullable_int_dtype
40-
expected = Series(source, dtype=dtype)
41-
result = +expected
42-
tm.assert_series_equal(result, expected)
43-
44-
@pytest.mark.parametrize(
45-
"source, target",
46-
[
47-
([1, 2, 3], [1, 2, 3]),
48-
([1, -2, None], [1, 2, None]),
49-
([-1, 0, 1], [1, 0, 1]),
50-
],
51-
)
52-
def test_abs_nullable_int(self, any_signed_nullable_int_dtype, source, target):
53-
dtype = any_signed_nullable_int_dtype
54-
ser = Series(source, dtype=dtype)
55-
result = abs(ser)
56-
expected = Series(target, dtype=dtype)
57-
tm.assert_series_equal(result, expected)
33+
neg_result, pos_result, abs_result = -ser, +ser, abs(ser)
34+
if dtype.startswith("U"):
35+
neg_target = -Series(source, dtype=dtype)
36+
else:
37+
neg_target = Series(neg_target, dtype=dtype)
38+
39+
abs_target = Series(abs_target, dtype=dtype)
40+
41+
tm.assert_series_equal(neg_result, neg_target)
42+
tm.assert_series_equal(pos_result, ser)
43+
tm.assert_series_equal(abs_result, abs_target)
44+
45+
@pytest.mark.parametrize("op", ["__neg__", "__abs__"])
46+
def test_unary_float_op_mask(self, float_ea_dtype, op):
47+
dtype = float_ea_dtype
48+
ser = Series([1.1, 2.2, 3.3], dtype=dtype)
49+
result = getattr(ser, op)()
50+
target = result.copy(deep=True)
51+
ser[0] = None
52+
tm.assert_series_equal(result, target)

0 commit comments

Comments
 (0)