Skip to content

Bug: adds support for unary plus #19297

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 12 commits into from
Feb 8, 2018
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
19 changes: 17 additions & 2 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1027,10 +1028,24 @@ def _indexed_same(self, other):

def __neg__(self):
values = com._values_from_object(self)
if values.dtype == np.bool_:
if is_bool_dtype(values):
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 = 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))
return self.__array_wrap__(arr)

def __invert__(self):
Expand Down
60 changes: 18 additions & 42 deletions pandas/tests/computation/test_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/frame/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')],
Expand All @@ -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)
43 changes: 40 additions & 3 deletions pandas/tests/frame/test_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,50 @@ def test_logical_with_nas(self):
expected = Series([True, True])
assert_series_equal(result, expected)

def test_neg(self):
# what to do?
assert_frame_equal(-self.frame, -1 * self.frame)
@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)
Copy link
Contributor

Choose a reason for hiding this comment

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

@jbrockmendel I think on your list is to rationalize locations of arithmetic tests between test_arithmetic and test_operations

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):
(- df)
with pytest.raises(TypeError):
(- df['a'])

def test_invert(self):
assert_frame_equal(-(self.frame < 0), ~(self.frame < 0))

@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):
(+ df['a'])

def test_arith_flex_frame(self):
ops = ['add', 'sub', 'mul', 'div', 'truediv', 'pow', 'floordiv', 'mod']
if not compat.PY3:
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/series/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,15 @@ 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')
assert s2.dtype == object

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):
Expand Down