From 37f7826e6c23c1b1f3eb05c12668bc7dcbcde8dc Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 22 Jun 2018 09:45:56 +0200 Subject: [PATCH 1/4] API/REGR: (re-)allow neg/pos unary operation on object dtype --- doc/source/whatsnew/v0.23.2.txt | 2 +- pandas/core/generic.py | 10 ++-------- pandas/tests/frame/test_operators.py | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/doc/source/whatsnew/v0.23.2.txt b/doc/source/whatsnew/v0.23.2.txt index c781f45715bd4..3eef63ee39b80 100644 --- a/doc/source/whatsnew/v0.23.2.txt +++ b/doc/source/whatsnew/v0.23.2.txt @@ -18,7 +18,7 @@ Fixed Regressions - Fixed regression in :meth:`to_csv` when handling file-like object incorrectly (:issue:`21471`) - Bug in both :meth:`DataFrame.first_valid_index` and :meth:`Series.first_valid_index` raised for a row index having duplicate values (:issue:`21441`) -- +- Fixed regression in unary negative operations with object dtype (:issue:`21380`) .. _whatsnew_0232.performance: diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 9902da4094404..97eacb5c1afe6 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1117,22 +1117,16 @@ def __neg__(self): values = com._values_from_object(self) if is_bool_dtype(values): arr = operator.inv(values) - elif (is_numeric_dtype(values) or is_timedelta64_dtype(values)): - arr = operator.neg(values) else: - raise TypeError("Unary negative expects numeric dtype, not {}" - .format(values.dtype)) + arr = operator.neg(values) return self.__array_wrap__(arr) def __pos__(self): values = com._values_from_object(self) if (is_bool_dtype(values) or is_period_arraylike(values)): arr = values - elif (is_numeric_dtype(values) or is_timedelta64_dtype(values)): - arr = operator.pos(values) else: - raise TypeError("Unary plus expects numeric dtype, not {}" - .format(values.dtype)) + arr = operator.pos(values) return self.__array_wrap__(arr) def __invert__(self): diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 5df50f3d7835b..dd54707fb788d 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -3,6 +3,7 @@ from __future__ import print_function from collections import deque from datetime import datetime +from decimal import Decimal import operator import pytest @@ -282,6 +283,17 @@ def test_neg_numeric(self, df, expected): assert_frame_equal(-df, expected) assert_series_equal(-df['a'], expected['a']) + @pytest.mark.parametrize('df, expected', [ + (np.array([1, 2], dtype=object), np.array([-1, -2], dtype=object)), + ([Decimal('1.0'), Decimal('2.0')], [Decimal('-1.0'), Decimal('-2.0')]), + ]) + def test_neg_object(self, df, expected): + # GH 21380 + df = pd.DataFrame({'a': df}) + expected = pd.DataFrame({'a': expected}) + assert_frame_equal(-df, expected) + assert_series_equal(-df['a'], expected['a']) + @pytest.mark.parametrize('df', [ pd.DataFrame({'a': ['a', 'b']}), pd.DataFrame({'a': pd.to_datetime(['2017-01-22', '1970-01-01'])}), @@ -305,6 +317,15 @@ def test_pos_numeric(self, df): assert_frame_equal(+df, df) assert_series_equal(+df['a'], df['a']) + @pytest.mark.parametrize('df', [ + pd.DataFrame({'a': np.array([-1, 2], dtype=object)}), + pd.DataFrame({'a': [Decimal('-1.0'), Decimal('2.0')]}), + ]) + def test_pos_object(self, df): + # GH 21380 + assert_frame_equal(+df, df) + assert_series_equal(+df['a'], df['a']) + @pytest.mark.parametrize('df', [ pd.DataFrame({'a': ['a', 'b']}), pd.DataFrame({'a': pd.to_datetime(['2017-01-22', '1970-01-01'])}), From 5068ebbf18be31bcd08b0e3ba089d7e05bbf0640 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Wed, 27 Jun 2018 06:16:04 -0400 Subject: [PATCH 2/4] allow non-string dtypes --- pandas/core/generic.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 77fa36a191792..91397f4db7458 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -10,7 +10,7 @@ import numpy as np import pandas as pd -from pandas._libs import tslib, properties +from pandas._libs import tslib, properties, lib from pandas.core.dtypes.common import ( _ensure_int64, _ensure_object, @@ -27,6 +27,7 @@ is_dict_like, is_re_compilable, is_period_arraylike, + is_object_dtype, pandas_dtype) from pandas.core.dtypes.cast import maybe_promote, maybe_upcast_putmask from pandas.core.dtypes.inference import is_hashable @@ -1117,16 +1118,30 @@ def __neg__(self): values = com._values_from_object(self) if is_bool_dtype(values): arr = operator.inv(values) - else: + elif is_numeric_dtype(values) or is_timedelta64_dtype(values): + arr = operator.neg(values) + elif (is_object_dtype(values) and + lib.infer_dtype(values) not in ['string', 'bytes', 'unicode']): + # explicity allow object dtypes that are not strings, gh-21380 arr = operator.neg(values) + else: + raise TypeError("Unary negative expects numeric dtype, not {}" + .format(values.dtype)) return self.__array_wrap__(arr) def __pos__(self): values = com._values_from_object(self) if (is_bool_dtype(values) or is_period_arraylike(values)): arr = values - else: + elif is_numeric_dtype(values) or is_timedelta64_dtype(values): + arr = operator.pos(values) + elif (is_object_dtype(values) and + lib.infer_dtype(values) not in ['string', 'bytes', 'unicode']): + # explicity allow object dtypes that are not strings, gh-21380 arr = operator.pos(values) + else: + raise TypeError("Unary plus expects numeric dtype, not {}" + .format(values.dtype)) return self.__array_wrap__(arr) def __invert__(self): From 8a81dee53d471e7598b0890fd5672a4ef3bd09ed Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Wed, 27 Jun 2018 23:28:21 +0200 Subject: [PATCH 3/4] simplify object dtype check --- pandas/core/generic.py | 14 ++++---------- pandas/tests/frame/test_operators.py | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 91397f4db7458..39d1e406bc2ff 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1118,11 +1118,8 @@ def __neg__(self): values = com._values_from_object(self) if is_bool_dtype(values): arr = operator.inv(values) - elif is_numeric_dtype(values) or is_timedelta64_dtype(values): - arr = operator.neg(values) - elif (is_object_dtype(values) and - lib.infer_dtype(values) not in ['string', 'bytes', 'unicode']): - # explicity allow object dtypes that are not strings, gh-21380 + elif (is_numeric_dtype(values) or is_timedelta64_dtype(values) + or is_object_dtype(values)): arr = operator.neg(values) else: raise TypeError("Unary negative expects numeric dtype, not {}" @@ -1133,11 +1130,8 @@ def __pos__(self): values = com._values_from_object(self) if (is_bool_dtype(values) or is_period_arraylike(values)): arr = values - elif is_numeric_dtype(values) or is_timedelta64_dtype(values): - arr = operator.pos(values) - elif (is_object_dtype(values) and - lib.infer_dtype(values) not in ['string', 'bytes', 'unicode']): - # explicity allow object dtypes that are not strings, gh-21380 + elif (is_numeric_dtype(values) or is_timedelta64_dtype(values) + or is_object_dtype(values)): arr = operator.pos(values) else: raise TypeError("Unary plus expects numeric dtype, not {}" diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index dd54707fb788d..fdf50805ad818 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -318,6 +318,7 @@ def test_pos_numeric(self, df): assert_series_equal(+df['a'], df['a']) @pytest.mark.parametrize('df', [ + pd.DataFrame({'a': ['a', 'b']}), pd.DataFrame({'a': np.array([-1, 2], dtype=object)}), pd.DataFrame({'a': [Decimal('-1.0'), Decimal('2.0')]}), ]) @@ -327,7 +328,6 @@ def test_pos_object(self, df): assert_series_equal(+df['a'], df['a']) @pytest.mark.parametrize('df', [ - pd.DataFrame({'a': ['a', 'b']}), pd.DataFrame({'a': pd.to_datetime(['2017-01-22', '1970-01-01'])}), ]) def test_pos_raises(self, df): From 1dcd4cff931454259c25b4a36c72ba2b488c1e56 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Thu, 28 Jun 2018 14:51:29 +0200 Subject: [PATCH 4/4] fix unused import --- pandas/core/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 39d1e406bc2ff..26c23b84a9c04 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -10,7 +10,7 @@ import numpy as np import pandas as pd -from pandas._libs import tslib, properties, lib +from pandas._libs import tslib, properties from pandas.core.dtypes.common import ( _ensure_int64, _ensure_object,