Skip to content

Commit 809b97f

Browse files
authored
DEPR: resample with PeriodIndex (#55968)
* DEPR: resample with PeriodIndex * update docstring * code block in 0.21.0 whatsnew * Suppress warning * deprecate convention keyword
1 parent 3fdf0d3 commit 809b97f

File tree

8 files changed

+197
-84
lines changed

8 files changed

+197
-84
lines changed

doc/source/whatsnew/v0.21.0.rst

+11-6
Original file line numberDiff line numberDiff line change
@@ -635,17 +635,22 @@ Previous behavior:
635635
636636
New behavior:
637637

638-
.. ipython:: python
638+
.. code-block:: ipython
639639
640-
pi = pd.period_range('2017-01', periods=12, freq='M')
640+
In [1]: pi = pd.period_range('2017-01', periods=12, freq='M')
641641
642-
s = pd.Series(np.arange(12), index=pi)
642+
In [2]: s = pd.Series(np.arange(12), index=pi)
643643
644-
resampled = s.resample('2Q').mean()
644+
In [3]: resampled = s.resample('2Q').mean()
645645
646-
resampled
646+
In [4]: resampled
647+
Out[4]:
648+
2017Q1 2.5
649+
2017Q3 8.5
650+
Freq: 2Q-DEC, dtype: float64
647651
648-
resampled.index
652+
In [5]: resampled.index
653+
Out[5]: PeriodIndex(['2017Q1', '2017Q3'], dtype='period[2Q-DEC]')
649654
650655
Upsampling and calling ``.ohlc()`` previously returned a ``Series``, basically identical to calling ``.asfreq()``. OHLC upsampling now returns a DataFrame with columns ``open``, ``high``, ``low`` and ``close`` (:issue:`13083`). This is consistent with downsampling and ``DatetimeIndex`` behavior.
651656

doc/source/whatsnew/v2.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ Other Deprecations
441441
- Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`)
442442
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
443443
- Deprecated :meth:`Series.ravel`, the underlying array is already 1D, so ravel is not necessary (:issue:`52511`)
444+
- Deprecated :meth:`Series.resample` and :meth:`DataFrame.resample` with a :class:`PeriodIndex` (and the 'convention' keyword), convert to :class:`DatetimeIndex` (with ``.to_timestamp()``) before resampling instead (:issue:`53481`)
444445
- Deprecated :meth:`Series.view`, use :meth:`Series.astype` instead to change the dtype (:issue:`20251`)
445446
- Deprecated ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock``, use public APIs instead (:issue:`55139`)
446447
- Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)

pandas/core/generic.py

+16-50
Original file line numberDiff line numberDiff line change
@@ -9333,7 +9333,7 @@ def resample(
93339333
axis: Axis | lib.NoDefault = lib.no_default,
93349334
closed: Literal["right", "left"] | None = None,
93359335
label: Literal["right", "left"] | None = None,
9336-
convention: Literal["start", "end", "s", "e"] = "start",
9336+
convention: Literal["start", "end", "s", "e"] | lib.NoDefault = lib.no_default,
93379337
kind: Literal["timestamp", "period"] | None | lib.NoDefault = lib.no_default,
93389338
on: Level | None = None,
93399339
level: Level | None = None,
@@ -9371,6 +9371,9 @@ def resample(
93719371
convention : {{'start', 'end', 's', 'e'}}, default 'start'
93729372
For `PeriodIndex` only, controls whether to use the start or
93739373
end of `rule`.
9374+
9375+
.. deprecated:: 2.2.0
9376+
Convert PeriodIndex to DatetimeIndex before resampling instead.
93749377
kind : {{'timestamp', 'period'}}, optional, default None
93759378
Pass 'timestamp' to convert the resulting index to a
93769379
`DateTimeIndex` or 'period' to convert it to a `PeriodIndex`.
@@ -9535,55 +9538,6 @@ def resample(
95359538
2000-01-01 00:06:00 26
95369539
Freq: 3min, dtype: int64
95379540
9538-
For a Series with a PeriodIndex, the keyword `convention` can be
9539-
used to control whether to use the start or end of `rule`.
9540-
9541-
Resample a year by quarter using 'start' `convention`. Values are
9542-
assigned to the first quarter of the period.
9543-
9544-
>>> s = pd.Series([1, 2], index=pd.period_range('2012-01-01',
9545-
... freq='Y',
9546-
... periods=2))
9547-
>>> s
9548-
2012 1
9549-
2013 2
9550-
Freq: Y-DEC, dtype: int64
9551-
>>> s.resample('Q', convention='start').asfreq()
9552-
2012Q1 1.0
9553-
2012Q2 NaN
9554-
2012Q3 NaN
9555-
2012Q4 NaN
9556-
2013Q1 2.0
9557-
2013Q2 NaN
9558-
2013Q3 NaN
9559-
2013Q4 NaN
9560-
Freq: Q-DEC, dtype: float64
9561-
9562-
Resample quarters by month using 'end' `convention`. Values are
9563-
assigned to the last month of the period.
9564-
9565-
>>> q = pd.Series([1, 2, 3, 4], index=pd.period_range('2018-01-01',
9566-
... freq='Q',
9567-
... periods=4))
9568-
>>> q
9569-
2018Q1 1
9570-
2018Q2 2
9571-
2018Q3 3
9572-
2018Q4 4
9573-
Freq: Q-DEC, dtype: int64
9574-
>>> q.resample('M', convention='end').asfreq()
9575-
2018-03 1.0
9576-
2018-04 NaN
9577-
2018-05 NaN
9578-
2018-06 2.0
9579-
2018-07 NaN
9580-
2018-08 NaN
9581-
2018-09 3.0
9582-
2018-10 NaN
9583-
2018-11 NaN
9584-
2018-12 4.0
9585-
Freq: M, dtype: float64
9586-
95879541
For DataFrame objects, the keyword `on` can be used to specify the
95889542
column instead of the index for resampling.
95899543
@@ -9748,6 +9702,18 @@ def resample(
97489702
else:
97499703
kind = None
97509704

9705+
if convention is not lib.no_default:
9706+
warnings.warn(
9707+
f"The 'convention' keyword in {type(self).__name__}.resample is "
9708+
"deprecated and will be removed in a future version. "
9709+
"Explicitly cast PeriodIndex to DatetimeIndex before resampling "
9710+
"instead.",
9711+
FutureWarning,
9712+
stacklevel=find_stack_level(),
9713+
)
9714+
else:
9715+
convention = "start"
9716+
97519717
return get_resampler(
97529718
cast("Series | DataFrame", self),
97539719
freq=rule,

pandas/core/resample.py

+21
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,12 @@ class PeriodIndexResampler(DatetimeIndexResampler):
18761876

18771877
@property
18781878
def _resampler_for_grouping(self):
1879+
warnings.warn(
1880+
"Resampling a groupby with a PeriodIndex is deprecated. "
1881+
"Cast to DatetimeIndex before resampling instead.",
1882+
FutureWarning,
1883+
stacklevel=find_stack_level(),
1884+
)
18791885
return PeriodIndexResamplerGroupby
18801886

18811887
def _get_binner_for_time(self):
@@ -2225,6 +2231,21 @@ def _get_resampler(self, obj: NDFrame, kind=None) -> Resampler:
22252231
gpr_index=ax,
22262232
)
22272233
elif isinstance(ax, PeriodIndex) or kind == "period":
2234+
if isinstance(ax, PeriodIndex):
2235+
# GH#53481
2236+
warnings.warn(
2237+
"Resampling with a PeriodIndex is deprecated. "
2238+
"Cast index to DatetimeIndex before resampling instead.",
2239+
FutureWarning,
2240+
stacklevel=find_stack_level(),
2241+
)
2242+
else:
2243+
warnings.warn(
2244+
"Resampling with kind='period' is deprecated. "
2245+
"Use datetime paths instead.",
2246+
FutureWarning,
2247+
stacklevel=find_stack_level(),
2248+
)
22282249
return PeriodIndexResampler(
22292250
obj,
22302251
timegrouper=self,

pandas/plotting/_matplotlib/timeseries.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ def maybe_resample(series: Series, ax: Axes, kwargs: dict[str, Any]):
8686
)
8787
freq = ax_freq
8888
elif _is_sup(freq, ax_freq): # one is weekly
89-
how = "last"
90-
series = getattr(series.resample("D"), how)().dropna()
91-
series = getattr(series.resample(ax_freq), how)().dropna()
89+
# Resampling with PeriodDtype is deprecated, so we convert to
90+
# DatetimeIndex, resample, then convert back.
91+
ser_ts = series.to_timestamp()
92+
ser_d = ser_ts.resample("D").last().dropna()
93+
ser_freq = ser_d.resample(ax_freq).last().dropna()
94+
series = ser_freq.to_period(ax_freq)
9295
freq = ax_freq
9396
elif is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq):
9497
_upsample_others(ax, freq, kwargs)

pandas/tests/resample/test_base.py

+88-15
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,13 @@ def test_asfreq_fill_value(series, create_index):
8585
def test_resample_interpolate(frame):
8686
# GH#12925
8787
df = frame
88-
result = df.resample("1min").asfreq().interpolate()
89-
expected = df.resample("1min").interpolate()
88+
warn = None
89+
if isinstance(df.index, PeriodIndex):
90+
warn = FutureWarning
91+
msg = "Resampling with a PeriodIndex is deprecated"
92+
with tm.assert_produces_warning(warn, match=msg):
93+
result = df.resample("1min").asfreq().interpolate()
94+
expected = df.resample("1min").interpolate()
9095
tm.assert_frame_equal(result, expected)
9196

9297

@@ -118,7 +123,13 @@ def test_resample_empty_series(freq, empty_series_dti, resample_method):
118123
elif freq == "ME" and isinstance(ser.index, PeriodIndex):
119124
# index is PeriodIndex, so convert to corresponding Period freq
120125
freq = "M"
121-
rs = ser.resample(freq)
126+
127+
warn = None
128+
if isinstance(ser.index, PeriodIndex):
129+
warn = FutureWarning
130+
msg = "Resampling with a PeriodIndex is deprecated"
131+
with tm.assert_produces_warning(warn, match=msg):
132+
rs = ser.resample(freq)
122133
result = getattr(rs, resample_method)()
123134

124135
if resample_method == "ohlc":
@@ -150,7 +161,10 @@ def test_resample_nat_index_series(freq, series, resample_method):
150161

151162
ser = series.copy()
152163
ser.index = PeriodIndex([NaT] * len(ser), freq=freq)
153-
rs = ser.resample(freq)
164+
165+
msg = "Resampling with a PeriodIndex is deprecated"
166+
with tm.assert_produces_warning(FutureWarning, match=msg):
167+
rs = ser.resample(freq)
154168
result = getattr(rs, resample_method)()
155169

156170
if resample_method == "ohlc":
@@ -182,7 +196,13 @@ def test_resample_count_empty_series(freq, empty_series_dti, resample_method):
182196
elif freq == "ME" and isinstance(ser.index, PeriodIndex):
183197
# index is PeriodIndex, so convert to corresponding Period freq
184198
freq = "M"
185-
rs = ser.resample(freq)
199+
200+
warn = None
201+
if isinstance(ser.index, PeriodIndex):
202+
warn = FutureWarning
203+
msg = "Resampling with a PeriodIndex is deprecated"
204+
with tm.assert_produces_warning(warn, match=msg):
205+
rs = ser.resample(freq)
186206

187207
result = getattr(rs, resample_method)()
188208

@@ -210,7 +230,13 @@ def test_resample_empty_dataframe(empty_frame_dti, freq, resample_method):
210230
elif freq == "ME" and isinstance(df.index, PeriodIndex):
211231
# index is PeriodIndex, so convert to corresponding Period freq
212232
freq = "M"
213-
rs = df.resample(freq, group_keys=False)
233+
234+
warn = None
235+
if isinstance(df.index, PeriodIndex):
236+
warn = FutureWarning
237+
msg = "Resampling with a PeriodIndex is deprecated"
238+
with tm.assert_produces_warning(warn, match=msg):
239+
rs = df.resample(freq, group_keys=False)
214240
result = getattr(rs, resample_method)()
215241
if resample_method == "ohlc":
216242
# TODO: no tests with len(df.columns) > 0
@@ -253,7 +279,14 @@ def test_resample_count_empty_dataframe(freq, empty_frame_dti):
253279
elif freq == "ME" and isinstance(empty_frame_dti.index, PeriodIndex):
254280
# index is PeriodIndex, so convert to corresponding Period freq
255281
freq = "M"
256-
result = empty_frame_dti.resample(freq).count()
282+
283+
warn = None
284+
if isinstance(empty_frame_dti.index, PeriodIndex):
285+
warn = FutureWarning
286+
msg = "Resampling with a PeriodIndex is deprecated"
287+
with tm.assert_produces_warning(warn, match=msg):
288+
rs = empty_frame_dti.resample(freq)
289+
result = rs.count()
257290

258291
index = _asfreq_compat(empty_frame_dti.index, freq)
259292

@@ -280,7 +313,14 @@ def test_resample_size_empty_dataframe(freq, empty_frame_dti):
280313
elif freq == "ME" and isinstance(empty_frame_dti.index, PeriodIndex):
281314
# index is PeriodIndex, so convert to corresponding Period freq
282315
freq = "M"
283-
result = empty_frame_dti.resample(freq).size()
316+
317+
msg = "Resampling with a PeriodIndex"
318+
warn = None
319+
if isinstance(empty_frame_dti.index, PeriodIndex):
320+
warn = FutureWarning
321+
with tm.assert_produces_warning(warn, match=msg):
322+
rs = empty_frame_dti.resample(freq)
323+
result = rs.size()
284324

285325
index = _asfreq_compat(empty_frame_dti.index, freq)
286326

@@ -298,12 +338,21 @@ def test_resample_size_empty_dataframe(freq, empty_frame_dti):
298338
],
299339
)
300340
@pytest.mark.parametrize("dtype", [float, int, object, "datetime64[ns]"])
341+
@pytest.mark.filterwarnings(r"ignore:PeriodDtype\[B\] is deprecated:FutureWarning")
301342
def test_resample_empty_dtypes(index, dtype, resample_method):
302343
# Empty series were sometimes causing a segfault (for the functions
303344
# with Cython bounds-checking disabled) or an IndexError. We just run
304345
# them to ensure they no longer do. (GH #10228)
346+
warn = None
347+
if isinstance(index, PeriodIndex):
348+
# GH#53511
349+
index = PeriodIndex([], freq="B", name=index.name)
350+
warn = FutureWarning
351+
msg = "Resampling with a PeriodIndex is deprecated"
352+
305353
empty_series_dti = Series([], index, dtype)
306-
rs = empty_series_dti.resample("d", group_keys=False)
354+
with tm.assert_produces_warning(warn, match=msg):
355+
rs = empty_series_dti.resample("d", group_keys=False)
307356
try:
308357
getattr(rs, resample_method)()
309358
except DataError:
@@ -329,8 +378,18 @@ def test_apply_to_empty_series(empty_series_dti, freq):
329378
elif freq == "ME" and isinstance(empty_series_dti.index, PeriodIndex):
330379
# index is PeriodIndex, so convert to corresponding Period freq
331380
freq = "M"
332-
result = ser.resample(freq, group_keys=False).apply(lambda x: 1)
333-
expected = ser.resample(freq).apply("sum")
381+
382+
msg = "Resampling with a PeriodIndex"
383+
warn = None
384+
if isinstance(empty_series_dti.index, PeriodIndex):
385+
warn = FutureWarning
386+
387+
with tm.assert_produces_warning(warn, match=msg):
388+
rs = ser.resample(freq, group_keys=False)
389+
390+
result = rs.apply(lambda x: 1)
391+
with tm.assert_produces_warning(warn, match=msg):
392+
expected = ser.resample(freq).apply("sum")
334393

335394
tm.assert_series_equal(result, expected, check_dtype=False)
336395

@@ -340,8 +399,16 @@ def test_resampler_is_iterable(series):
340399
# GH 15314
341400
freq = "h"
342401
tg = Grouper(freq=freq, convention="start")
343-
grouped = series.groupby(tg)
344-
resampled = series.resample(freq)
402+
msg = "Resampling with a PeriodIndex"
403+
warn = None
404+
if isinstance(series.index, PeriodIndex):
405+
warn = FutureWarning
406+
407+
with tm.assert_produces_warning(warn, match=msg):
408+
grouped = series.groupby(tg)
409+
410+
with tm.assert_produces_warning(warn, match=msg):
411+
resampled = series.resample(freq)
345412
for (rk, rv), (gk, gv) in zip(resampled, grouped):
346413
assert rk == gk
347414
tm.assert_series_equal(rv, gv)
@@ -353,6 +420,12 @@ def test_resample_quantile(series):
353420
ser = series
354421
q = 0.75
355422
freq = "h"
356-
result = ser.resample(freq).quantile(q)
357-
expected = ser.resample(freq).agg(lambda x: x.quantile(q)).rename(ser.name)
423+
424+
msg = "Resampling with a PeriodIndex"
425+
warn = None
426+
if isinstance(series.index, PeriodIndex):
427+
warn = FutureWarning
428+
with tm.assert_produces_warning(warn, match=msg):
429+
result = ser.resample(freq).quantile(q)
430+
expected = ser.resample(freq).agg(lambda x: x.quantile(q)).rename(ser.name)
358431
tm.assert_series_equal(result, expected)

pandas/tests/resample/test_datetime_index.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ def test_resample_basic_grouper(series, unit):
184184
tm.assert_series_equal(result, expected)
185185

186186

187+
@pytest.mark.filterwarnings(
188+
"ignore:The 'convention' keyword in Series.resample:FutureWarning"
189+
)
187190
@pytest.mark.parametrize(
188191
"_index_start,_index_end,_index_name",
189192
[("1/1/2000 00:00:00", "1/1/2000 00:13:00", "index")],
@@ -1055,7 +1058,10 @@ def test_period_with_agg():
10551058
)
10561059

10571060
expected = s2.to_timestamp().resample("D").mean().to_period()
1058-
result = s2.resample("D").agg(lambda x: x.mean())
1061+
msg = "Resampling with a PeriodIndex is deprecated"
1062+
with tm.assert_produces_warning(FutureWarning, match=msg):
1063+
rs = s2.resample("D")
1064+
result = rs.agg(lambda x: x.mean())
10591065
tm.assert_series_equal(result, expected)
10601066

10611067

0 commit comments

Comments
 (0)