Skip to content

Commit ff9fc90

Browse files
gfyounggfyoung
gfyoung
authored andcommitted
BUG: Make round signature compatible with numpy's
Closes gh-12600.
1 parent 0f3a7b8 commit ff9fc90

File tree

8 files changed

+228
-22
lines changed

8 files changed

+228
-22
lines changed

doc/source/whatsnew/v0.18.1.txt

+14
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Performance Improvements
9090
Bug Fixes
9191
~~~~~~~~~
9292

93+
- ``round`` now accepts an ``out`` argument to maintain compatibility with numpy's ``round`` function (:issue:`12600`)
9394
- Bug in ``Period`` and ``PeriodIndex`` creation raises ``KeyError`` if ``freq="Minute"`` is specified. Note that "Minute" freq is deprecated in v0.17.0, and recommended to use ``freq="T"`` instead (:issue:`11854`)
9495
- Bug in printing data which contains ``Period`` with different ``freq`` raises ``ValueError`` (:issue:`12615`)
9596
- Bug in ``Series`` construction with ``Categorical`` and ``dtype='category'`` is specified (:issue:`12574`)
@@ -114,6 +115,18 @@ Bug Fixes
114115

115116

116117

118+
119+
120+
121+
122+
123+
124+
125+
126+
127+
128+
129+
117130

118131

119132

@@ -130,6 +143,7 @@ Bug Fixes
130143

131144

132145

146+
133147
- Bug in ``concat`` raises ``AttributeError`` when input data contains tz-aware datetime and timedelta (:issue:`12620`)
134148

135149

pandas/core/frame.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from pandas import compat
4747
from pandas.util.decorators import (deprecate, Appender, Substitution,
4848
deprecate_kwarg)
49+
from pandas.util.validators import validate_args
4950

5051
from pandas.tseries.period import PeriodIndex
5152
from pandas.tseries.index import DatetimeIndex
@@ -4420,7 +4421,7 @@ def merge(self, right, how='inner', on=None, left_on=None, right_on=None,
44204421
right_index=right_index, sort=sort, suffixes=suffixes,
44214422
copy=copy, indicator=indicator)
44224423

4423-
def round(self, decimals=0, out=None):
4424+
def round(self, decimals=0, *args):
44244425
"""
44254426
Round a DataFrame to a variable number of decimal places.
44264427
@@ -4471,6 +4472,8 @@ def round(self, decimals=0, out=None):
44714472
See Also
44724473
--------
44734474
numpy.around
4475+
Series.round
4476+
44744477
"""
44754478
from pandas.tools.merge import concat
44764479

@@ -4486,6 +4489,9 @@ def _series_round(s, decimals):
44864489
return s.round(decimals)
44874490
return s
44884491

4492+
validate_args(args, min_length=0, max_length=1,
4493+
msg="Inplace rounding is not supported")
4494+
44894495
if isinstance(decimals, (dict, Series)):
44904496
if isinstance(decimals, Series):
44914497
if not decimals.index.is_unique:

pandas/core/generic.py

+5-20
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
AbstractMethodError)
2929
import pandas.core.nanops as nanops
3030
from pandas.util.decorators import Appender, Substitution, deprecate_kwarg
31+
from pandas.util.validators import validate_kwargs
3132
from pandas.core import config
3233

3334
# goal is to be able to define the docs close to function, while still being
@@ -5231,29 +5232,13 @@ def _doc_parms(cls):
52315232
%(outname)s : %(name1)s\n"""
52325233

52335234

5234-
def _validate_kwargs(fname, kwargs, *compat_args):
5235-
"""
5236-
Checks whether parameters passed to the
5237-
**kwargs argument in a 'stat' function 'fname'
5238-
are valid parameters as specified in *compat_args
5239-
5240-
"""
5241-
list(map(kwargs.__delitem__, filter(
5242-
kwargs.__contains__, compat_args)))
5243-
if kwargs:
5244-
bad_arg = list(kwargs)[0] # first 'key' element
5245-
raise TypeError(("{fname}() got an unexpected "
5246-
"keyword argument '{arg}'".
5247-
format(fname=fname, arg=bad_arg)))
5248-
5249-
52505235
def _make_stat_function(name, name1, name2, axis_descr, desc, f):
52515236
@Substitution(outname=name, desc=desc, name1=name1, name2=name2,
52525237
axis_descr=axis_descr)
52535238
@Appender(_num_doc)
52545239
def stat_func(self, axis=None, skipna=None, level=None, numeric_only=None,
52555240
**kwargs):
5256-
_validate_kwargs(name, kwargs, 'out', 'dtype')
5241+
validate_kwargs(name, kwargs, 'out', 'dtype')
52575242
if skipna is None:
52585243
skipna = True
52595244
if axis is None:
@@ -5274,7 +5259,7 @@ def _make_stat_function_ddof(name, name1, name2, axis_descr, desc, f):
52745259
@Appender(_num_ddof_doc)
52755260
def stat_func(self, axis=None, skipna=None, level=None, ddof=1,
52765261
numeric_only=None, **kwargs):
5277-
_validate_kwargs(name, kwargs, 'out', 'dtype')
5262+
validate_kwargs(name, kwargs, 'out', 'dtype')
52785263
if skipna is None:
52795264
skipna = True
52805265
if axis is None:
@@ -5296,7 +5281,7 @@ def _make_cum_function(name, name1, name2, axis_descr, desc, accum_func,
52965281
@Appender("Return cumulative {0} over requested axis.".format(name) +
52975282
_cnum_doc)
52985283
def func(self, axis=None, dtype=None, out=None, skipna=True, **kwargs):
5299-
_validate_kwargs(name, kwargs, 'out', 'dtype')
5284+
validate_kwargs(name, kwargs, 'out', 'dtype')
53005285
if axis is None:
53015286
axis = self._stat_axis_number
53025287
else:
@@ -5331,7 +5316,7 @@ def _make_logical_function(name, name1, name2, axis_descr, desc, f):
53315316
@Appender(_bool_doc)
53325317
def logical_func(self, axis=None, bool_only=None, skipna=None, level=None,
53335318
**kwargs):
5334-
_validate_kwargs(name, kwargs, 'out', 'dtype')
5319+
validate_kwargs(name, kwargs, 'out', 'dtype')
53355320
if skipna is None:
53365321
skipna = True
53375322
if axis is None:

pandas/core/series.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from pandas.tseries.period import PeriodIndex, Period
4040
from pandas import compat
4141
from pandas.util.terminal import get_terminal_size
42+
from pandas.util.validators import validate_args
4243
from pandas.compat import zip, u, OrderedDict, StringIO
4344

4445

@@ -1256,7 +1257,7 @@ def idxmax(self, axis=None, out=None, skipna=True):
12561257
argmin = idxmin
12571258
argmax = idxmax
12581259

1259-
def round(self, decimals=0):
1260+
def round(self, decimals=0, *args):
12601261
"""
12611262
Round each value in a Series to the given number of decimals.
12621263
@@ -1274,8 +1275,12 @@ def round(self, decimals=0):
12741275
See Also
12751276
--------
12761277
numpy.around
1278+
DataFrame.round
12771279
12781280
"""
1281+
validate_args(args, min_length=0, max_length=1,
1282+
msg="Inplace rounding is not supported")
1283+
12791284
result = _values_from_object(self).round(decimals)
12801285
result = self._constructor(result, index=self.index).__finalize__(self)
12811286

pandas/tests/frame/test_analytics.py

+11
Original file line numberDiff line numberDiff line change
@@ -2060,6 +2060,17 @@ def test_round(self):
20602060
assert_series_equal(df.round(decimals)['col1'],
20612061
expected_rounded['col1'])
20622062

2063+
def test_numpy_round(self):
2064+
# See gh-12600
2065+
df = DataFrame([[1.53, 1.36], [0.06, 7.01]])
2066+
out = np.round(df, decimals=0)
2067+
expected = DataFrame([[2., 1.], [0., 7.]])
2068+
assert_frame_equal(out, expected)
2069+
2070+
msg = "Inplace rounding is not supported"
2071+
with tm.assertRaisesRegexp(ValueError, msg):
2072+
np.round(df, decimals=0, out=df)
2073+
20632074
def test_round_mixed_type(self):
20642075
# GH11885
20652076
df = DataFrame({'col1': [1.1, 2.2, 3.3, 4.4],

pandas/tests/series/test_analytics.py

+11
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,17 @@ def test_round(self):
511511
assert_series_equal(result, expected)
512512
self.assertEqual(result.name, self.ts.name)
513513

514+
def test_numpy_round(self):
515+
# See gh-12600
516+
s = Series([1.53, 1.36, 0.06])
517+
out = np.round(s, decimals=0)
518+
expected = Series([2., 1., 0.])
519+
assert_series_equal(out, expected)
520+
521+
msg = "Inplace rounding is not supported"
522+
with tm.assertRaisesRegexp(ValueError, msg):
523+
np.round(s, decimals=0, out=s)
524+
514525
def test_built_in_round(self):
515526
if not compat.PY3:
516527
raise nose.SkipTest(

pandas/tests/test_util.py

+77
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import nose
33

44
from pandas.util.decorators import deprecate_kwarg
5+
from pandas.util.validators import validate_args, validate_kwargs
6+
57
import pandas.util.testing as tm
68

79

@@ -73,6 +75,81 @@ def test_rands_array():
7375
assert(arr.shape == (10, 10))
7476
assert(len(arr[1, 1]) == 7)
7577

78+
79+
class TestValidateArgs(tm.TestCase):
80+
81+
def test_bad_min_length(self):
82+
msg = "'min_length' must be non-negative"
83+
with tm.assertRaisesRegexp(ValueError, msg):
84+
validate_args((None,), min_length=-1, max_length=5)
85+
86+
def test_bad_arg_length_no_max(self):
87+
min_length = 5
88+
msg = "expected at least {min_length} arguments".format(
89+
min_length=min_length)
90+
91+
with tm.assertRaisesRegexp(ValueError, msg):
92+
validate_args((None,), min_length=min_length, max_length=None)
93+
94+
def test_bad_arg_length_with_max(self):
95+
min_length = 5
96+
max_length = 10
97+
msg = ("expected between {min_length} and {max_length}"
98+
" arguments inclusive".format(min_length=min_length,
99+
max_length=max_length))
100+
101+
with tm.assertRaisesRegexp(ValueError, msg):
102+
validate_args((None,), min_length=min_length,
103+
max_length=max_length)
104+
105+
def test_bad_min_max_length(self):
106+
msg = "'min_length' > 'max_length'"
107+
with tm.assertRaisesRegexp(ValueError, msg):
108+
validate_args((None,), min_length=5, max_length=2)
109+
110+
def test_not_all_none(self):
111+
msg = "All arguments must be None"
112+
with tm.assertRaisesRegexp(ValueError, msg):
113+
validate_args(('foo',), min_length=0,
114+
max_length=1, msg=msg)
115+
116+
with tm.assertRaisesRegexp(ValueError, msg):
117+
validate_args(('foo', 'bar', 'baz'), min_length=2,
118+
max_length=5, msg=msg)
119+
120+
with tm.assertRaisesRegexp(ValueError, msg):
121+
validate_args((None, 'bar', None), min_length=2,
122+
max_length=5, msg=msg)
123+
124+
def test_validation(self):
125+
# No exceptions should be thrown
126+
validate_args((None,), min_length=0, max_length=1)
127+
validate_args((None, None), min_length=1, max_length=5)
128+
129+
130+
class TestValidateKwargs(tm.TestCase):
131+
132+
def test_bad_kwarg(self):
133+
goodarg = 'f'
134+
badarg = goodarg + 'o'
135+
136+
kwargs = {goodarg: 'foo', badarg: 'bar'}
137+
compat_args = (goodarg, badarg + 'o')
138+
fname = 'func'
139+
140+
msg = ("{fname}\(\) got an unexpected "
141+
"keyword argument '{arg}'".format(
142+
fname=fname, arg=badarg))
143+
144+
with tm.assertRaisesRegexp(TypeError, msg):
145+
validate_kwargs(fname, kwargs, *compat_args)
146+
147+
def test_validation(self):
148+
# No exceptions should be thrown
149+
compat_args = ('f', 'b', 'ba')
150+
kwargs = {'f': 'foo', 'b': 'bar'}
151+
validate_kwargs('func', kwargs, *compat_args)
152+
76153
if __name__ == '__main__':
77154
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
78155
exit=False)

pandas/util/validators.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Module that contains many useful utilities
3+
for validating data or function arguments
4+
"""
5+
6+
7+
def validate_args(args, min_length=0, max_length=None, msg=""):
8+
"""
9+
Checks whether the length of the `*args` argument passed into a function
10+
has at least `min_length` arguments. If `max_length` is an integer, checks
11+
whether `*args` has at most `max_length` arguments inclusive. Raises a
12+
ValueError if any of the aforementioned conditions are False.
13+
14+
Parameters
15+
----------
16+
args: tuple
17+
The `*args` parameter passed into a function
18+
19+
min_length: int, optional
20+
The minimum number of arguments that should be contained in the `args`.
21+
tuple. This number must be non-negative. The default is '0'.
22+
23+
max_length: int, optional
24+
If not `None`, the maximum number of arguments that should be contained
25+
in the `args` parameter. This number must be at least as large as the
26+
provided `min_length` value. The default is None.
27+
28+
msg: str, optional
29+
Error message to display when a custom check of args fails. For
30+
example, pandas does not support a non-None argument for `out`
31+
when rounding a `Series` or `DataFrame` object. `msg` in this
32+
case can be "Inplace rounding is not supported".
33+
34+
Raises
35+
------
36+
ValueError if `args` fails to have a length that is at least `min_length`
37+
and at most `max_length` inclusive (provided `max_length` is not None)
38+
39+
"""
40+
length = len(args)
41+
42+
if min_length < 0:
43+
raise ValueError("'min_length' must be non-negative")
44+
45+
if max_length is None:
46+
if length < min_length:
47+
raise ValueError(("expected at least {min_length} arguments "
48+
"but got {length} arguments instead".
49+
format(min_length=min_length, length=length)))
50+
51+
if min_length > max_length:
52+
raise ValueError("'min_length' > 'max_length'")
53+
54+
if (length < min_length) or (length > max_length):
55+
raise ValueError(("expected between {min_length} and {max_length} "
56+
"arguments inclusive but got {length} arguments "
57+
"instead".format(min_length=min_length,
58+
length=length,
59+
max_length=max_length)))
60+
61+
# See gh-12600; this is to allow compatibility with NumPy,
62+
# which passes in an 'out' parameter as a positional argument
63+
if args:
64+
args = list(filter(lambda elt: elt is not None, args))
65+
66+
if args:
67+
raise ValueError(msg)
68+
69+
70+
def validate_kwargs(fname, kwargs, *compat_args):
71+
"""
72+
Checks whether parameters passed to the **kwargs argument in a
73+
function 'fname' are valid parameters as specified in *compat_args
74+
75+
Parameters
76+
----------
77+
fname: str
78+
The name of the function being passed the `**kwargs` parameter
79+
80+
kwargs: dict
81+
The `**kwargs` parameter passed into `fname`
82+
83+
compat_args: *args
84+
A tuple of keys that `kwargs` is allowed to have
85+
86+
Raises
87+
------
88+
ValueError if `kwargs` contains keys not in `compat_args`
89+
90+
"""
91+
list(map(kwargs.__delitem__, filter(
92+
kwargs.__contains__, compat_args)))
93+
if kwargs:
94+
bad_arg = list(kwargs)[0] # first 'key' element
95+
raise TypeError(("{fname}() got an unexpected "
96+
"keyword argument '{arg}'".
97+
format(fname=fname, arg=bad_arg)))

0 commit comments

Comments
 (0)