From eb4762cc209df5202d833f559e6af6be5d26a9c4 Mon Sep 17 00:00:00 2001 From: gfyoung Date: Sat, 7 May 2016 18:17:53 +0100 Subject: [PATCH] COMPAT: Expand compatibility with fromnumeric.py Expands compatibility with fromnumeric.py in tslib.pyx and puts checks in window.py, groupby.py, and resample.py to ensure that pandas functions such as 'mean' are not called via the numpy library. Closes gh-12811. --- doc/source/whatsnew/v0.18.2.txt | 3 +- pandas/compat/numpy/function.py | 76 +++++++++++++++++++++- pandas/core/common.py | 4 ++ pandas/core/generic.py | 8 +-- pandas/core/groupby.py | 19 ++++-- pandas/core/window.py | 86 ++++++++++++++++--------- pandas/tests/test_groupby.py | 14 ++++ pandas/tests/test_window.py | 93 +++++++++++++++++++++++++++ pandas/tseries/resample.py | 11 +++- pandas/tseries/tests/test_resample.py | 19 +++++- pandas/tseries/tests/test_tslib.py | 28 ++++++++ pandas/tslib.pyx | 4 +- 12 files changed, 316 insertions(+), 49 deletions(-) diff --git a/doc/source/whatsnew/v0.18.2.txt b/doc/source/whatsnew/v0.18.2.txt index 61461be87801e..abf6ee2953f8b 100644 --- a/doc/source/whatsnew/v0.18.2.txt +++ b/doc/source/whatsnew/v0.18.2.txt @@ -46,7 +46,8 @@ API changes - Non-convertible dates in an excel date column will be returned without conversion and the column will be ``object`` dtype, rather than raising an exception (:issue:`10001`) - +- Compatibility with NumPy array methods has been expanded to timestamps (:issue: `12811`) +- An ``UnsupportedFunctionCall`` error is now raised if groupby or resample functions like ``mean`` are called via NumPy (:issue: `12811`) .. _whatsnew_0182.api.tolist: diff --git a/pandas/compat/numpy/function.py b/pandas/compat/numpy/function.py index 069cb3638fe75..274761f5d0b9c 100644 --- a/pandas/compat/numpy/function.py +++ b/pandas/compat/numpy/function.py @@ -21,7 +21,7 @@ from numpy import ndarray from pandas.util.validators import (validate_args, validate_kwargs, validate_args_and_kwargs) -from pandas.core.common import is_integer +from pandas.core.common import is_integer, UnsupportedFunctionCall from pandas.compat import OrderedDict @@ -245,3 +245,77 @@ def validate_transpose_for_generic(inst, kwargs): msg += " for {klass} instances".format(klass=klass) raise ValueError(msg) + + +def validate_window_func(name, args, kwargs): + numpy_args = ('axis', 'dtype', 'out') + msg = ("numpy operations are not " + "valid with window objects. " + "Use .{func}() directly instead ".format(func=name)) + + if len(args) > 0: + raise UnsupportedFunctionCall(msg) + + for arg in numpy_args: + if arg in kwargs: + raise UnsupportedFunctionCall(msg) + + +def validate_rolling_func(name, args, kwargs): + numpy_args = ('axis', 'dtype', 'out') + msg = ("numpy operations are not " + "valid with window objects. " + "Use .rolling(...).{func}() instead ".format(func=name)) + + if len(args) > 0: + raise UnsupportedFunctionCall(msg) + + for arg in numpy_args: + if arg in kwargs: + raise UnsupportedFunctionCall(msg) + + +def validate_expanding_func(name, args, kwargs): + numpy_args = ('axis', 'dtype', 'out') + msg = ("numpy operations are not " + "valid with window objects. " + "Use .expanding(...).{func}() instead ".format(func=name)) + + if len(args) > 0: + raise UnsupportedFunctionCall(msg) + + for arg in numpy_args: + if arg in kwargs: + raise UnsupportedFunctionCall(msg) + + +def validate_groupby_func(name, args, kwargs): + """ + 'args' and 'kwargs' should be empty because all of + their necessary parameters are explicitly listed in + the function signature + """ + if len(args) + len(kwargs) > 0: + raise UnsupportedFunctionCall(( + "numpy operations are not valid " + "with groupby. Use .groupby(...)." + "{func}() instead".format(func=name))) + +RESAMPLER_NUMPY_OPS = ('min', 'max', 'sum', 'prod', + 'mean', 'std', 'var') + + +def validate_resampler_func(method, args, kwargs): + """ + 'args' and 'kwargs' should be empty because all of + their necessary parameters are explicitly listed in + the function signature + """ + if len(args) + len(kwargs) > 0: + if method in RESAMPLER_NUMPY_OPS: + raise UnsupportedFunctionCall(( + "numpy operations are not valid " + "with resample. Use .resample(...)." + "{func}() instead".format(func=method))) + else: + raise TypeError("too many arguments passed in") diff --git a/pandas/core/common.py b/pandas/core/common.py index c64cfa77b9e62..64bfbdde0c5c3 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -41,6 +41,10 @@ class AmbiguousIndexError(PandasError, KeyError): pass +class UnsupportedFunctionCall(ValueError): + pass + + class AbstractMethodError(NotImplementedError): """Raise this error instead of NotImplementedError for abstract methods while keeping compatibility with Python 2 and Python 3. diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 6c80ab9d87e33..99599d2b04a45 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -5299,7 +5299,7 @@ def _make_stat_function(cls, name, name1, name2, axis_descr, desc, f): @Appender(_num_doc) def stat_func(self, axis=None, skipna=None, level=None, numeric_only=None, **kwargs): - nv.validate_stat_func(tuple(), kwargs) + nv.validate_stat_func(tuple(), kwargs, fname=name) if skipna is None: skipna = True if axis is None: @@ -5319,7 +5319,7 @@ def _make_stat_function_ddof(cls, name, name1, name2, axis_descr, desc, f): @Appender(_num_ddof_doc) def stat_func(self, axis=None, skipna=None, level=None, ddof=1, numeric_only=None, **kwargs): - nv.validate_stat_ddof_func(tuple(), kwargs) + nv.validate_stat_ddof_func(tuple(), kwargs, fname=name) if skipna is None: skipna = True if axis is None: @@ -5340,7 +5340,7 @@ def _make_cum_function(cls, name, name1, name2, axis_descr, desc, accum_func, @Appender("Return cumulative {0} over requested axis.".format(name) + _cnum_doc) def cum_func(self, axis=None, dtype=None, out=None, skipna=True, **kwargs): - nv.validate_cum_func(tuple(), kwargs) + nv.validate_cum_func(tuple(), kwargs, fname=name) if axis is None: axis = self._stat_axis_number else: @@ -5374,7 +5374,7 @@ def _make_logical_function(cls, name, name1, name2, axis_descr, desc, f): @Appender(_bool_doc) def logical_func(self, axis=None, bool_only=None, skipna=None, level=None, **kwargs): - nv.validate_logical_func(tuple(), kwargs) + nv.validate_logical_func(tuple(), kwargs, fname=name) if skipna is None: skipna = True if axis is None: diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py index 424859da82877..2346be5c854f5 100644 --- a/pandas/core/groupby.py +++ b/pandas/core/groupby.py @@ -11,6 +11,7 @@ callable, map ) from pandas import compat +from pandas.compat.numpy import function as nv from pandas.compat.numpy import _np_version_under1p8 from pandas.core.base import (PandasObject, SelectionMixin, GroupByError, DataError, SpecificationError) @@ -954,12 +955,13 @@ def count(self): @Substitution(name='groupby') @Appender(_doc_template) - def mean(self): + def mean(self, *args, **kwargs): """ Compute mean of groups, excluding missing values For multiple groupings, the result index will be a MultiIndex """ + nv.validate_groupby_func('mean', args, kwargs) try: return self._cython_agg_general('mean') except GroupByError: @@ -993,7 +995,7 @@ def f(x): @Substitution(name='groupby') @Appender(_doc_template) - def std(self, ddof=1): + def std(self, ddof=1, *args, **kwargs): """ Compute standard deviation of groups, excluding missing values @@ -1005,12 +1007,13 @@ def std(self, ddof=1): degrees of freedom """ - # todo, implement at cython level? + # TODO: implement at Cython level? + nv.validate_groupby_func('std', args, kwargs) return np.sqrt(self.var(ddof=ddof)) @Substitution(name='groupby') @Appender(_doc_template) - def var(self, ddof=1): + def var(self, ddof=1, *args, **kwargs): """ Compute variance of groups, excluding missing values @@ -1021,7 +1024,7 @@ def var(self, ddof=1): ddof : integer, default 1 degrees of freedom """ - + nv.validate_groupby_func('var', args, kwargs) if ddof == 1: return self._cython_agg_general('var') else: @@ -1317,8 +1320,9 @@ def cumcount(self, ascending=True): @Substitution(name='groupby') @Appender(_doc_template) - def cumprod(self, axis=0): + def cumprod(self, axis=0, *args, **kwargs): """Cumulative product for each group""" + nv.validate_groupby_func('cumprod', args, kwargs) if axis != 0: return self.apply(lambda x: x.cumprod(axis=axis)) @@ -1326,8 +1330,9 @@ def cumprod(self, axis=0): @Substitution(name='groupby') @Appender(_doc_template) - def cumsum(self, axis=0): + def cumsum(self, axis=0, *args, **kwargs): """Cumulative sum for each group""" + nv.validate_groupby_func('cumsum', args, kwargs) if axis != 0: return self.apply(lambda x: x.cumprod(axis=axis)) diff --git a/pandas/core/window.py b/pandas/core/window.py index eb0d996436661..cd66d4e30c351 100644 --- a/pandas/core/window.py +++ b/pandas/core/window.py @@ -18,6 +18,7 @@ import pandas.core.common as com import pandas.algos as algos from pandas import compat +from pandas.compat.numpy import function as nv from pandas.util.decorators import Substitution, Appender from textwrap import dedent @@ -435,13 +436,15 @@ def aggregate(self, arg, *args, **kwargs): @Substitution(name='window') @Appender(_doc_template) @Appender(_shared_docs['sum']) - def sum(self, **kwargs): + def sum(self, *args, **kwargs): + nv.validate_window_func('sum', args, kwargs) return self._apply_window(mean=False, **kwargs) @Substitution(name='window') @Appender(_doc_template) @Appender(_shared_docs['mean']) - def mean(self, **kwargs): + def mean(self, *args, **kwargs): + nv.validate_window_func('mean', args, kwargs) return self._apply_window(mean=True, **kwargs) @@ -620,7 +623,8 @@ def f(arg, window, min_periods): return self._apply(f, func, args=args, kwargs=kwargs, center=False) - def sum(self, **kwargs): + def sum(self, *args, **kwargs): + nv.validate_window_func('sum', args, kwargs) return self._apply('roll_sum', 'sum', **kwargs) _shared_docs['max'] = dedent(""" @@ -631,7 +635,8 @@ def sum(self, **kwargs): how : string, default 'max' (DEPRECATED) Method for down- or re-sampling""") - def max(self, how=None, **kwargs): + def max(self, how=None, *args, **kwargs): + nv.validate_window_func('max', args, kwargs) if self.freq is not None and how is None: how = 'max' return self._apply('roll_max', 'max', how=how, **kwargs) @@ -644,12 +649,14 @@ def max(self, how=None, **kwargs): how : string, default 'min' (DEPRECATED) Method for down- or re-sampling""") - def min(self, how=None, **kwargs): + def min(self, how=None, *args, **kwargs): + nv.validate_window_func('min', args, kwargs) if self.freq is not None and how is None: how = 'min' return self._apply('roll_min', 'min', how=how, **kwargs) - def mean(self, **kwargs): + def mean(self, *args, **kwargs): + nv.validate_window_func('mean', args, kwargs) return self._apply('roll_mean', 'mean', **kwargs) _shared_docs['median'] = dedent(""" @@ -674,7 +681,8 @@ def median(self, how=None, **kwargs): Delta Degrees of Freedom. The divisor used in calculations is ``N - ddof``, where ``N`` represents the number of elements.""") - def std(self, ddof=1, **kwargs): + def std(self, ddof=1, *args, **kwargs): + nv.validate_window_func('std', args, kwargs) window = self._get_window() def f(arg, *args, **kwargs): @@ -693,7 +701,8 @@ def f(arg, *args, **kwargs): Delta Degrees of Freedom. The divisor used in calculations is ``N - ddof``, where ``N`` represents the number of elements.""") - def var(self, ddof=1, **kwargs): + def var(self, ddof=1, *args, **kwargs): + nv.validate_window_func('var', args, kwargs) return self._apply('roll_var', 'var', check_minp=_require_min_periods(1), ddof=ddof, **kwargs) @@ -865,26 +874,30 @@ def apply(self, func, args=(), kwargs={}): @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['sum']) - def sum(self, **kwargs): - return super(Rolling, self).sum(**kwargs) + def sum(self, *args, **kwargs): + nv.validate_rolling_func('sum', args, kwargs) + return super(Rolling, self).sum(*args, **kwargs) @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['max']) - def max(self, **kwargs): - return super(Rolling, self).max(**kwargs) + def max(self, *args, **kwargs): + nv.validate_rolling_func('max', args, kwargs) + return super(Rolling, self).max(*args, **kwargs) @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['min']) - def min(self, **kwargs): - return super(Rolling, self).min(**kwargs) + def min(self, *args, **kwargs): + nv.validate_rolling_func('min', args, kwargs) + return super(Rolling, self).min(*args, **kwargs) @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['mean']) - def mean(self, **kwargs): - return super(Rolling, self).mean(**kwargs) + def mean(self, *args, **kwargs): + nv.validate_rolling_func('mean', args, kwargs) + return super(Rolling, self).mean(*args, **kwargs) @Substitution(name='rolling') @Appender(_doc_template) @@ -895,13 +908,15 @@ def median(self, **kwargs): @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['std']) - def std(self, ddof=1, **kwargs): + def std(self, ddof=1, *args, **kwargs): + nv.validate_rolling_func('std', args, kwargs) return super(Rolling, self).std(ddof=ddof, **kwargs) @Substitution(name='rolling') @Appender(_doc_template) @Appender(_shared_docs['var']) - def var(self, ddof=1, **kwargs): + def var(self, ddof=1, *args, **kwargs): + nv.validate_rolling_func('var', args, kwargs) return super(Rolling, self).var(ddof=ddof, **kwargs) @Substitution(name='rolling') @@ -1023,26 +1038,30 @@ def apply(self, func, args=(), kwargs={}): @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['sum']) - def sum(self, **kwargs): - return super(Expanding, self).sum(**kwargs) + def sum(self, *args, **kwargs): + nv.validate_expanding_func('sum', args, kwargs) + return super(Expanding, self).sum(*args, **kwargs) @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['max']) - def max(self, **kwargs): - return super(Expanding, self).max(**kwargs) + def max(self, *args, **kwargs): + nv.validate_expanding_func('max', args, kwargs) + return super(Expanding, self).max(*args, **kwargs) @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['min']) - def min(self, **kwargs): - return super(Expanding, self).min(**kwargs) + def min(self, *args, **kwargs): + nv.validate_expanding_func('min', args, kwargs) + return super(Expanding, self).min(*args, **kwargs) @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['mean']) - def mean(self, **kwargs): - return super(Expanding, self).mean(**kwargs) + def mean(self, *args, **kwargs): + nv.validate_expanding_func('mean', args, kwargs) + return super(Expanding, self).mean(*args, **kwargs) @Substitution(name='expanding') @Appender(_doc_template) @@ -1053,13 +1072,15 @@ def median(self, **kwargs): @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['std']) - def std(self, ddof=1, **kwargs): + def std(self, ddof=1, *args, **kwargs): + nv.validate_expanding_func('std', args, kwargs) return super(Expanding, self).std(ddof=ddof, **kwargs) @Substitution(name='expanding') @Appender(_doc_template) @Appender(_shared_docs['var']) - def var(self, ddof=1, **kwargs): + def var(self, ddof=1, *args, **kwargs): + nv.validate_expanding_func('var', args, kwargs) return super(Expanding, self).var(ddof=ddof, **kwargs) @Substitution(name='expanding') @@ -1273,15 +1294,17 @@ def func(arg): @Substitution(name='ewm') @Appender(_doc_template) - def mean(self, **kwargs): + def mean(self, *args, **kwargs): """exponential weighted moving average""" + nv.validate_window_func('mean', args, kwargs) return self._apply('ewma', **kwargs) @Substitution(name='ewm') @Appender(_doc_template) @Appender(_bias_template) - def std(self, bias=False, **kwargs): + def std(self, bias=False, *args, **kwargs): """exponential weighted moving stddev""" + nv.validate_window_func('std', args, kwargs) return _zsqrt(self.var(bias=bias, **kwargs)) vol = std @@ -1289,8 +1312,9 @@ def std(self, bias=False, **kwargs): @Substitution(name='ewm') @Appender(_doc_template) @Appender(_bias_template) - def var(self, bias=False, **kwargs): + def var(self, bias=False, *args, **kwargs): """exponential weighted moving variance""" + nv.validate_window_func('var', args, kwargs) def f(arg): return algos.ewmcov(arg, arg, self.com, int(self.adjust), diff --git a/pandas/tests/test_groupby.py b/pandas/tests/test_groupby.py index 571b0fa1ee78f..74048536bd1f3 100644 --- a/pandas/tests/test_groupby.py +++ b/pandas/tests/test_groupby.py @@ -8,6 +8,7 @@ from pandas import date_range, bdate_range, Timestamp from pandas.core.index import Index, MultiIndex, CategoricalIndex from pandas.core.api import Categorical, DataFrame +from pandas.core.common import UnsupportedFunctionCall from pandas.core.groupby import (SpecificationError, DataError, _nargsort, _lexsort_indexer) from pandas.core.series import Series @@ -6393,6 +6394,19 @@ def test_transform_with_non_scalar_group(self): (axis=1, level=1).transform, lambda z: z.div(z.sum(axis=1), axis=0)) + def test_numpy_compat(self): + # see gh-12811 + df = pd.DataFrame({'A': [1, 2, 1], 'B': [1, 2, 3]}) + g = df.groupby('A') + + msg = "numpy operations are not valid with groupby" + + for func in ('mean', 'var', 'std', 'cumprod', 'cumsum'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(g, func), 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(g, func), foo=1) + def assert_fp_equal(a, b): assert (np.abs(a - b) < 1e-12).all() diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index a043e92bd2c76..8d9a55bade30d 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -20,6 +20,7 @@ import pandas.stats.moments as mom import pandas.core.window as rwindow from pandas.core.base import SpecificationError +from pandas.core.common import UnsupportedFunctionCall import pandas.util.testing as tm from pandas.compat import range, zip, PY3 @@ -296,6 +297,18 @@ def test_constructor(self): with self.assertRaises(ValueError): c(win_type=wt, window=2) + def test_numpy_compat(self): + # see gh-12811 + w = rwindow.Window(Series([2, 4, 6]), window=[0, 2]) + + msg = "numpy operations are not valid with window objects" + + for func in ('sum', 'mean'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(w, func), 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(w, func), dtype=np.float64) + class TestRolling(Base): @@ -323,6 +336,18 @@ def test_constructor(self): with self.assertRaises(ValueError): c(window=2, min_periods=1, center=w) + def test_numpy_compat(self): + # see gh-12811 + r = rwindow.Rolling(Series([2, 4, 6]), window=2) + + msg = "numpy operations are not valid with window objects" + + for func in ('std', 'mean', 'sum', 'max', 'min', 'var'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(r, func), 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(r, func), dtype=np.float64) + class TestExpanding(Base): @@ -347,6 +372,74 @@ def test_constructor(self): with self.assertRaises(ValueError): c(min_periods=1, center=w) + def test_numpy_compat(self): + # see gh-12811 + e = rwindow.Expanding(Series([2, 4, 6]), window=2) + + msg = "numpy operations are not valid with window objects" + + for func in ('std', 'mean', 'sum', 'max', 'min', 'var'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(e, func), 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(e, func), dtype=np.float64) + + +class TestEWM(Base): + + def setUp(self): + self._create_data() + + def test_constructor(self): + for o in [self.series, self.frame]: + c = o.ewm + + # valid + c(com=0.5) + c(span=1.5) + c(alpha=0.5) + c(halflife=0.75) + c(com=0.5, span=None) + c(alpha=0.5, com=None) + c(halflife=0.75, alpha=None) + + # not valid: mutually exclusive + with self.assertRaises(ValueError): + c(com=0.5, alpha=0.5) + with self.assertRaises(ValueError): + c(span=1.5, halflife=0.75) + with self.assertRaises(ValueError): + c(alpha=0.5, span=1.5) + + # not valid: com < 0 + with self.assertRaises(ValueError): + c(com=-0.5) + + # not valid: span < 1 + with self.assertRaises(ValueError): + c(span=0.5) + + # not valid: halflife <= 0 + with self.assertRaises(ValueError): + c(halflife=0) + + # not valid: alpha <= 0 or alpha > 1 + for alpha in (-0.5, 1.5): + with self.assertRaises(ValueError): + c(alpha=alpha) + + def test_numpy_compat(self): + # see gh-12811 + e = rwindow.EWM(Series([2, 4, 6]), alpha=0.5) + + msg = "numpy operations are not valid with window objects" + + for func in ('std', 'mean', 'var'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(e, func), 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(e, func), dtype=np.float64) + class TestDeprecations(Base): """ test that we are catching deprecation warnings """ diff --git a/pandas/tseries/resample.py b/pandas/tseries/resample.py index bb7915e978c3e..ac30db35c0f85 100644 --- a/pandas/tseries/resample.py +++ b/pandas/tseries/resample.py @@ -16,7 +16,9 @@ from pandas.tseries.period import PeriodIndex, period_range import pandas.core.common as com import pandas.core.algorithms as algos + import pandas.compat as compat +from pandas.compat.numpy import function as nv from pandas.lib import Timestamp import pandas.lib as lib @@ -480,7 +482,7 @@ def asfreq(self): """ return self._upsample('asfreq') - def std(self, ddof=1): + def std(self, ddof=1, *args, **kwargs): """ Compute standard deviation of groups, excluding missing values @@ -489,9 +491,10 @@ def std(self, ddof=1): ddof : integer, default 1 degrees of freedom """ + nv.validate_resampler_func('std', args, kwargs) return self._downsample('std', ddof=ddof) - def var(self, ddof=1): + def var(self, ddof=1, *args, **kwargs): """ Compute variance of groups, excluding missing values @@ -500,6 +503,7 @@ def var(self, ddof=1): ddof : integer, default 1 degrees of freedom """ + nv.validate_resampler_func('var', args, kwargs) return self._downsample('var', ddof=ddof) Resampler._deprecated_valids += dir(Resampler) @@ -507,7 +511,8 @@ def var(self, ddof=1): for method in ['min', 'max', 'first', 'last', 'sum', 'mean', 'sem', 'median', 'prod', 'ohlc']: - def f(self, _method=method): + def f(self, _method=method, *args, **kwargs): + nv.validate_resampler_func(_method, args, kwargs) return self._downsample(_method) f.__doc__ = getattr(GroupBy, method).__doc__ setattr(Resampler, method, f) diff --git a/pandas/tseries/tests/test_resample.py b/pandas/tseries/tests/test_resample.py index dd5ab36d10a45..27b15a412ae37 100644 --- a/pandas/tseries/tests/test_resample.py +++ b/pandas/tseries/tests/test_resample.py @@ -13,7 +13,8 @@ notnull, Timestamp) from pandas.compat import range, lrange, zip, product, OrderedDict from pandas.core.base import SpecificationError -from pandas.core.common import ABCSeries, ABCDataFrame +from pandas.core.common import (ABCSeries, ABCDataFrame, + UnsupportedFunctionCall) from pandas.core.groupby import DataError from pandas.tseries.frequencies import MONTHS, DAYS from pandas.tseries.index import date_range @@ -746,6 +747,22 @@ def _ohlc(group): exc.args += ('how=%s' % arg,) raise + def test_numpy_compat(self): + # see gh-12811 + s = Series([1, 2, 3, 4, 5], index=date_range( + '20130101', periods=5, freq='s')) + r = s.resample('2s') + + msg = "numpy operations are not valid with resample" + + for func in ('min', 'max', 'sum', 'prod', + 'mean', 'var', 'std'): + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(r, func), + func, 1, 2, 3) + tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, + getattr(r, func), axis=1) + def test_resample_how_callables(self): # GH 7929 data = np.arange(5, dtype=np.int64) diff --git a/pandas/tseries/tests/test_tslib.py b/pandas/tseries/tests/test_tslib.py index 4543047a8a72a..79f9c60c9deb7 100644 --- a/pandas/tseries/tests/test_tslib.py +++ b/pandas/tseries/tests/test_tslib.py @@ -1290,6 +1290,34 @@ def test_shift_months(self): years=years, months=months) for x in s]) tm.assert_index_equal(actual, expected) + def test_round(self): + # see gh-12811 + stamp = Timestamp('2000-01-05 05:09:15.13') + + def _check_round(freq, expected): + result = stamp.round(freq=freq) + npResult = np.round(stamp, freq) + self.assertEqual(result, expected) + self.assertEqual(npResult, expected) + + for freq, expected in [ + ('D', Timestamp('2000-01-05 00:00:00')), + ('H', Timestamp('2000-01-05 05:00:00')), + ('S', Timestamp('2000-01-05 05:09:15')) + ]: + _check_round(freq, expected) + + msg = "the 'out' parameter is not supported" + tm.assertRaisesRegexp(ValueError, msg, np.round, + stamp, 'D', out=[]) + + # 'freq' is a required parameter, so we cannot + # assign a default should the user accidentally + # assign a 'decimals' input instead + msg = "Could not evaluate" + tm.assertRaisesRegexp(ValueError, msg, np.round, + stamp, 2) + class TestTimestampOps(tm.TestCase): def test_timestamp_and_datetime(self): diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index a240558025090..281a74d640292 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -330,7 +330,7 @@ class Timestamp(_Timestamp): result = result.tz_localize(self.tz) return result - def round(self, freq): + def round(self, freq, *args, **kwargs): """ Round the Timestamp to the specified resolution @@ -346,6 +346,8 @@ class Timestamp(_Timestamp): ------ ValueError if the freq cannot be converted """ + from pandas.compat.numpy.function import validate_round + validate_round(args, kwargs) return self._round(freq, np.round) def floor(self, freq):