Skip to content

Commit 2ca088d

Browse files
[backport 2.3.x] DEPR: remove the Period resampling deprecation (#62480) (#62485)
1 parent 92bf98f commit 2ca088d

File tree

9 files changed

+94
-187
lines changed

9 files changed

+94
-187
lines changed

doc/source/whatsnew/v0.21.0.rst

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

638-
.. code-block:: ipython
638+
.. ipython:: python
639639
640-
In [1]: pi = pd.period_range('2017-01', periods=12, freq='M')
640+
pi = pd.period_range('2017-01', periods=12, freq='M')
641641
642-
In [2]: s = pd.Series(np.arange(12), index=pi)
642+
s = pd.Series(np.arange(12), index=pi)
643643
644-
In [3]: resampled = s.resample('2Q').mean()
644+
resampled = s.resample('2Q').mean()
645645
646-
In [4]: resampled
647-
Out[4]:
648-
2017Q1 2.5
649-
2017Q3 8.5
650-
Freq: 2Q-DEC, dtype: float64
646+
resampled
651647
652-
In [5]: resampled.index
653-
Out[5]: PeriodIndex(['2017Q1', '2017Q3'], dtype='period[2Q-DEC]')
648+
resampled.index
654649
655650
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.
656651

doc/source/whatsnew/v2.2.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ Other Deprecations
662662
- Deprecated :meth:`DatetimeArray.__init__` and :meth:`TimedeltaArray.__init__`, use :func:`array` instead (:issue:`55623`)
663663
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
664664
- Deprecated :meth:`Series.ravel`, the underlying array is already 1D, so ravel is not necessary (:issue:`52511`)
665-
- 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`)
665+
- 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`). Note: this deprecation was later undone in pandas 2.3.3 (:issue:`57033`)
666666
- Deprecated :meth:`Series.view`, use :meth:`Series.astype` instead to change the dtype (:issue:`20251`)
667667
- Deprecated :meth:`offsets.Tick.is_anchored`, use ``False`` instead (:issue:`55388`)
668668
- Deprecated ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock``, use public APIs instead (:issue:`55139`)

doc/source/whatsnew/v2.3.3.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ Bug fixes
5757

5858
- The :meth:`DataFrame.iloc` now works correctly with ``copy_on_write`` option when assigning values after subsetting the columns of a homogeneous DataFrame (:issue:`60309`)
5959

60+
Other changes
61+
~~~~~~~~~~~~~
62+
63+
- The deprecation of using :meth:`Series.resample` and :meth:`DataFrame.resample`
64+
with a :class:`PeriodIndex` (and the 'convention' keyword) has been undone.
65+
Resampling with a :class:`PeriodIndex` is supported again, but a subset of
66+
methods that return incorrect results will raise an error in pandas 3.0 (:issue:`57033`)
67+
6068

6169
Other Bug fixes
6270
~~~~~~~~~~~~~~~~

pandas/core/generic.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9415,7 +9415,7 @@ def resample(
94159415
axis: Axis | lib.NoDefault = lib.no_default,
94169416
closed: Literal["right", "left"] | None = None,
94179417
label: Literal["right", "left"] | None = None,
9418-
convention: Literal["start", "end", "s", "e"] | lib.NoDefault = lib.no_default,
9418+
convention: Literal["start", "end", "s", "e"] = "start",
94199419
kind: Literal["timestamp", "period"] | None | lib.NoDefault = lib.no_default,
94209420
on: Level | None = None,
94219421
level: Level | None = None,
@@ -9454,8 +9454,6 @@ def resample(
94549454
For `PeriodIndex` only, controls whether to use the start or
94559455
end of `rule`.
94569456
9457-
.. deprecated:: 2.2.0
9458-
Convert PeriodIndex to DatetimeIndex before resampling instead.
94599457
kind : {{'timestamp', 'period'}}, optional, default None
94609458
Pass 'timestamp' to convert the resulting index to a
94619459
`DateTimeIndex` or 'period' to convert it to a `PeriodIndex`.
@@ -9620,6 +9618,55 @@ def resample(
96209618
2000-01-01 00:06:00 26
96219619
Freq: 3min, dtype: int64
96229620
9621+
For a Series with a PeriodIndex, the keyword `convention` can be
9622+
used to control whether to use the start or end of `rule`.
9623+
9624+
Resample a year by quarter using 'start' `convention`. Values are
9625+
assigned to the first quarter of the period.
9626+
9627+
>>> s = pd.Series(
9628+
... [1, 2], index=pd.period_range("2012-01-01", freq="Y", periods=2)
9629+
... )
9630+
>>> s
9631+
2012 1
9632+
2013 2
9633+
Freq: Y-DEC, dtype: int64
9634+
>>> s.resample("Q", convention="start").asfreq()
9635+
2012Q1 1.0
9636+
2012Q2 NaN
9637+
2012Q3 NaN
9638+
2012Q4 NaN
9639+
2013Q1 2.0
9640+
2013Q2 NaN
9641+
2013Q3 NaN
9642+
2013Q4 NaN
9643+
Freq: Q-DEC, dtype: float64
9644+
9645+
Resample quarters by month using 'end' `convention`. Values are
9646+
assigned to the last month of the period.
9647+
9648+
>>> q = pd.Series(
9649+
... [1, 2, 3, 4], index=pd.period_range("2018-01-01", freq="Q", periods=4)
9650+
... )
9651+
>>> q
9652+
2018Q1 1
9653+
2018Q2 2
9654+
2018Q3 3
9655+
2018Q4 4
9656+
Freq: Q-DEC, dtype: int64
9657+
>>> q.resample("M", convention="end").asfreq()
9658+
2018-03 1.0
9659+
2018-04 NaN
9660+
2018-05 NaN
9661+
2018-06 2.0
9662+
2018-07 NaN
9663+
2018-08 NaN
9664+
2018-09 3.0
9665+
2018-10 NaN
9666+
2018-11 NaN
9667+
2018-12 4.0
9668+
Freq: M, dtype: float64
9669+
96239670
For DataFrame objects, the keyword `on` can be used to specify the
96249671
column instead of the index for resampling.
96259672
@@ -9784,18 +9831,6 @@ def resample(
97849831
else:
97859832
kind = None
97869833

9787-
if convention is not lib.no_default:
9788-
warnings.warn(
9789-
f"The 'convention' keyword in {type(self).__name__}.resample is "
9790-
"deprecated and will be removed in a future version. "
9791-
"Explicitly cast PeriodIndex to DatetimeIndex before resampling "
9792-
"instead.",
9793-
FutureWarning,
9794-
stacklevel=find_stack_level(),
9795-
)
9796-
else:
9797-
convention = "start"
9798-
97999834
return get_resampler(
98009835
cast("Series | DataFrame", self),
98019836
freq=rule,

pandas/core/resample.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,12 +1882,6 @@ class PeriodIndexResampler(DatetimeIndexResampler):
18821882

18831883
@property
18841884
def _resampler_for_grouping(self):
1885-
warnings.warn(
1886-
"Resampling a groupby with a PeriodIndex is deprecated. "
1887-
"Cast to DatetimeIndex before resampling instead.",
1888-
FutureWarning,
1889-
stacklevel=find_stack_level(),
1890-
)
18911885
return PeriodIndexResamplerGroupby
18921886

18931887
def _get_binner_for_time(self):
@@ -2237,15 +2231,7 @@ def _get_resampler(self, obj: NDFrame, kind=None) -> Resampler:
22372231
gpr_index=ax,
22382232
)
22392233
elif isinstance(ax, PeriodIndex) or kind == "period":
2240-
if isinstance(ax, PeriodIndex):
2241-
# GH#53481
2242-
warnings.warn(
2243-
"Resampling with a PeriodIndex is deprecated. "
2244-
"Cast index to DatetimeIndex before resampling instead.",
2245-
FutureWarning,
2246-
stacklevel=find_stack_level(),
2247-
)
2248-
else:
2234+
if not isinstance(ax, PeriodIndex):
22492235
warnings.warn(
22502236
"Resampling with kind='period' is deprecated. "
22512237
"Use datetime paths instead.",

pandas/plotting/_matplotlib/timeseries.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,9 @@ 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-
# 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)
89+
how = "last"
90+
series = getattr(series.resample("D"), how)().dropna()
91+
series = getattr(series.resample(ax_freq), how)().dropna()
9592
freq = ax_freq
9693
elif is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq):
9794
_upsample_others(ax, freq, kwargs)

pandas/tests/resample/test_base.py

Lines changed: 15 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,8 @@ def test_asfreq_fill_value(series, create_index):
8787
def test_resample_interpolate(frame):
8888
# GH#12925
8989
df = frame
90-
warn = None
91-
if isinstance(df.index, PeriodIndex):
92-
warn = FutureWarning
93-
msg = "Resampling with a PeriodIndex is deprecated"
94-
with tm.assert_produces_warning(warn, match=msg):
95-
result = df.resample("1min").asfreq().interpolate()
96-
expected = df.resample("1min").interpolate()
90+
result = df.resample("1min").asfreq().interpolate()
91+
expected = df.resample("1min").interpolate()
9792
tm.assert_frame_equal(result, expected)
9893

9994

@@ -125,13 +120,7 @@ def test_resample_empty_series(freq, empty_series_dti, resample_method):
125120
elif freq == "ME" and isinstance(ser.index, PeriodIndex):
126121
# index is PeriodIndex, so convert to corresponding Period freq
127122
freq = "M"
128-
129-
warn = None
130-
if isinstance(ser.index, PeriodIndex):
131-
warn = FutureWarning
132-
msg = "Resampling with a PeriodIndex is deprecated"
133-
with tm.assert_produces_warning(warn, match=msg):
134-
rs = ser.resample(freq)
123+
rs = ser.resample(freq)
135124
result = getattr(rs, resample_method)()
136125

137126
if resample_method == "ohlc":
@@ -189,9 +178,7 @@ def test_resample_nat_index_series(freq, series, resample_method):
189178
ser = series.copy()
190179
ser.index = PeriodIndex([NaT] * len(ser), freq=freq)
191180

192-
msg = "Resampling with a PeriodIndex is deprecated"
193-
with tm.assert_produces_warning(FutureWarning, match=msg):
194-
rs = ser.resample(freq)
181+
rs = ser.resample(freq)
195182
result = getattr(rs, resample_method)()
196183

197184
if resample_method == "ohlc":
@@ -223,13 +210,7 @@ def test_resample_count_empty_series(freq, empty_series_dti, resample_method):
223210
elif freq == "ME" and isinstance(ser.index, PeriodIndex):
224211
# index is PeriodIndex, so convert to corresponding Period freq
225212
freq = "M"
226-
227-
warn = None
228-
if isinstance(ser.index, PeriodIndex):
229-
warn = FutureWarning
230-
msg = "Resampling with a PeriodIndex is deprecated"
231-
with tm.assert_produces_warning(warn, match=msg):
232-
rs = ser.resample(freq)
213+
rs = ser.resample(freq)
233214

234215
result = getattr(rs, resample_method)()
235216

@@ -257,13 +238,7 @@ def test_resample_empty_dataframe(empty_frame_dti, freq, resample_method):
257238
elif freq == "ME" and isinstance(df.index, PeriodIndex):
258239
# index is PeriodIndex, so convert to corresponding Period freq
259240
freq = "M"
260-
261-
warn = None
262-
if isinstance(df.index, PeriodIndex):
263-
warn = FutureWarning
264-
msg = "Resampling with a PeriodIndex is deprecated"
265-
with tm.assert_produces_warning(warn, match=msg):
266-
rs = df.resample(freq, group_keys=False)
241+
rs = df.resample(freq, group_keys=False)
267242
result = getattr(rs, resample_method)()
268243
if resample_method == "ohlc":
269244
# TODO: no tests with len(df.columns) > 0
@@ -306,14 +281,7 @@ def test_resample_count_empty_dataframe(freq, empty_frame_dti):
306281
elif freq == "ME" and isinstance(empty_frame_dti.index, PeriodIndex):
307282
# index is PeriodIndex, so convert to corresponding Period freq
308283
freq = "M"
309-
310-
warn = None
311-
if isinstance(empty_frame_dti.index, PeriodIndex):
312-
warn = FutureWarning
313-
msg = "Resampling with a PeriodIndex is deprecated"
314-
with tm.assert_produces_warning(warn, match=msg):
315-
rs = empty_frame_dti.resample(freq)
316-
result = rs.count()
284+
result = empty_frame_dti.resample(freq).count()
317285

318286
index = _asfreq_compat(empty_frame_dti.index, freq)
319287

@@ -340,14 +308,7 @@ def test_resample_size_empty_dataframe(freq, empty_frame_dti):
340308
elif freq == "ME" and isinstance(empty_frame_dti.index, PeriodIndex):
341309
# index is PeriodIndex, so convert to corresponding Period freq
342310
freq = "M"
343-
344-
msg = "Resampling with a PeriodIndex"
345-
warn = None
346-
if isinstance(empty_frame_dti.index, PeriodIndex):
347-
warn = FutureWarning
348-
with tm.assert_produces_warning(warn, match=msg):
349-
rs = empty_frame_dti.resample(freq)
350-
result = rs.size()
311+
result = empty_frame_dti.resample(freq).size()
351312

352313
index = _asfreq_compat(empty_frame_dti.index, freq)
353314

@@ -365,21 +326,12 @@ def test_resample_size_empty_dataframe(freq, empty_frame_dti):
365326
],
366327
)
367328
@pytest.mark.parametrize("dtype", [float, int, object, "datetime64[ns]"])
368-
@pytest.mark.filterwarnings(r"ignore:PeriodDtype\[B\] is deprecated:FutureWarning")
369329
def test_resample_empty_dtypes(index, dtype, resample_method):
370330
# Empty series were sometimes causing a segfault (for the functions
371331
# with Cython bounds-checking disabled) or an IndexError. We just run
372332
# them to ensure they no longer do. (GH #10228)
373-
warn = None
374-
if isinstance(index, PeriodIndex):
375-
# GH#53511
376-
index = PeriodIndex([], freq="B", name=index.name)
377-
warn = FutureWarning
378-
msg = "Resampling with a PeriodIndex is deprecated"
379-
380333
empty_series_dti = Series([], index, dtype)
381-
with tm.assert_produces_warning(warn, match=msg):
382-
rs = empty_series_dti.resample("d", group_keys=False)
334+
rs = empty_series_dti.resample("d", group_keys=False)
383335
try:
384336
getattr(rs, resample_method)()
385337
except DataError:
@@ -406,17 +358,8 @@ def test_apply_to_empty_series(empty_series_dti, freq):
406358
# index is PeriodIndex, so convert to corresponding Period freq
407359
freq = "M"
408360

409-
msg = "Resampling with a PeriodIndex"
410-
warn = None
411-
if isinstance(empty_series_dti.index, PeriodIndex):
412-
warn = FutureWarning
413-
414-
with tm.assert_produces_warning(warn, match=msg):
415-
rs = ser.resample(freq, group_keys=False)
416-
417-
result = rs.apply(lambda x: 1)
418-
with tm.assert_produces_warning(warn, match=msg):
419-
expected = ser.resample(freq).apply("sum")
361+
result = ser.resample(freq, group_keys=False).apply(lambda x: 1)
362+
expected = ser.resample(freq).apply("sum")
420363

421364
tm.assert_series_equal(result, expected, check_dtype=False)
422365

@@ -426,16 +369,8 @@ def test_resampler_is_iterable(series):
426369
# GH 15314
427370
freq = "h"
428371
tg = Grouper(freq=freq, convention="start")
429-
msg = "Resampling with a PeriodIndex"
430-
warn = None
431-
if isinstance(series.index, PeriodIndex):
432-
warn = FutureWarning
433-
434-
with tm.assert_produces_warning(warn, match=msg):
435-
grouped = series.groupby(tg)
436-
437-
with tm.assert_produces_warning(warn, match=msg):
438-
resampled = series.resample(freq)
372+
grouped = series.groupby(tg)
373+
resampled = series.resample(freq)
439374
for (rk, rv), (gk, gv) in zip(resampled, grouped):
440375
assert rk == gk
441376
tm.assert_series_equal(rv, gv)
@@ -448,13 +383,8 @@ def test_resample_quantile(series):
448383
q = 0.75
449384
freq = "h"
450385

451-
msg = "Resampling with a PeriodIndex"
452-
warn = None
453-
if isinstance(series.index, PeriodIndex):
454-
warn = FutureWarning
455-
with tm.assert_produces_warning(warn, match=msg):
456-
result = ser.resample(freq).quantile(q)
457-
expected = ser.resample(freq).agg(lambda x: x.quantile(q)).rename(ser.name)
386+
result = ser.resample(freq).quantile(q)
387+
expected = ser.resample(freq).agg(lambda x: x.quantile(q)).rename(ser.name)
458388
tm.assert_series_equal(result, expected)
459389

460390

pandas/tests/resample/test_datetime_index.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,6 @@ 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-
)
190187
@pytest.mark.parametrize(
191188
"_index_start,_index_end,_index_name",
192189
[("1/1/2000 00:00:00", "1/1/2000 00:13:00", "index")],
@@ -1058,10 +1055,7 @@ def test_period_with_agg():
10581055
)
10591056

10601057
expected = s2.to_timestamp().resample("D").mean().to_period()
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())
1058+
result = s2.resample("D").agg(lambda x: x.mean())
10651059
tm.assert_series_equal(result, expected)
10661060

10671061

0 commit comments

Comments
 (0)