From b0187158f73e415f27763e163d3a8403444695ab Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Thu, 18 Jan 2018 06:53:26 -0600 Subject: [PATCH 01/12] BUG: correct behavior for unary plus and negative Adds special method for unary plus to ndframe generics, and restricts the allowed dtypes for the unary negative. Closes #16073. --- pandas/core/generic.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index cb4bbb7b27c42..ff893a3afadd1 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1029,8 +1029,22 @@ def __neg__(self): values = com._values_from_object(self) if values.dtype == np.bool_: arr = operator.inv(values) - else: + 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)) + return self.__array_wrap__(arr) + + def __pos__(self): + values = _values_from_object(self) + if values.dtype == np.bool_: + 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)) return self.__array_wrap__(arr) def __invert__(self): From 8429618889076c92e4dbf7f68d0e43ddf904cd9d Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Thu, 18 Jan 2018 06:56:00 -0600 Subject: [PATCH 02/12] TST: adds tests for unary plus and unary neg --- pandas/tests/frame/test_operators.py | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 26974b6398694..ba44ced29cdad 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -272,12 +272,42 @@ def test_logical_with_nas(self): assert_series_equal(result, expected) def test_neg(self): - # what to do? - assert_frame_equal(-self.frame, -1 * self.frame) + numeric = pd.DataFrame({ + 'a': [-1, 0, 1], + 'b': [1, 0, 1], + }) + boolean = pd.DataFrame({ + 'a': [True, False, True], + 'b': [False, False, True] + }) + timedelta = pd.Series(pd.to_timedelta([-1, 0, 10])) + not_numeric = pd.DataFrame({'string': ['a', 'b', 'c']}) + assert_frame_equal(-numeric, -1 * numeric) + assert_frame_equal(-boolean, ~boolean) + assert_series_equal(-timedelta, pd.to_timedelta(-1 * timedelta)) + with pytest.raises(TypeError): + (+ not_numeric) def test_invert(self): assert_frame_equal(-(self.frame < 0), ~(self.frame < 0)) + def test_pos(self): + numeric = pd.DataFrame({ + 'a': [-1, 0, 1], + 'b': [1, 0, 1], + }) + boolean = pd.DataFrame({ + 'a': [True, False, True], + 'b': [False, False, True] + }) + timedelta = pd.Series(pd.to_timedelta([-1, 0, 10])) + not_numeric = pd.DataFrame({'string': ['a', 'b', 'c']}) + assert_frame_equal(+numeric, +1 * numeric) + assert_frame_equal(+boolean, (+1 * boolean).astype(bool)) + assert_series_equal(+timedelta, pd.to_timedelta(+1 * timedelta)) + with pytest.raises(TypeError): + (+ not_numeric) + def test_arith_flex_frame(self): ops = ['add', 'sub', 'mul', 'div', 'truediv', 'pow', 'floordiv', 'mod'] if not compat.PY3: From bfe628a8cc86932f74982eea6109fdf23efec7f0 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Thu, 18 Jan 2018 06:56:29 -0600 Subject: [PATCH 03/12] DOC: adds whatsnew entry for unary plus operator --- doc/source/whatsnew/v0.23.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 1c6b698605521..bb974149b85dc 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -463,6 +463,7 @@ To restore previous behavior, simply set ``expand`` to ``False``: Other API Changes ^^^^^^^^^^^^^^^^^ +- Unary ``+`` operator now permitted for ``Series`` and ``DataFrame`` (:issue:`16073`) - :func:`Series.astype` and :func:`Index.astype` with an incompatible dtype will now raise a ``TypeError`` rather than a ``ValueError`` (:issue:`18231`) - ``Series`` construction with an ``object`` dtyped tz-aware datetime and ``dtype=object`` specified, will now return an ``object`` dtyped ``Series``, previously this would infer the datetime dtype (:issue:`18231`) - A :class:`Series` of ``dtype=category`` constructed from an empty ``dict`` will now have categories of ``dtype=object`` rather than ``dtype=float64``, consistently with the case in which an empty list is passed (:issue:`18515`) From 6c0d45612d1cc3461ca453d45a3c53929f60d368 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Thu, 18 Jan 2018 07:01:41 -0600 Subject: [PATCH 04/12] CLN: fixes bracket indents --- pandas/tests/frame/test_operators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index ba44ced29cdad..c283bb3027d94 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -275,7 +275,7 @@ def test_neg(self): numeric = pd.DataFrame({ 'a': [-1, 0, 1], 'b': [1, 0, 1], - }) + }) boolean = pd.DataFrame({ 'a': [True, False, True], 'b': [False, False, True] @@ -295,7 +295,7 @@ def test_pos(self): numeric = pd.DataFrame({ 'a': [-1, 0, 1], 'b': [1, 0, 1], - }) + }) boolean = pd.DataFrame({ 'a': [True, False, True], 'b': [False, False, True] From 3e887c2966cc9271684c8cdbc0bb25c06484bc17 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 22 Jan 2018 05:30:57 -0600 Subject: [PATCH 05/12] DOC: language changes to whatsnew --- doc/source/whatsnew/v0.23.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index bb974149b85dc..f70e1198aa999 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -253,6 +253,7 @@ Current Behavior: Other Enhancements ^^^^^^^^^^^^^^^^^^ +- Unary ``+`` now permitted for ``Series`` and ``DataFrame`` as numeric operator (:issue:`16073`) - Better support for :func:`Dataframe.style.to_excel` output with the ``xlsxwriter`` engine. (:issue:`16149`) - :func:`pandas.tseries.frequencies.to_offset` now accepts leading '+' signs e.g. '+1h'. (:issue:`18171`) - :func:`MultiIndex.unique` now supports the ``level=`` argument, to get unique values from a specific index level (:issue:`17896`) @@ -463,7 +464,6 @@ To restore previous behavior, simply set ``expand`` to ``False``: Other API Changes ^^^^^^^^^^^^^^^^^ -- Unary ``+`` operator now permitted for ``Series`` and ``DataFrame`` (:issue:`16073`) - :func:`Series.astype` and :func:`Index.astype` with an incompatible dtype will now raise a ``TypeError`` rather than a ``ValueError`` (:issue:`18231`) - ``Series`` construction with an ``object`` dtyped tz-aware datetime and ``dtype=object`` specified, will now return an ``object`` dtyped ``Series``, previously this would infer the datetime dtype (:issue:`18231`) - A :class:`Series` of ``dtype=category`` constructed from an empty ``dict`` will now have categories of ``dtype=object`` rather than ``dtype=float64``, consistently with the case in which an empty list is passed (:issue:`18515`) From f88d38dd0f9f3d205ce25486187e8e04478ad48a Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 22 Jan 2018 05:33:48 -0600 Subject: [PATCH 06/12] BUG: replaces typechecks with is_bool_dtype --- pandas/core/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index ff893a3afadd1..04d10bed7e467 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1026,8 +1026,8 @@ def _indexed_same(self, other): for a in self._AXIS_ORDERS) def __neg__(self): - values = com._values_from_object(self) - if values.dtype == np.bool_: + values = _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) @@ -1038,7 +1038,7 @@ def __neg__(self): def __pos__(self): values = _values_from_object(self) - if values.dtype == np.bool_: + if is_bool_dtype(values): arr = values elif (is_numeric_dtype(values) or is_timedelta64_dtype(values)): arr = operator.pos(values) From c18a9763e222ee75a6144030d977858432d4894f Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 22 Jan 2018 05:41:27 -0600 Subject: [PATCH 07/12] TST: paramterizes pos and neg tests --- pandas/tests/frame/test_operators.py | 66 +++++++++++++++------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index c283bb3027d94..84a860190b039 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -271,42 +271,48 @@ def test_logical_with_nas(self): expected = Series([True, True]) assert_series_equal(result, expected) - def test_neg(self): - numeric = pd.DataFrame({ - 'a': [-1, 0, 1], - 'b': [1, 0, 1], - }) - boolean = pd.DataFrame({ - 'a': [True, False, True], - 'b': [False, False, True] - }) - timedelta = pd.Series(pd.to_timedelta([-1, 0, 10])) - not_numeric = pd.DataFrame({'string': ['a', 'b', 'c']}) - assert_frame_equal(-numeric, -1 * numeric) - assert_frame_equal(-boolean, ~boolean) - assert_series_equal(-timedelta, pd.to_timedelta(-1 * timedelta)) + @pytest.mark.parametrize('df,expected', [ + (pd.DataFrame({'a': [-1, 1]}), pd.DataFrame({'a': [1, -1],})), + (pd.DataFrame({'a': [False, True]}), pd.DataFrame({'a': [True, False]})), + (pd.DataFrame({'a': pd.Series(pd.to_timedelta([-1, 1]))}), + pd.DataFrame({'a': pd.Series(pd.to_timedelta([1, -1]))})) + ]) + def test_neg_numeric(self, df, 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'])}), + ]) + def test_neg_raises(self, df): with pytest.raises(TypeError): - (+ not_numeric) + (- df) + with pytest.raises(TypeError): + (- df['a']) def test_invert(self): assert_frame_equal(-(self.frame < 0), ~(self.frame < 0)) - def test_pos(self): - numeric = pd.DataFrame({ - 'a': [-1, 0, 1], - 'b': [1, 0, 1], - }) - boolean = pd.DataFrame({ - 'a': [True, False, True], - 'b': [False, False, True] - }) - timedelta = pd.Series(pd.to_timedelta([-1, 0, 10])) - not_numeric = pd.DataFrame({'string': ['a', 'b', 'c']}) - assert_frame_equal(+numeric, +1 * numeric) - assert_frame_equal(+boolean, (+1 * boolean).astype(bool)) - assert_series_equal(+timedelta, pd.to_timedelta(+1 * timedelta)) + @pytest.mark.parametrize('df', [ + pd.DataFrame({'a': [-1, 1]}), + pd.DataFrame({'a': [False, True]}), + pd.DataFrame({'a': pd.Series(pd.to_timedelta([-1, 1]))}), + ]) + def test_pos_numeric(self, df): + # GH 16073 + 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'])}), + ]) + def test_pos_raises(self, df): + with pytest.raises(TypeError): + (+ df) with pytest.raises(TypeError): - (+ not_numeric) + (+ df['a']) def test_arith_flex_frame(self): ops = ['add', 'sub', 'mul', 'div', 'truediv', 'pow', 'floordiv', 'mod'] From a696d6405e287a240dd23ff368af5b392c779e6c Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 22 Jan 2018 19:19:04 -0600 Subject: [PATCH 08/12] CLN: pep8 fixes --- pandas/tests/frame/test_operators.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 84a860190b039..5df50f3d7835b 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -272,11 +272,12 @@ def test_logical_with_nas(self): assert_series_equal(result, expected) @pytest.mark.parametrize('df,expected', [ - (pd.DataFrame({'a': [-1, 1]}), pd.DataFrame({'a': [1, -1],})), - (pd.DataFrame({'a': [False, True]}), pd.DataFrame({'a': [True, False]})), + (pd.DataFrame({'a': [-1, 1]}), pd.DataFrame({'a': [1, -1]})), + (pd.DataFrame({'a': [False, True]}), + pd.DataFrame({'a': [True, False]})), (pd.DataFrame({'a': pd.Series(pd.to_timedelta([-1, 1]))}), pd.DataFrame({'a': pd.Series(pd.to_timedelta([1, -1]))})) - ]) + ]) def test_neg_numeric(self, df, expected): assert_frame_equal(-df, expected) assert_series_equal(-df['a'], expected['a']) From 122fcd6a295ea76ba0556bcc1a347913e7fa89c5 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 22 Jan 2018 19:27:57 -0600 Subject: [PATCH 09/12] BUG: fixes NameError introduced by rebase --- pandas/core/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 04d10bed7e467..9ac7186c413cd 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1026,7 +1026,7 @@ def _indexed_same(self, other): for a in self._AXIS_ORDERS) def __neg__(self): - values = _values_from_object(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)): @@ -1037,7 +1037,7 @@ def __neg__(self): return self.__array_wrap__(arr) def __pos__(self): - values = _values_from_object(self) + values = com._values_from_object(self) if is_bool_dtype(values): arr = values elif (is_numeric_dtype(values) or is_timedelta64_dtype(values)): From ff8b2506b99343d72aae527222c80999153aa360 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Mon, 29 Jan 2018 05:57:44 -0600 Subject: [PATCH 10/12] TST: removes expected raises for unary pos from eval tests --- pandas/tests/computation/test_eval.py | 60 ++++++++------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py index 9c3572f9ffe72..07ba0b681418e 100644 --- a/pandas/tests/computation/test_eval.py +++ b/pandas/tests/computation/test_eval.py @@ -542,66 +542,42 @@ def test_frame_pos(self): # float lhs = DataFrame(randn(5, 2)) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_frame_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_frame_equal(expect, result) # int lhs = DataFrame(randint(5, size=(5, 2))) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_frame_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_frame_equal(expect, result) # bool doesn't work with numexpr but works elsewhere lhs = DataFrame(rand(5, 2) > 0.5) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_frame_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_frame_equal(expect, result) def test_series_pos(self): expr = self.ex('+') # float lhs = Series(randn(5)) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_series_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_series_equal(expect, result) # int lhs = Series(randint(5, size=5)) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_series_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_series_equal(expect, result) # bool doesn't work with numexpr but works elsewhere lhs = Series(rand(5) > 0.5) - if self.engine == 'python': - with pytest.raises(TypeError): - result = pd.eval(expr, engine=self.engine, parser=self.parser) - else: - expect = lhs - result = pd.eval(expr, engine=self.engine, parser=self.parser) - assert_series_equal(expect, result) + expect = lhs + result = pd.eval(expr, engine=self.engine, parser=self.parser) + assert_series_equal(expect, result) def test_scalar_unary(self): with pytest.raises(TypeError): From 11e150d70b8f259004830f2da1650d0b2812d947 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Wed, 7 Feb 2018 19:17:11 -0600 Subject: [PATCH 11/12] ENH: permits unary pos for period objects --- pandas/core/generic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 9ac7186c413cd..35f866c9e7d58 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -25,6 +25,7 @@ is_list_like, is_dict_like, is_re_compilable, + is_period_arraylike, pandas_dtype) from pandas.core.dtypes.cast import maybe_promote, maybe_upcast_putmask from pandas.core.dtypes.inference import is_hashable @@ -1038,7 +1039,7 @@ def __neg__(self): def __pos__(self): values = com._values_from_object(self) - if is_bool_dtype(values): + 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) From 2b8072e31e8c21086b13dd49afc02ebe86493561 Mon Sep 17 00:00:00 2001 From: Dillon Niederhut Date: Wed, 7 Feb 2018 19:17:48 -0600 Subject: [PATCH 12/12] TST: fixes arithmetic tests for object output of period math --- pandas/tests/frame/test_arithmetic.py | 4 ++-- pandas/tests/series/test_arithmetic.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 1bb8e8edffc6e..a3a799aed1c55 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -245,7 +245,7 @@ def test_ops_frame_period(self): exp = pd.DataFrame({'A': np.array([2, 1], dtype=object), 'B': np.array([14, 13], dtype=object)}) tm.assert_frame_equal(p - df, exp) - tm.assert_frame_equal(df - p, -exp) + tm.assert_frame_equal(df - p, -1 * exp) df2 = pd.DataFrame({'A': [pd.Period('2015-05', freq='M'), pd.Period('2015-06', freq='M')], @@ -257,4 +257,4 @@ def test_ops_frame_period(self): exp = pd.DataFrame({'A': np.array([4, 4], dtype=object), 'B': np.array([16, 16], dtype=object)}) tm.assert_frame_equal(df2 - df, exp) - tm.assert_frame_equal(df - df2, -exp) + tm.assert_frame_equal(df - df2, -1 * exp) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index 1d9fa9dc15531..94da97ef45301 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -315,7 +315,7 @@ def test_ops_series_period(self): # dtype will be object because of original dtype expected = pd.Series([9, 8], name='xxx', dtype=object) tm.assert_series_equal(per - ser, expected) - tm.assert_series_equal(ser - per, -expected) + tm.assert_series_equal(ser - per, -1 * expected) s2 = pd.Series([pd.Period('2015-01-05', freq='D'), pd.Period('2015-01-04', freq='D')], name='xxx') @@ -323,7 +323,7 @@ def test_ops_series_period(self): expected = pd.Series([4, 2], name='xxx', dtype=object) tm.assert_series_equal(s2 - ser, expected) - tm.assert_series_equal(ser - s2, -expected) + tm.assert_series_equal(ser - s2, -1 * expected) class TestTimestampSeriesArithmetic(object):