Repository: dask/dask

Issue:
Refactor resample API to match Pandas
Pandas recently changed their resample API: http://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#resample-api

We should try to match.



Discussion:

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -976,38 +976,10 @@ def quantile(self, q=0.5):
         """
         return quantile(self, q)
 
-    def resample(self, rule, how='mean', axis=0, fill_method=None, closed=None,
-                 label=None, convention='start', kind=None, loffset=None,
-                 limit=None, base=0):
-        """Group by a DatetimeIndex values in time periods of size `rule`.
-
-        Parameters
-        ----------
-        rule : str or pandas.datetools.Tick
-            The frequency to resample by. For example, 'H' is one hour
-            intervals.
-        how : str or callable
-            Method to use to summarize your data. For example, 'mean' takes the
-            average value of the Series in the time interval `rule`.
-
-        Notes
-        -----
-        For additional argument descriptions please consult the pandas
-        documentation.
-
-        Returns
-        -------
-        dask.dataframe.Series
-
-        See Also
-        --------
-        pandas.Series.resample
-        """
-
+    @derived_from(pd.Series)
+    def resample(self, rule, how=None, closed=None, label=None):
         from .tseries.resample import _resample
-        return _resample(self, rule, how=how, axis=axis, fill_method=fill_method,
-                         closed=closed, label=label, convention=convention,
-                         kind=kind, loffset=loffset, limit=limit, base=base)
+        return _resample(self, rule, how=how, closed=closed, label=label)
 
     def __getitem__(self, key):
         if isinstance(key, Series) and self.divisions == key.divisions:
diff --git a/dask/dataframe/tseries/resample.py b/dask/dataframe/tseries/resample.py
--- a/dask/dataframe/tseries/resample.py
+++ b/dask/dataframe/tseries/resample.py
@@ -1,62 +1,51 @@
 from __future__ import absolute_import, division, print_function
 
+from distutils.version import LooseVersion
+
 import pandas as pd
+import numpy as np
 
 from ..core import DataFrame, Series
 from ...base import tokenize
 
 
-def _resample(series, rule, how='mean', axis=0, fill_method=None, closed=None,
-              label=None, convention='start', kind=None, loffset=None,
-              limit=None, base=0):
-
-    # Validate Inputs
-    rule = pd.datetools.to_offset(rule)
-    day_nanos = pd.datetools.Day().nanos
-
-    if getattr(rule, 'nanos', None) and day_nanos % rule.nanos:
-        raise NotImplementedError('Resampling frequency %s that does'
-                                  ' not evenly divide a day is not '
-                                  'implemented' % rule)
-
-    kwargs = {'fill_method': fill_method, 'limit': limit,
-              'loffset': loffset, 'base': base,
-              'convention': convention != 'start', 'kind': kind}
-    err = ', '.join('`{0}`'.format(k) for (k, v) in kwargs.items() if v)
-    if err:
-        raise NotImplementedError('Keywords: ' + err)
-
-    kwargs = {'how': how, 'closed': closed, 'label': label}
-
-    # Create a grouper to determine closed and label conventions
-    newdivs, outdivs = _resample_bin_and_out_divs(series.divisions, rule,
-                                                  closed, label)
+def getnanos(rule):
+    try:
+        return getattr(rule, 'nanos', None)
+    except ValueError:
+        return None
 
-    # Repartition divs into bins. These won't match labels after mapping
-    partitioned = series.repartition(newdivs, force=True)
 
-    name = tokenize(series, rule, kwargs)
-    dsk = partitioned.dask
+if LooseVersion(pd.__version__) >= '0.18.0':
+    def _resample_apply(s, rule, how, resample_kwargs):
+        return getattr(s.resample(rule, how=how, **resample_kwargs), how)()
 
-    keys = partitioned._keys()
-    args = zip(keys, outdivs, outdivs[1:], ['left']*(len(keys)-1) + [None])
-    for i, (k, s, e, c) in enumerate(args):
-        dsk[(name, i)] = (_resample_series, k, s, e, c, rule,
-                          how, label, closed)
+    def _resample(obj, rule, how, **kwargs):
+        resampler = Resampler(obj, rule, **kwargs)
+        if how is not None:
+            raise FutureWarning(("how in .resample() is deprecated "
+                                 "the new syntax is .resample(...)"
+                                 ".{0}()").format(how))
+            return getattr(resampler, how)()
+        return resampler
+else:
+    def _resample_apply(s, rule, how, resample_kwargs):
+        return s.resample(rule, how=how, **resample_kwargs)
 
-    if how == 'ohlc':
-        return DataFrame(dsk, name, ['open', 'high', 'low', 'close'], outdivs)
-    return Series(dsk, name, series.name, outdivs)
+    def _resample(obj, rule, how, **kwargs):
+        how = how or 'mean'
+        return getattr(Resampler(obj, rule, **kwargs), how)()
 
 
-def _resample_series(series, start, end, reindex_closed,
-                     rule, how, label, resample_closed):
-    out = series.resample(rule, how=how, closed=resample_closed, label=label)
+def _resample_series(series, start, end, reindex_closed, rule,
+                     resample_kwargs, how, fill_value):
+    out = _resample_apply(series, rule, how, resample_kwargs)
     return out.reindex(pd.date_range(start, end, freq=rule,
-                                     closed=reindex_closed))
+                                     closed=reindex_closed),
+                       fill_value=fill_value)
 
 
-def _resample_bin_and_out_divs(divisions, rule, closed, label):
+def _resample_bin_and_out_divs(divisions, rule, closed='left', label='left'):
     rule = pd.datetools.to_offset(rule)
     g = pd.TimeGrouper(rule, how='count', closed=closed, label=label)
 
@@ -95,3 +84,78 @@ def _resample_bin_and_out_divs(divisions, rule, closed, label):
 
     return tuple(map(pd.Timestamp, newdivs)), tuple(map(pd.Timestamp, outdivs))
 
+
+class Resampler(object):
+    def __init__(self, obj, rule, **kwargs):
+        self.obj = obj
+        rule = pd.datetools.to_offset(rule)
+        day_nanos = pd.datetools.Day().nanos
+
+        if getnanos(rule) and day_nanos % rule.nanos:
+            raise NotImplementedError('Resampling frequency %s that does'
+                                      ' not evenly divide a day is not '
+                                      'implemented' % rule)
+        self._rule = rule
+        self._kwargs = kwargs
+
+    def _agg(self, how, columns=None, fill_value=np.nan):
+        rule = self._rule
+        kwargs = self._kwargs
+        name = tokenize(self.obj, rule, kwargs, how)
+
+        # Create a grouper to determine closed and label conventions
+        newdivs, outdivs = _resample_bin_and_out_divs(self.obj.divisions, rule,
+                                                      **kwargs)
+
+        # Repartition divs into bins. These won't match labels after mapping
+        partitioned = self.obj.repartition(newdivs, force=True)
+
+        keys = partitioned._keys()
+        dsk = partitioned.dask
+
+        args = zip(keys, outdivs, outdivs[1:], ['left']*(len(keys)-1) + [None])
+        for i, (k, s, e, c) in enumerate(args):
+            dsk[(name, i)] = (_resample_series, k, s, e, c,
+                              rule, kwargs, how, fill_value)
+        if columns:
+            return DataFrame(dsk, name, columns, outdivs)
+        return Series(dsk, name, self.obj.name, outdivs)
+
+    def count(self):
+        return self._agg('count', fill_value=0)
+
+    def first(self):
+        return self._agg('first')
+
+    def last(self):
+        return self._agg('last')
+
+    def mean(self):
+        return self._agg('mean')
+
+    def min(self):
+        return self._agg('min')
+
+    def median(self):
+        return self._agg('median')
+
+    def max(self):
+        return self._agg('max')
+
+    def ohlc(self):
+        return self._agg('ohlc', columns=['open', 'high', 'low', 'close'])
+
+    def prod(self):
+        return self._agg('prod')
+
+    def sem(self):
+        return self._agg('sem')
+
+    def std(self):
+        return self._agg('std')
+
+    def sum(self):
+        return self._agg('sum')
+
+    def var(self):
+        return self._agg('var')


Test patch:
diff --git a/dask/dataframe/tseries/tests/test_resample.py b/dask/dataframe/tseries/tests/test_resample.py
--- a/dask/dataframe/tseries/tests/test_resample.py
+++ b/dask/dataframe/tseries/tests/test_resample.py
@@ -6,26 +6,28 @@
 
 from dask.utils import raises
 import dask.dataframe as dd
-from dask.dataframe.utils import eq
 
-@pytest.mark.parametrize(['npartitions', 'freq', 'closed', 'label'],
-                         list(product([2, 5], ['30T', 'h', 'd', 'w', 'M'],
-                                      ['right', 'left'], ['right', 'left'])))
-def test_series_resample(npartitions, freq, closed, label):
+
+@pytest.mark.parametrize(['method', 'npartitions', 'freq', 'closed', 'label'],
+                         list(product(['count', 'mean', 'ohlc'],
+                                      [2, 5],
+                                      ['30T', 'h', 'd', 'w', 'M'],
+                                      ['right', 'left'],
+                                      ['right', 'left'])))
+def test_series_resample(method, npartitions, freq, closed, label):
     index = pd.date_range('1-1-2000', '2-15-2000', freq='h')
     index = index.union(pd.date_range('4-15-2000', '5-15-2000', freq='h'))
     df = pd.Series(range(len(index)), index=index)
     ds = dd.from_pandas(df, npartitions=npartitions)
     # Series output
-    result = ds.resample(freq, how='mean', closed=closed, label=label).compute()
-    expected = df.resample(freq, how='mean', closed=closed, label=label)
-    tm.assert_series_equal(result, expected, check_dtype=False)
-    # Frame output
-    resampled = ds.resample(freq, how='ohlc', closed=closed, label=label)
-    divisions = resampled.divisions
-    result = resampled.compute()
-    expected = df.resample(freq, how='ohlc', closed=closed, label=label)
-    tm.assert_frame_equal(result, expected, check_dtype=False)
+    result = ds.resample(freq, how=method, closed=closed, label=label)
+    divisions = result.divisions
+    result = result.compute()
+    expected = df.resample(freq, how=method, closed=closed, label=label)
+    if method != 'ohlc':
+        tm.assert_series_equal(result, expected, check_dtype=False)
+    else:
+        tm.assert_frame_equal(result, expected, check_dtype=False)
     assert expected.index[0] == divisions[0]
     assert expected.index[-1] == divisions[-1]
 
@@ -36,8 +38,3 @@ def test_series_resample_not_implemented():
     ds = dd.from_pandas(s, npartitions=5)
     # Frequency doesn't evenly divide day
     assert raises(NotImplementedError, lambda: ds.resample('57T'))
-    # Kwargs not implemented
-    kwargs = {'fill_method': 'bfill', 'limit': 2, 'loffset': 2, 'base': 2,
-              'convention': 'end', 'kind': 'period'}
-    for k, v in kwargs.items():
-        assert raises(NotImplementedError, lambda: ds.resample('6h', **{k: v}))