diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 91ee0d75aaa..b0be0fdc74d 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -45,6 +45,9 @@ Enhancements "dayofyear" and "dayofweek" accessors (:issue:`2597`). By `Spencer Clark `_. - Support Dask ``HighLevelGraphs`` by `Matthew Rocklin `_. +- :py:meth:`DataArray.resample` and :py:meth:`Dataset.resample` now supports the + ``loffset`` kwarg just like Pandas. + By `Deepak Cherian `_ Bug fixes diff --git a/xarray/core/common.py b/xarray/core/common.py index 34057e3715d..c0a0201c7ce 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -592,7 +592,7 @@ def rolling(self, dim=None, min_periods=None, center=False, **dim_kwargs): center=center) def resample(self, indexer=None, skipna=None, closed=None, label=None, - base=0, keep_attrs=None, **indexer_kwargs): + base=0, keep_attrs=None, loffset=None, **indexer_kwargs): """Returns a Resample object for performing resampling operations. Handles both downsampling and upsampling. If any intervals contain no @@ -612,6 +612,9 @@ def resample(self, indexer=None, skipna=None, closed=None, label=None, For frequencies that evenly subdivide 1 day, the "origin" of the aggregated intervals. For example, for '24H' frequency, base could range from 0 through 23. + loffset : timedelta or str, optional + Offset used to adjust the resampled time labels. Some pandas date + offset strings are supported. keep_attrs : bool, optional If True, the object's attributes (`attrs`) will be copied from the original object to the new one. If False (default), the new @@ -700,7 +703,9 @@ def resample(self, indexer=None, skipna=None, closed=None, label=None, group = DataArray(dim_coord, coords=dim_coord.coords, dims=dim_coord.dims, name=RESAMPLE_DIM) - grouper = pd.Grouper(freq=freq, closed=closed, label=label, base=base) + # TODO: to_offset() call required for pandas==0.19.2 + grouper = pd.Grouper(freq=freq, closed=closed, label=label, base=base, + loffset=pd.tseries.frequencies.to_offset(loffset)) resampler = self._resample_cls(self, group=group, dim=dim_name, grouper=grouper, resample_dim=RESAMPLE_DIM) diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index defe72ab3ee..58ba4570ede 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -3,6 +3,7 @@ import functools import warnings +import datetime import numpy as np import pandas as pd @@ -154,6 +155,32 @@ def _unique_and_monotonic(group): return index.is_unique and index.is_monotonic +def _apply_loffset(grouper, result): + """ + (copied from pandas) + if loffset is set, offset the result index + + This is NOT an idempotent routine, it will be applied + exactly once to the result. + + Parameters + ---------- + result : Series or DataFrame + the result of resample + """ + + needs_offset = ( + isinstance(grouper.loffset, (pd.DateOffset, datetime.timedelta)) + and isinstance(result.index, pd.DatetimeIndex) + and len(result.index) > 0 + ) + + if needs_offset: + result.index = result.index + grouper.loffset + + grouper.loffset = None + + class GroupBy(SupportsArithmetic): """A object that implements the split-apply-combine pattern. @@ -235,6 +262,7 @@ def __init__(self, obj, group, squeeze=False, grouper=None, bins=None, raise ValueError('index must be monotonic for resampling') s = pd.Series(np.arange(index.size), index) first_items = s.groupby(grouper).first() + _apply_loffset(grouper, first_items) full_index = first_items.index if first_items.isnull().any(): first_items = first_items.dropna() diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 87ee60715a1..ecb60239b72 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -2273,6 +2273,11 @@ def test_resample(self): actual = array.resample(time='24H').reduce(np.mean) assert_identical(expected, actual) + actual = array.resample(time='24H', loffset='-12H').mean() + expected = DataArray(array.to_series().resample('24H', loffset='-12H') + .mean()) + assert_identical(expected, actual) + with raises_regex(ValueError, 'index must be monotonic'): array[[2, 0, 1]].resample(time='1D') diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 89ea3ba78a0..d4253ae445e 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -2804,6 +2804,18 @@ def test_resample_by_mean_with_keep_attrs(self): expected = ds.attrs assert expected == actual + def test_resample_loffset(self): + times = pd.date_range('2000-01-01', freq='6H', periods=10) + ds = Dataset({'foo': (['time', 'x', 'y'], np.random.randn(10, 5, 3)), + 'bar': ('time', np.random.randn(10), {'meta': 'data'}), + 'time': times}) + ds.attrs['dsmeta'] = 'dsdata' + + actual = ds.resample(time='24H', loffset='-12H').mean('time').time + expected = xr.DataArray(ds.bar.to_series() + .resample('24H', loffset='-12H').mean()).time + assert_identical(expected, actual) + def test_resample_by_mean_discarding_attrs(self): times = pd.date_range('2000-01-01', freq='6H', periods=10) ds = Dataset({'foo': (['time', 'x', 'y'], np.random.randn(10, 5, 3)),