Skip to content

Commit 14cf67f

Browse files
committed
BUG: resample fixes
make sure .resample(...).plot() warns and returns a correct plotting object make sure that .groupby(...).resample(....) is hitting warnings when appropriate closes #12448 Author: Jeff Reback <[email protected]> Closes #12449 from jreback/resample and squashes the following commits: 4a49f1b [Jeff Reback] BUG: resample fixes
1 parent 5f7e290 commit 14cf67f

File tree

8 files changed

+245
-81
lines changed

8 files changed

+245
-81
lines changed

doc/source/whatsnew/v0.18.0.txt

+24-1
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ other anchored offsets like ``MonthBegin`` and ``YearBegin``.
705705
Resample API
706706
^^^^^^^^^^^^
707707

708-
Like the change in the window functions API :ref:`above <whatsnew_0180.enhancements.moments>`, ``.resample(...)`` is changing to have a more groupby-like API. (:issue:`11732`, :issue:`12702`, :issue:`12202`, :issue:`12332`, :issue:`12334`, :issue:`12348`).
708+
Like the change in the window functions API :ref:`above <whatsnew_0180.enhancements.moments>`, ``.resample(...)`` is changing to have a more groupby-like API. (:issue:`11732`, :issue:`12702`, :issue:`12202`, :issue:`12332`, :issue:`12334`, :issue:`12348`, :issue:`12448`).
709709

710710
.. ipython:: python
711711

@@ -774,6 +774,29 @@ You could also specify a ``how`` directly
774774
use .resample(...).mean() instead of .resample(...)
775775
assignment will have no effect as you are working on a copy
776776

777+
There is a situation where the new API can not perform all the operations when using original code.
778+
This code is intending to resample every 2s, take the ``mean`` AND then take the ``min` of those results.
779+
780+
.. code-block:: python
781+
782+
In [4]: df.resample('2s').min()
783+
Out[4]:
784+
A 0.433985
785+
B 0.314582
786+
C 0.357096
787+
D 0.531096
788+
dtype: float64
789+
790+
The new API will:
791+
792+
.. ipython: python
793+
794+
df.resample('2s').min()
795+
796+
Good news is the return dimensions will differ (between the new API and the old API), so this should loudly raise
797+
an exception.
798+
799+
777800
**New API**:
778801

779802
Now, you can write ``.resample`` as a 2-stage operation like groupby, which

pandas/core/generic.py

+8-47
Original file line numberDiff line numberDiff line change
@@ -3932,57 +3932,18 @@ def resample(self, rule, how=None, axis=0, fill_method=None, closed=None,
39323932
Freq: 3T, dtype: int64
39333933
39343934
"""
3935-
from pandas.tseries.resample import resample
3935+
from pandas.tseries.resample import (resample,
3936+
_maybe_process_deprecations)
39363937

39373938
axis = self._get_axis_number(axis)
39383939
r = resample(self, freq=rule, label=label, closed=closed,
39393940
axis=axis, kind=kind, loffset=loffset,
3940-
fill_method=fill_method, convention=convention,
3941-
limit=limit, base=base)
3942-
3943-
# deprecation warnings
3944-
# but call methods anyhow
3945-
3946-
if how is not None:
3947-
3948-
# .resample(..., how='sum')
3949-
if isinstance(how, compat.string_types):
3950-
method = "{0}()".format(how)
3951-
3952-
# .resample(..., how=lambda x: ....)
3953-
else:
3954-
method = ".apply(<func>)"
3955-
3956-
# if we have both a how and fill_method, then show
3957-
# the following warning
3958-
if fill_method is None:
3959-
warnings.warn("how in .resample() is deprecated\n"
3960-
"the new syntax is "
3961-
".resample(...).{method}".format(
3962-
method=method),
3963-
FutureWarning, stacklevel=2)
3964-
r = r.aggregate(how)
3965-
3966-
if fill_method is not None:
3967-
3968-
# show the prior function call
3969-
method = '.' + method if how is not None else ''
3970-
3971-
args = "limit={0}".format(limit) if limit is not None else ""
3972-
warnings.warn("fill_method is deprecated to .resample()\n"
3973-
"the new syntax is .resample(...){method}"
3974-
".{fill_method}({args})".format(
3975-
method=method,
3976-
fill_method=fill_method,
3977-
args=args),
3978-
FutureWarning, stacklevel=2)
3979-
3980-
if how is not None:
3981-
r = getattr(r, fill_method)(limit=limit)
3982-
else:
3983-
r = r.aggregate(fill_method, limit=limit)
3984-
3985-
return r
3941+
convention=convention,
3942+
base=base)
3943+
return _maybe_process_deprecations(r,
3944+
how=how,
3945+
fill_method=fill_method,
3946+
limit=limit)
39863947

39873948
def first(self, offset):
39883949
"""

pandas/core/groupby.py

+55-11
Original file line numberDiff line numberDiff line change
@@ -1044,27 +1044,71 @@ def ohlc(self):
10441044

10451045
@Substitution(name='groupby')
10461046
@Appender(_doc_template)
1047-
def resample(self, rule, **kwargs):
1047+
def resample(self, rule, how=None, fill_method=None, limit=None, **kwargs):
10481048
"""
10491049
Provide resampling when using a TimeGrouper
10501050
Return a new grouper with our resampler appended
10511051
"""
1052-
from pandas.tseries.resample import TimeGrouper
1052+
from pandas.tseries.resample import (TimeGrouper,
1053+
_maybe_process_deprecations)
10531054
gpr = TimeGrouper(axis=self.axis, freq=rule, **kwargs)
10541055

10551056
# we by definition have at least 1 key as we are already a grouper
10561057
groupings = list(self.grouper.groupings)
10571058
groupings.append(gpr)
10581059

1059-
return self.__class__(self.obj,
1060-
keys=groupings,
1061-
axis=self.axis,
1062-
level=self.level,
1063-
as_index=self.as_index,
1064-
sort=self.sort,
1065-
group_keys=self.group_keys,
1066-
squeeze=self.squeeze,
1067-
selection=self._selection)
1060+
result = self.__class__(self.obj,
1061+
keys=groupings,
1062+
axis=self.axis,
1063+
level=self.level,
1064+
as_index=self.as_index,
1065+
sort=self.sort,
1066+
group_keys=self.group_keys,
1067+
squeeze=self.squeeze,
1068+
selection=self._selection)
1069+
1070+
return _maybe_process_deprecations(result,
1071+
how=how,
1072+
fill_method=fill_method,
1073+
limit=limit)
1074+
1075+
@Substitution(name='groupby')
1076+
@Appender(_doc_template)
1077+
def pad(self, limit=None):
1078+
"""
1079+
Forward fill the values
1080+
1081+
Parameters
1082+
----------
1083+
limit : integer, optional
1084+
limit of how many values to fill
1085+
1086+
See Also
1087+
--------
1088+
Series.fillna
1089+
DataFrame.fillna
1090+
"""
1091+
return self.apply(lambda x: x.ffill(limit=limit))
1092+
ffill = pad
1093+
1094+
@Substitution(name='groupby')
1095+
@Appender(_doc_template)
1096+
def backfill(self, limit=None):
1097+
"""
1098+
Backward fill the values
1099+
1100+
Parameters
1101+
----------
1102+
limit : integer, optional
1103+
limit of how many values to fill
1104+
1105+
See Also
1106+
--------
1107+
Series.fillna
1108+
DataFrame.fillna
1109+
"""
1110+
return self.apply(lambda x: x.bfill(limit=limit))
1111+
bfill = backfill
10681112

10691113
@Substitution(name='groupby')
10701114
@Appender(_doc_template)

pandas/tests/test_graphics.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
from pandas.util.decorators import cache_readonly
1818
import pandas.core.common as com
1919
import pandas.util.testing as tm
20-
from pandas.util.testing import ensure_clean
20+
from pandas.util.testing import (ensure_clean,
21+
assert_is_valid_plot_return_object)
22+
2123
from pandas.core.config import set_option
2224

2325
import numpy as np
@@ -3916,21 +3918,6 @@ def test_plot_kwargs(self):
39163918
self.assertEqual(len(res['a'].collections), 1)
39173919

39183920

3919-
def assert_is_valid_plot_return_object(objs):
3920-
import matplotlib.pyplot as plt
3921-
if isinstance(objs, np.ndarray):
3922-
for el in objs.flat:
3923-
assert isinstance(el, plt.Axes), ('one of \'objs\' is not a '
3924-
'matplotlib Axes instance, '
3925-
'type encountered {0!r}'
3926-
''.format(el.__class__.__name__))
3927-
else:
3928-
assert isinstance(objs, (plt.Artist, tuple, dict)), \
3929-
('objs is neither an ndarray of Artist instances nor a '
3930-
'single Artist instance, tuple, or dict, "objs" is a {0!r} '
3931-
''.format(objs.__class__.__name__))
3932-
3933-
39343921
def _check_plot_works(f, filterwarnings='always', **kwargs):
39353922
import matplotlib.pyplot as plt
39363923
ret = None

pandas/tests/test_groupby.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5610,7 +5610,8 @@ def test_tab_completion(self):
56105610
'cumprod', 'tail', 'resample', 'cummin', 'fillna', 'cumsum',
56115611
'cumcount', 'all', 'shift', 'skew', 'bfill', 'ffill', 'take',
56125612
'tshift', 'pct_change', 'any', 'mad', 'corr', 'corrwith', 'cov',
5613-
'dtypes', 'ndim', 'diff', 'idxmax', 'idxmin'])
5613+
'dtypes', 'ndim', 'diff', 'idxmax', 'idxmin',
5614+
'ffill', 'bfill', 'pad', 'backfill'])
56145615
self.assertEqual(results, expected)
56155616

56165617
def test_lexsort_indexer(self):

pandas/tseries/resample.py

+64-4
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _typ(self):
102102
def _deprecated(self):
103103
warnings.warn(".resample() is now a deferred operation\n"
104104
"use .resample(...).mean() instead of .resample(...)",
105-
FutureWarning, stacklevel=2)
105+
FutureWarning, stacklevel=3)
106106
return self.mean()
107107

108108
def _make_deprecated_binop(op):
@@ -154,9 +154,7 @@ def __getattr__(self, attr):
154154
if attr in self._deprecated_invalids:
155155
raise ValueError(".resample() is now a deferred operation\n"
156156
"\tuse .resample(...).mean() instead of "
157-
".resample(...)\n"
158-
"\tassignment will have no effect as you "
159-
"are working on a copy")
157+
".resample(...)")
160158
if attr not in self._deprecated_valids:
161159
self = self._deprecated()
162160
return object.__getattribute__(self, attr)
@@ -167,6 +165,17 @@ def __setattr__(self, attr, value):
167165
self.__class__.__name__))
168166
object.__setattr__(self, attr, value)
169167

168+
def __getitem__(self, key):
169+
try:
170+
return super(Resampler, self).__getitem__(key)
171+
except (KeyError, com.AbstractMethodError):
172+
173+
# compat for deprecated
174+
if isinstance(self.obj, com.ABCSeries):
175+
return self._deprecated()[key]
176+
177+
raise
178+
170179
def __setitem__(self, attr, value):
171180
raise ValueError("cannot set items on {0}".format(
172181
self.__class__.__name__))
@@ -208,6 +217,11 @@ def _assure_grouper(self):
208217
""" make sure that we are creating our binner & grouper """
209218
self._set_binner()
210219

220+
def plot(self, *args, **kwargs):
221+
# for compat with prior versions, we want to
222+
# have the warnings shown here and just have this work
223+
return self._deprecated().plot(*args, **kwargs)
224+
211225
def aggregate(self, arg, *args, **kwargs):
212226
"""
213227
Apply aggregation function or functions to resampled groups, yielding
@@ -468,6 +482,52 @@ def f(self, _method=method):
468482
setattr(Resampler, method, f)
469483

470484

485+
def _maybe_process_deprecations(r, how=None, fill_method=None, limit=None):
486+
""" potentially we might have a deprecation warning, show it
487+
but call the appropriate methods anyhow """
488+
489+
if how is not None:
490+
491+
# .resample(..., how='sum')
492+
if isinstance(how, compat.string_types):
493+
method = "{0}()".format(how)
494+
495+
# .resample(..., how=lambda x: ....)
496+
else:
497+
method = ".apply(<func>)"
498+
499+
# if we have both a how and fill_method, then show
500+
# the following warning
501+
if fill_method is None:
502+
warnings.warn("how in .resample() is deprecated\n"
503+
"the new syntax is "
504+
".resample(...).{method}".format(
505+
method=method),
506+
FutureWarning, stacklevel=3)
507+
r = r.aggregate(how)
508+
509+
if fill_method is not None:
510+
511+
# show the prior function call
512+
method = '.' + method if how is not None else ''
513+
514+
args = "limit={0}".format(limit) if limit is not None else ""
515+
warnings.warn("fill_method is deprecated to .resample()\n"
516+
"the new syntax is .resample(...){method}"
517+
".{fill_method}({args})".format(
518+
method=method,
519+
fill_method=fill_method,
520+
args=args),
521+
FutureWarning, stacklevel=3)
522+
523+
if how is not None:
524+
r = getattr(r, fill_method)(limit=limit)
525+
else:
526+
r = r.aggregate(fill_method, limit=limit)
527+
528+
return r
529+
530+
471531
class DatetimeIndexResampler(Resampler):
472532

473533
def _get_binner_for_time(self):

0 commit comments

Comments
 (0)