Skip to content

Commit 421be44

Browse files
shoyermax-sixty
authored andcommitted
Remove the old syntax for resample. (#2541)
This has been deprecated since xarray 0.10. I also added support for passing a mapping ``{dim: freq}`` as the first argument.
1 parent 38399cc commit 421be44

File tree

4 files changed

+51
-124
lines changed

4 files changed

+51
-124
lines changed

doc/whats-new.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ Breaking changes
5757
includes only data variables.
5858
- ``DataArray.__contains__`` (used by Python's ``in`` operator) now checks
5959
array data, not coordinates.
60+
- The old resample syntax from before xarray 0.10, e.g.,
61+
``data.resample('1D', dim='time', how='mean')``, is no longer supported will
62+
raise an error in most cases. You need to use the new resample syntax
63+
instead, e.g., ``data.resample(time='1D').mean()`` or
64+
``data.resample({'time': '1D'}).mean()``.
6065
- Xarray's storage backends now automatically open and close files when
6166
necessary, rather than requiring opening a file with ``autoclose=True``. A
6267
global least-recently-used cache is used to store open files; the default
@@ -111,6 +116,10 @@ Enhancements
111116
python driver and *ecCodes* C-library. (:issue:`2475`)
112117
By `Alessandro Amici <https://github.com/alexamici>`_,
113118
sponsored by `ECMWF <https://github.com/ecmwf>`_.
119+
- Resample now supports a dictionary mapping from dimension to frequency as
120+
its first argument, e.g., ``data.resample({'time': '1D'}).mean()``. This is
121+
consistent with other xarray functions that accept either dictionaries or
122+
keyword arguments. By `Stephan Hoyer <https://github.com/shoyer>`_.
114123

115124
Bug fixes
116125
~~~~~~~~~

xarray/core/common.py

Lines changed: 21 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ def rolling(self, dim=None, min_periods=None, center=False, **dim_kwargs):
548548
Set the labels at the center of the window.
549549
**dim_kwargs : optional
550550
The keyword arguments form of ``dim``.
551-
One of dim or dim_kwarg must be provided.
551+
One of dim or dim_kwargs must be provided.
552552
553553
Returns
554554
-------
@@ -591,15 +591,17 @@ def rolling(self, dim=None, min_periods=None, center=False, **dim_kwargs):
591591
return self._rolling_cls(self, dim, min_periods=min_periods,
592592
center=center)
593593

594-
def resample(self, freq=None, dim=None, how=None, skipna=None,
595-
closed=None, label=None, base=0, keep_attrs=None, **indexer):
594+
def resample(self, indexer=None, skipna=None, closed=None, label=None,
595+
base=0, keep_attrs=None, **indexer_kwargs):
596596
"""Returns a Resample object for performing resampling operations.
597597
598598
Handles both downsampling and upsampling. If any intervals contain no
599599
values from the original object, they will be given the value ``NaN``.
600600
601601
Parameters
602602
----------
603+
indexer : {dim: freq}, optional
604+
Mapping from the dimension name to resample frequency.
603605
skipna : bool, optional
604606
Whether to skip missing values when aggregating in downsampling.
605607
closed : 'left' or 'right', optional
@@ -614,9 +616,9 @@ def resample(self, freq=None, dim=None, how=None, skipna=None,
614616
If True, the object's attributes (`attrs`) will be copied from
615617
the original object to the new one. If False (default), the new
616618
object will be returned without attributes.
617-
**indexer : {dim: freq}
618-
Dictionary with a key indicating the dimension name to resample
619-
over and a value corresponding to the resampling frequency.
619+
**indexer_kwargs : {dim: freq}
620+
The keyword arguments form of ``indexer``.
621+
One of indexer or indexer_kwargs must be provided.
620622
621623
Returns
622624
-------
@@ -664,30 +666,24 @@ def resample(self, freq=None, dim=None, how=None, skipna=None,
664666
if keep_attrs is None:
665667
keep_attrs = _get_keep_attrs(default=False)
666668

667-
if dim is not None:
668-
if how is None:
669-
how = 'mean'
670-
return self._resample_immediately(freq, dim, how, skipna, closed,
671-
label, base, keep_attrs)
669+
# note: the second argument (now 'skipna') use to be 'dim'
670+
if ((skipna is not None and not isinstance(skipna, bool))
671+
or ('how' in indexer_kwargs and 'how' not in self.dims)
672+
or ('dim' in indexer_kwargs and 'dim' not in self.dims)):
673+
raise TypeError('resample() no longer supports the `how` or '
674+
'`dim` arguments. Instead call methods on resample '
675+
"objects, e.g., data.resample(time='1D').mean()")
676+
677+
indexer = either_dict_or_kwargs(indexer, indexer_kwargs, 'resample')
672678

673-
if (how is not None) and indexer:
674-
raise TypeError("If passing an 'indexer' then 'dim' "
675-
"and 'how' should not be used")
676-
677-
# More than one indexer is ambiguous, but we do in fact need one if
678-
# "dim" was not provided, until the old API is fully deprecated
679679
if len(indexer) != 1:
680680
raise ValueError(
681681
"Resampling only supported along single dimensions."
682682
)
683683
dim, freq = indexer.popitem()
684684

685-
if isinstance(dim, basestring):
686-
dim_name = dim
687-
dim = self[dim]
688-
else:
689-
raise TypeError("Dimension name should be a string; "
690-
"was passed %r" % dim)
685+
dim_name = dim
686+
dim_coord = self[dim]
691687

692688
if isinstance(self.indexes[dim_name], CFTimeIndex):
693689
raise NotImplementedError(
@@ -702,63 +698,15 @@ def resample(self, freq=None, dim=None, how=None, skipna=None,
702698
'errors.'
703699
)
704700

705-
group = DataArray(dim, [(dim.dims, dim)], name=RESAMPLE_DIM)
701+
group = DataArray(dim_coord, coords=dim_coord.coords,
702+
dims=dim_coord.dims, name=RESAMPLE_DIM)
706703
grouper = pd.Grouper(freq=freq, closed=closed, label=label, base=base)
707704
resampler = self._resample_cls(self, group=group, dim=dim_name,
708705
grouper=grouper,
709706
resample_dim=RESAMPLE_DIM)
710707

711708
return resampler
712709

713-
def _resample_immediately(self, freq, dim, how, skipna,
714-
closed, label, base, keep_attrs):
715-
"""Implement the original version of .resample() which immediately
716-
executes the desired resampling operation. """
717-
from .dataarray import DataArray
718-
from ..coding.cftimeindex import CFTimeIndex
719-
720-
RESAMPLE_DIM = '__resample_dim__'
721-
722-
warnings.warn("\n.resample() has been modified to defer "
723-
"calculations. Instead of passing 'dim' and "
724-
"how=\"{how}\", instead consider using "
725-
".resample({dim}=\"{freq}\").{how}('{dim}') ".format(
726-
dim=dim, freq=freq, how=how),
727-
FutureWarning, stacklevel=3)
728-
729-
if isinstance(self.indexes[dim], CFTimeIndex):
730-
raise NotImplementedError(
731-
'Resample is currently not supported along a dimension '
732-
'indexed by a CFTimeIndex. For certain kinds of downsampling '
733-
'it may be possible to work around this by converting your '
734-
'time index to a DatetimeIndex using '
735-
'CFTimeIndex.to_datetimeindex. Use caution when doing this '
736-
'however, because switching to a DatetimeIndex from a '
737-
'CFTimeIndex with a non-standard calendar entails a change '
738-
'in the calendar type, which could lead to subtle and silent '
739-
'errors.'
740-
)
741-
742-
if isinstance(dim, basestring):
743-
dim = self[dim]
744-
745-
group = DataArray(dim, [(dim.dims, dim)], name=RESAMPLE_DIM)
746-
grouper = pd.Grouper(freq=freq, how=how, closed=closed, label=label,
747-
base=base)
748-
gb = self._groupby_cls(self, group, grouper=grouper)
749-
if isinstance(how, basestring):
750-
f = getattr(gb, how)
751-
if how in ['first', 'last']:
752-
result = f(skipna=skipna, keep_attrs=keep_attrs)
753-
elif how == 'count':
754-
result = f(dim=dim.name, keep_attrs=keep_attrs)
755-
else:
756-
result = f(dim=dim.name, skipna=skipna, keep_attrs=keep_attrs)
757-
else:
758-
result = gb.reduce(how, dim=dim.name, keep_attrs=keep_attrs)
759-
result = result.rename({RESAMPLE_DIM: dim.name})
760-
return result
761-
762710
def where(self, cond, other=dtypes.NA, drop=False):
763711
"""Filter elements from this object according to a condition.
764712

xarray/tests/test_dataarray.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2358,53 +2358,24 @@ def test_resample_drop_nondim_coords(self):
23582358
actual = array.resample(time="1H").interpolate('linear')
23592359
assert 'tc' not in actual.coords
23602360

2361-
def test_resample_old_vs_new_api(self):
2361+
def test_resample_keep_attrs(self):
23622362
times = pd.date_range('2000-01-01', freq='6H', periods=10)
23632363
array = DataArray(np.ones(10), [('time', times)])
2364+
array.attrs['meta'] = 'data'
23642365

2365-
# Simple mean
2366-
with pytest.warns(FutureWarning):
2367-
old_mean = array.resample('1D', 'time', how='mean')
2368-
new_mean = array.resample(time='1D').mean()
2369-
assert_identical(old_mean, new_mean)
2370-
2371-
# Mean, while keeping attributes
2372-
attr_array = array.copy()
2373-
attr_array.attrs['meta'] = 'data'
2374-
2375-
with pytest.warns(FutureWarning):
2376-
old_mean = attr_array.resample('1D', dim='time', how='mean',
2377-
keep_attrs=True)
2378-
new_mean = attr_array.resample(time='1D').mean(keep_attrs=True)
2379-
assert old_mean.attrs == new_mean.attrs
2380-
assert_identical(old_mean, new_mean)
2366+
result = array.resample(time='1D').mean(keep_attrs=True)
2367+
expected = DataArray([1, 1, 1], [('time', times[::4])],
2368+
attrs=array.attrs)
2369+
assert_identical(result, expected)
23812370

2382-
# Mean, with NaN to skip
2383-
nan_array = array.copy()
2384-
nan_array[1] = np.nan
2371+
def test_resample_skipna(self):
2372+
times = pd.date_range('2000-01-01', freq='6H', periods=10)
2373+
array = DataArray(np.ones(10), [('time', times)])
2374+
array[1] = np.nan
23852375

2386-
with pytest.warns(FutureWarning):
2387-
old_mean = nan_array.resample('1D', 'time', how='mean',
2388-
skipna=False)
2389-
new_mean = nan_array.resample(time='1D').mean(skipna=False)
2376+
result = array.resample(time='1D').mean(skipna=False)
23902377
expected = DataArray([np.nan, 1, 1], [('time', times[::4])])
2391-
assert_identical(old_mean, expected)
2392-
assert_identical(new_mean, expected)
2393-
2394-
# Try other common resampling methods
2395-
resampler = array.resample(time='1D')
2396-
for method in ['mean', 'median', 'sum', 'first', 'last', 'count']:
2397-
# Discard attributes on the call using the new api to match
2398-
# convention from old api
2399-
new_api = getattr(resampler, method)(keep_attrs=False)
2400-
with pytest.warns(FutureWarning):
2401-
old_api = array.resample('1D', dim='time', how=method)
2402-
assert_identical(new_api, old_api)
2403-
for method in [np.mean, np.sum, np.max, np.min]:
2404-
new_api = resampler.reduce(method)
2405-
with pytest.warns(FutureWarning):
2406-
old_api = array.resample('1D', dim='time', how=method)
2407-
assert_identical(new_api, old_api)
2378+
assert_identical(result, expected)
24082379

24092380
def test_upsample(self):
24102381
times = pd.date_range('2000-01-01', freq='6H', periods=5)

xarray/tests/test_dataset.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,22 +2858,21 @@ def test_resample_drop_nondim_coords(self):
28582858
actual = ds.resample(time="1H").interpolate('linear')
28592859
assert 'tc' not in actual.coords
28602860

2861-
def test_resample_old_vs_new_api(self):
2861+
def test_resample_old_api(self):
28622862

28632863
times = pd.date_range('2000-01-01', freq='6H', periods=10)
28642864
ds = Dataset({'foo': (['time', 'x', 'y'], np.random.randn(10, 5, 3)),
28652865
'bar': ('time', np.random.randn(10), {'meta': 'data'}),
28662866
'time': times})
2867-
ds.attrs['dsmeta'] = 'dsdata'
28682867

2869-
for method in ['mean', 'sum', 'count', 'first', 'last']:
2870-
resampler = ds.resample(time='1D')
2871-
# Discard attributes on the call using the new api to match
2872-
# convention from old api
2873-
new_api = getattr(resampler, method)(keep_attrs=False)
2874-
with pytest.warns(FutureWarning):
2875-
old_api = ds.resample('1D', dim='time', how=method)
2876-
assert_identical(new_api, old_api)
2868+
with raises_regex(TypeError, r'resample\(\) no longer supports'):
2869+
ds.resample('1D', 'time')
2870+
2871+
with raises_regex(TypeError, r'resample\(\) no longer supports'):
2872+
ds.resample('1D', dim='time', how='mean')
2873+
2874+
with raises_regex(TypeError, r'resample\(\) no longer supports'):
2875+
ds.resample('1D', dim='time')
28772876

28782877
def test_to_array(self):
28792878
ds = Dataset(OrderedDict([('a', 1), ('b', ('x', [1, 2, 3]))]),

0 commit comments

Comments
 (0)