Skip to content

ENH: Implement unary operators for FloatingArray class #39916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 28, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e40c3c6
Add neg, pos, abs methods to FloatingArray
zitorelova Feb 19, 2021
9929bc4
Add tests for arrays
zitorelova Feb 19, 2021
1952467
Add tests for Series objects
zitorelova Feb 19, 2021
5fdf667
Add whatsnew entry for v1.3.0
zitorelova Feb 19, 2021
de24336
Fix pre-commit errors
zitorelova Feb 19, 2021
853bc5d
Simplify testing for FloatingArray unary operators
zitorelova Feb 19, 2021
6a3e07d
Move whatsnew entry to enhancements
zitorelova Feb 19, 2021
03801fc
Add fixture for signed nullable numeric dtypes
zitorelova Feb 20, 2021
1b9759d
Consolidate all numeric unary operator tests
zitorelova Feb 20, 2021
bb77076
Consolidate integer unary operator tests
zitorelova Feb 20, 2021
42e8cdf
Fix shared mask bug
zitorelova Feb 22, 2021
957edc3
Add test for float op mask
zitorelova Feb 22, 2021
38c610b
Do not return copy when using pos op
zitorelova Feb 22, 2021
eba6e04
Don't test pos
zitorelova Feb 23, 2021
be5eb84
Remove fixture
zitorelova Feb 23, 2021
9367b1b
Edit test to include all numeric dtypes
zitorelova Feb 23, 2021
3e67655
Fix pre-commit
zitorelova Feb 23, 2021
495ddb7
Move unary operator definitions to NumericArray
zitorelova Feb 26, 2021
0162e07
Test mask on all EA dtypes
zitorelova Feb 26, 2021
7dda62d
Add newline to numeric.py
zitorelova Feb 26, 2021
41e42fc
Remove definitions cause they've been moved to NumericArray
zitorelova Feb 26, 2021
cf2732a
Xfail instead of skip for invert ops on float ea dtypes
zitorelova Feb 27, 2021
3cc5212
Fix pre-commit error
zitorelova Feb 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Other enhancements
- :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`)
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)
- Add support for unary operators in :class:`FloatingArray` (:issue:`38749`)

.. ---------------------------------------------------------------------------

Expand Down
9 changes: 9 additions & 0 deletions pandas/core/arrays/numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,12 @@ def reconstruct(x):
return tuple(reconstruct(x) for x in result)
else:
return reconstruct(result)

def __neg__(self):
return type(self)(-self._data, self._mask.copy())

def __pos__(self):
return self

def __abs__(self):
return type(self)(abs(self._data), self._mask.copy())
21 changes: 21 additions & 0 deletions pandas/tests/arrays/floating/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,24 @@ def test_cross_type_arithmetic():
result = df.A + df.B
expected = pd.Series([2, np.nan, np.nan], dtype="Float64")
tm.assert_series_equal(result, expected)


@pytest.mark.parametrize(
"source, neg_target, abs_target",
[
([1.1, 2.2, 3.3], [-1.1, -2.2, -3.3], [1.1, 2.2, 3.3]),
([1.1, 2.2, None], [-1.1, -2.2, None], [1.1, 2.2, None]),
([-1.1, 0.0, 1.1], [1.1, 0.0, -1.1], [1.1, 0.0, 1.1]),
],
)
def test_unary_float_operators(float_ea_dtype, source, neg_target, abs_target):
# GH38794
dtype = float_ea_dtype
arr = pd.array(source, dtype=dtype)
neg_result, pos_result, abs_result = -arr, +arr, abs(arr)
neg_target = pd.array(neg_target, dtype=dtype)
abs_target = pd.array(abs_target, dtype=dtype)

tm.assert_extension_array_equal(neg_result, neg_target)
tm.assert_extension_array_equal(pos_result, arr)
tm.assert_extension_array_equal(abs_result, abs_target)
40 changes: 13 additions & 27 deletions pandas/tests/arrays/integer/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,36 +284,22 @@ def test_reduce_to_float(op):


@pytest.mark.parametrize(
"source, target",
"source, neg_target, abs_target",
[
([1, 2, 3], [-1, -2, -3]),
([1, 2, None], [-1, -2, None]),
([-1, 0, 1], [1, 0, -1]),
([1, 2, 3], [-1, -2, -3], [1, 2, 3]),
([1, 2, None], [-1, -2, None], [1, 2, None]),
([-1, 0, 1], [1, 0, -1], [1, 0, 1]),
],
)
def test_unary_minus_nullable_int(any_signed_nullable_int_dtype, source, target):
def test_unary_int_operators(
any_signed_nullable_int_dtype, source, neg_target, abs_target
):
dtype = any_signed_nullable_int_dtype
arr = pd.array(source, dtype=dtype)
result = -arr
expected = pd.array(target, dtype=dtype)
tm.assert_extension_array_equal(result, expected)


@pytest.mark.parametrize("source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]])
def test_unary_plus_nullable_int(any_signed_nullable_int_dtype, source):
dtype = any_signed_nullable_int_dtype
expected = pd.array(source, dtype=dtype)
result = +expected
tm.assert_extension_array_equal(result, expected)
neg_result, pos_result, abs_result = -arr, +arr, abs(arr)
neg_target = pd.array(neg_target, dtype=dtype)
abs_target = pd.array(abs_target, dtype=dtype)


@pytest.mark.parametrize(
"source, target",
[([1, 2, 3], [1, 2, 3]), ([1, -2, None], [1, 2, None]), ([-1, 0, 1], [1, 0, 1])],
)
def test_abs_nullable_int(any_signed_nullable_int_dtype, source, target):
dtype = any_signed_nullable_int_dtype
s = pd.array(source, dtype=dtype)
result = abs(s)
expected = pd.array(target, dtype=dtype)
tm.assert_extension_array_equal(result, expected)
tm.assert_extension_array_equal(neg_result, neg_target)
tm.assert_extension_array_equal(pos_result, arr)
tm.assert_extension_array_equal(abs_result, abs_target)
10 changes: 5 additions & 5 deletions pandas/tests/arrays/masked/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ def test_error_len_mismatch(data, all_arithmetic_operators):


@pytest.mark.parametrize("op", ["__neg__", "__abs__", "__invert__"])
@pytest.mark.parametrize(
"values, dtype", [([1, 2, 3], "Int64"), ([True, False, True], "boolean")]
)
def test_unary_op_does_not_propagate_mask(op, values, dtype):
def test_unary_op_does_not_propagate_mask(data, op):
# https://github.com/pandas-dev/pandas/issues/39943
s = pd.Series(values, dtype=dtype)
data, _ = data
if data.dtype in ["Float32", "Float64"] and op == "__invert__":
pytest.skip("invert is not implemented for float ea dtypes")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe change this to add an xfail marker instead of skipping

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set to xfail

s = pd.Series(data)
result = getattr(s, op)()
expected = result.copy(deep=True)
s[0] = None
Expand Down
59 changes: 27 additions & 32 deletions pandas/tests/series/test_unary.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,35 @@ def test_invert(self):
tm.assert_series_equal(-(ser < 0), ~(ser < 0))

@pytest.mark.parametrize(
"source, target",
"source, neg_target, abs_target",
[
([1, 2, 3], [-1, -2, -3]),
([1, 2, None], [-1, -2, None]),
([-1, 0, 1], [1, 0, -1]),
([1, 2, 3], [-1, -2, -3], [1, 2, 3]),
([1, 2, None], [-1, -2, None], [1, 2, None]),
],
)
def test_unary_minus_nullable_int(
self, any_signed_nullable_int_dtype, source, target
def test_all_numeric_unary_operators(
self, any_nullable_numeric_dtype, source, neg_target, abs_target
):
dtype = any_signed_nullable_int_dtype
# GH38794
dtype = any_nullable_numeric_dtype
ser = Series(source, dtype=dtype)
result = -ser
expected = Series(target, dtype=dtype)
tm.assert_series_equal(result, expected)

@pytest.mark.parametrize("source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]])
def test_unary_plus_nullable_int(self, any_signed_nullable_int_dtype, source):
dtype = any_signed_nullable_int_dtype
expected = Series(source, dtype=dtype)
result = +expected
tm.assert_series_equal(result, expected)

@pytest.mark.parametrize(
"source, target",
[
([1, 2, 3], [1, 2, 3]),
([1, -2, None], [1, 2, None]),
([-1, 0, 1], [1, 0, 1]),
],
)
def test_abs_nullable_int(self, any_signed_nullable_int_dtype, source, target):
dtype = any_signed_nullable_int_dtype
ser = Series(source, dtype=dtype)
result = abs(ser)
expected = Series(target, dtype=dtype)
tm.assert_series_equal(result, expected)
neg_result, pos_result, abs_result = -ser, +ser, abs(ser)
if dtype.startswith("U"):
neg_target = -Series(source, dtype=dtype)
else:
neg_target = Series(neg_target, dtype=dtype)

abs_target = Series(abs_target, dtype=dtype)

tm.assert_series_equal(neg_result, neg_target)
tm.assert_series_equal(pos_result, ser)
tm.assert_series_equal(abs_result, abs_target)

@pytest.mark.parametrize("op", ["__neg__", "__abs__"])
def test_unary_float_op_mask(self, float_ea_dtype, op):
dtype = float_ea_dtype
ser = Series([1.1, 2.2, 3.3], dtype=dtype)
result = getattr(ser, op)()
target = result.copy(deep=True)
ser[0] = None
tm.assert_series_equal(result, target)