Skip to content

Commit b89b2f1

Browse files
authored
CLN: Enforce deprecation of method and limit in pct_change methods (#57742)
* CLN: Enforce deprecation of method and limit in pct_change methods * Test fixups * mypy fixup
1 parent 5c1303a commit b89b2f1

File tree

7 files changed

+71
-307
lines changed

7 files changed

+71
-307
lines changed

Diff for: doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ Removal of prior version deprecations/changes
228228
- Removed ``read_gbq`` and ``DataFrame.to_gbq``. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
229229
- Removed ``use_nullable_dtypes`` from :func:`read_parquet` (:issue:`51853`)
230230
- Removed ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
231+
- Removed argument ``limit`` from :meth:`DataFrame.pct_change`, :meth:`Series.pct_change`, :meth:`.DataFrameGroupBy.pct_change`, and :meth:`.SeriesGroupBy.pct_change`; the argument ``method`` must be set to ``None`` and will be removed in a future version of pandas (:issue:`53520`)
231232
- Removed deprecated argument ``obj`` in :meth:`.DataFrameGroupBy.get_group` and :meth:`.SeriesGroupBy.get_group` (:issue:`53545`)
232233
- Removed deprecated behavior of :meth:`Series.agg` using :meth:`Series.apply` (:issue:`53325`)
233234
- Removed option ``mode.use_inf_as_na``, convert inf entries to ``NaN`` before instead (:issue:`51684`)

Diff for: pandas/core/generic.py

+8-48
Original file line numberDiff line numberDiff line change
@@ -11122,8 +11122,7 @@ def describe(
1112211122
def pct_change(
1112311123
self,
1112411124
periods: int = 1,
11125-
fill_method: FillnaOptions | None | lib.NoDefault = lib.no_default,
11126-
limit: int | None | lib.NoDefault = lib.no_default,
11125+
fill_method: None = None,
1112711126
freq=None,
1112811127
**kwargs,
1112911128
) -> Self:
@@ -11145,17 +11144,12 @@ def pct_change(
1114511144
----------
1114611145
periods : int, default 1
1114711146
Periods to shift for forming percent change.
11148-
fill_method : {'backfill', 'bfill', 'pad', 'ffill', None}, default 'pad'
11149-
How to handle NAs **before** computing percent changes.
11147+
fill_method : None
11148+
Must be None. This argument will be removed in a future version of pandas.
1115011149
1115111150
.. deprecated:: 2.1
1115211151
All options of `fill_method` are deprecated except `fill_method=None`.
1115311152
11154-
limit : int, default None
11155-
The number of consecutive NAs to fill before stopping.
11156-
11157-
.. deprecated:: 2.1
11158-
1115911153
freq : DateOffset, timedelta, or str, optional
1116011154
Increment to use from time series API (e.g. 'ME' or BDay()).
1116111155
**kwargs
@@ -11262,52 +11256,18 @@ def pct_change(
1126211256
APPL -0.252395 -0.011860 NaN
1126311257
"""
1126411258
# GH#53491
11265-
if fill_method not in (lib.no_default, None) or limit is not lib.no_default:
11266-
warnings.warn(
11267-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
11268-
f"{type(self).__name__}.pct_change are deprecated and will be removed "
11269-
"in a future version. Either fill in any non-leading NA values prior "
11270-
"to calling pct_change or specify 'fill_method=None' to not fill NA "
11271-
"values.",
11272-
FutureWarning,
11273-
stacklevel=find_stack_level(),
11274-
)
11275-
if fill_method is lib.no_default:
11276-
if limit is lib.no_default:
11277-
cols = self.items() if self.ndim == 2 else [(None, self)]
11278-
for _, col in cols:
11279-
if len(col) > 0:
11280-
mask = col.isna().values
11281-
mask = mask[np.argmax(~mask) :]
11282-
if mask.any():
11283-
warnings.warn(
11284-
"The default fill_method='pad' in "
11285-
f"{type(self).__name__}.pct_change is deprecated and "
11286-
"will be removed in a future version. Either fill in "
11287-
"any non-leading NA values prior to calling pct_change "
11288-
"or specify 'fill_method=None' to not fill NA values.",
11289-
FutureWarning,
11290-
stacklevel=find_stack_level(),
11291-
)
11292-
break
11293-
fill_method = "pad"
11294-
if limit is lib.no_default:
11295-
limit = None
11259+
if fill_method is not None:
11260+
raise ValueError(f"fill_method must be None; got {fill_method=}.")
1129611261

1129711262
axis = self._get_axis_number(kwargs.pop("axis", "index"))
11298-
if fill_method is None:
11299-
data = self
11300-
else:
11301-
data = self._pad_or_backfill(fill_method, axis=axis, limit=limit)
11302-
11303-
shifted = data.shift(periods=periods, freq=freq, axis=axis, **kwargs)
11263+
shifted = self.shift(periods=periods, freq=freq, axis=axis, **kwargs)
1130411264
# Unsupported left operand type for / ("Self")
11305-
rs = data / shifted - 1 # type: ignore[operator]
11265+
rs = self / shifted - 1 # type: ignore[operator]
1130611266
if freq is not None:
1130711267
# Shift method is implemented differently when freq is not None
1130811268
# We want to restore the original index
1130911269
rs = rs.loc[~rs.index.duplicated()]
11310-
rs = rs.reindex_like(data)
11270+
rs = rs.reindex_like(self)
1131111271
return rs.__finalize__(self, method="pct_change")
1131211272

1131311273
@final

Diff for: pandas/core/groupby/groupby.py

+11-46
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class providing the base-class of operations.
4545
AnyArrayLike,
4646
ArrayLike,
4747
DtypeObj,
48-
FillnaOptions,
4948
IndexLabel,
5049
IntervalClosedType,
5150
NDFrameT,
@@ -5147,8 +5146,7 @@ def diff(
51475146
def pct_change(
51485147
self,
51495148
periods: int = 1,
5150-
fill_method: FillnaOptions | None | lib.NoDefault = lib.no_default,
5151-
limit: int | None | lib.NoDefault = lib.no_default,
5149+
fill_method: None = None,
51525150
freq=None,
51535151
):
51545152
"""
@@ -5161,19 +5159,11 @@ def pct_change(
51615159
a period of 1 means adjacent elements are compared, whereas a period
51625160
of 2 compares every other element.
51635161
5164-
fill_method : FillnaOptions or None, default None
5165-
Specifies how to handle missing values after the initial shift
5166-
operation necessary for percentage change calculation. Users are
5167-
encouraged to handle missing values manually in future versions.
5168-
Valid options are:
5169-
- A FillnaOptions value ('ffill', 'bfill') for forward or backward filling.
5170-
- None to avoid filling.
5171-
Note: Usage is discouraged due to impending deprecation.
5162+
fill_method : None
5163+
Must be None. This argument will be removed in a future version of pandas.
51725164
5173-
limit : int or None, default None
5174-
The maximum number of consecutive NA values to fill, based on the chosen
5175-
`fill_method`. Address NaN values prior to using `pct_change` as this
5176-
parameter is nearing deprecation.
5165+
.. deprecated:: 2.1
5166+
All options of `fill_method` are deprecated except `fill_method=None`.
51775167
51785168
freq : str, pandas offset object, or None, default None
51795169
The frequency increment for time series data (e.g., 'M' for month-end).
@@ -5227,49 +5217,24 @@ def pct_change(
52275217
goldfish 0.2 0.125
52285218
"""
52295219
# GH#53491
5230-
if fill_method not in (lib.no_default, None) or limit is not lib.no_default:
5231-
warnings.warn(
5232-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
5233-
f"{type(self).__name__}.pct_change are deprecated and will be removed "
5234-
"in a future version. Either fill in any non-leading NA values prior "
5235-
"to calling pct_change or specify 'fill_method=None' to not fill NA "
5236-
"values.",
5237-
FutureWarning,
5238-
stacklevel=find_stack_level(),
5239-
)
5240-
if fill_method is lib.no_default:
5241-
if limit is lib.no_default and any(
5242-
grp.isna().values.any() for _, grp in self
5243-
):
5244-
warnings.warn(
5245-
"The default fill_method='ffill' in "
5246-
f"{type(self).__name__}.pct_change is deprecated and will "
5247-
"be removed in a future version. Either fill in any "
5248-
"non-leading NA values prior to calling pct_change or "
5249-
"specify 'fill_method=None' to not fill NA values.",
5250-
FutureWarning,
5251-
stacklevel=find_stack_level(),
5252-
)
5253-
fill_method = "ffill"
5254-
if limit is lib.no_default:
5255-
limit = None
5220+
if fill_method is not None:
5221+
raise ValueError(f"fill_method must be None; got {fill_method=}.")
52565222

52575223
# TODO(GH#23918): Remove this conditional for SeriesGroupBy when
52585224
# GH#23918 is fixed
52595225
if freq is not None:
52605226
f = lambda x: x.pct_change(
52615227
periods=periods,
5262-
fill_method=fill_method,
5263-
limit=limit,
52645228
freq=freq,
52655229
axis=0,
52665230
)
52675231
return self._python_apply_general(f, self._selected_obj, is_transform=True)
52685232

52695233
if fill_method is None: # GH30463
5270-
fill_method = "ffill"
5271-
limit = 0
5272-
filled = getattr(self, fill_method)(limit=limit)
5234+
op = "ffill"
5235+
else:
5236+
op = fill_method
5237+
filled = getattr(self, op)(limit=0)
52735238
fill_grp = filled.groupby(self._grouper.codes, group_keys=self.group_keys)
52745239
shifted = fill_grp.shift(periods=periods, freq=freq)
52755240
return (filled / shifted) - 1

Diff for: pandas/tests/frame/methods/test_pct_change.py

+32-87
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,17 @@
1010

1111
class TestDataFramePctChange:
1212
@pytest.mark.parametrize(
13-
"periods, fill_method, limit, exp",
13+
"periods, exp",
1414
[
15-
(1, "ffill", None, [np.nan, np.nan, np.nan, 1, 1, 1.5, 0, 0]),
16-
(1, "ffill", 1, [np.nan, np.nan, np.nan, 1, 1, 1.5, 0, np.nan]),
17-
(1, "bfill", None, [np.nan, 0, 0, 1, 1, 1.5, np.nan, np.nan]),
18-
(1, "bfill", 1, [np.nan, np.nan, 0, 1, 1, 1.5, np.nan, np.nan]),
19-
(-1, "ffill", None, [np.nan, np.nan, -0.5, -0.5, -0.6, 0, 0, np.nan]),
20-
(-1, "ffill", 1, [np.nan, np.nan, -0.5, -0.5, -0.6, 0, np.nan, np.nan]),
21-
(-1, "bfill", None, [0, 0, -0.5, -0.5, -0.6, np.nan, np.nan, np.nan]),
22-
(-1, "bfill", 1, [np.nan, 0, -0.5, -0.5, -0.6, np.nan, np.nan, np.nan]),
15+
(1, [np.nan, np.nan, np.nan, 1, 1, 1.5, np.nan, np.nan]),
16+
(-1, [np.nan, np.nan, -0.5, -0.5, -0.6, np.nan, np.nan, np.nan]),
2317
],
2418
)
25-
def test_pct_change_with_nas(
26-
self, periods, fill_method, limit, exp, frame_or_series
27-
):
19+
def test_pct_change_with_nas(self, periods, exp, frame_or_series):
2820
vals = [np.nan, np.nan, 1, 2, 4, 10, np.nan, np.nan]
2921
obj = frame_or_series(vals)
3022

31-
msg = (
32-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
33-
f"{type(obj).__name__}.pct_change are deprecated"
34-
)
35-
with tm.assert_produces_warning(FutureWarning, match=msg):
36-
res = obj.pct_change(periods=periods, fill_method=fill_method, limit=limit)
23+
res = obj.pct_change(periods=periods)
3724
tm.assert_equal(res, frame_or_series(exp))
3825

3926
def test_pct_change_numeric(self):
@@ -45,124 +32,82 @@ def test_pct_change_numeric(self):
4532
pnl.iat[1, 1] = np.nan
4633
pnl.iat[2, 3] = 60
4734

48-
msg = (
49-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
50-
"DataFrame.pct_change are deprecated"
51-
)
52-
5335
for axis in range(2):
54-
expected = pnl.ffill(axis=axis) / pnl.ffill(axis=axis).shift(axis=axis) - 1
55-
56-
with tm.assert_produces_warning(FutureWarning, match=msg):
57-
result = pnl.pct_change(axis=axis, fill_method="pad")
36+
expected = pnl / pnl.shift(axis=axis) - 1
37+
result = pnl.pct_change(axis=axis)
5838
tm.assert_frame_equal(result, expected)
5939

6040
def test_pct_change(self, datetime_frame):
61-
msg = (
62-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
63-
"DataFrame.pct_change are deprecated"
64-
)
65-
66-
rs = datetime_frame.pct_change(fill_method=None)
41+
rs = datetime_frame.pct_change()
6742
tm.assert_frame_equal(rs, datetime_frame / datetime_frame.shift(1) - 1)
6843

6944
rs = datetime_frame.pct_change(2)
7045
filled = datetime_frame.ffill()
7146
tm.assert_frame_equal(rs, filled / filled.shift(2) - 1)
7247

73-
with tm.assert_produces_warning(FutureWarning, match=msg):
74-
rs = datetime_frame.pct_change(fill_method="bfill", limit=1)
75-
filled = datetime_frame.bfill(limit=1)
76-
tm.assert_frame_equal(rs, filled / filled.shift(1) - 1)
48+
rs = datetime_frame.pct_change()
49+
tm.assert_frame_equal(rs, datetime_frame / datetime_frame.shift(1) - 1)
7750

7851
rs = datetime_frame.pct_change(freq="5D")
79-
filled = datetime_frame.ffill()
8052
tm.assert_frame_equal(
81-
rs, (filled / filled.shift(freq="5D") - 1).reindex_like(filled)
53+
rs,
54+
(datetime_frame / datetime_frame.shift(freq="5D") - 1).reindex_like(
55+
datetime_frame
56+
),
8257
)
8358

8459
def test_pct_change_shift_over_nas(self):
8560
s = Series([1.0, 1.5, np.nan, 2.5, 3.0])
8661

8762
df = DataFrame({"a": s, "b": s})
8863

89-
msg = "The default fill_method='pad' in DataFrame.pct_change is deprecated"
90-
with tm.assert_produces_warning(FutureWarning, match=msg):
91-
chg = df.pct_change()
92-
93-
expected = Series([np.nan, 0.5, 0.0, 2.5 / 1.5 - 1, 0.2])
64+
chg = df.pct_change()
65+
expected = Series([np.nan, 0.5, np.nan, np.nan, 0.2])
9466
edf = DataFrame({"a": expected, "b": expected})
9567
tm.assert_frame_equal(chg, edf)
9668

9769
@pytest.mark.parametrize(
98-
"freq, periods, fill_method, limit",
70+
"freq, periods",
9971
[
100-
("5B", 5, None, None),
101-
("3B", 3, None, None),
102-
("3B", 3, "bfill", None),
103-
("7B", 7, "pad", 1),
104-
("7B", 7, "bfill", 3),
105-
("14B", 14, None, None),
72+
("5B", 5),
73+
("3B", 3),
74+
("14B", 14),
10675
],
10776
)
10877
def test_pct_change_periods_freq(
109-
self, datetime_frame, freq, periods, fill_method, limit
78+
self,
79+
datetime_frame,
80+
freq,
81+
periods,
11082
):
111-
msg = (
112-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
113-
"DataFrame.pct_change are deprecated"
114-
)
115-
11683
# GH#7292
117-
with tm.assert_produces_warning(FutureWarning, match=msg):
118-
rs_freq = datetime_frame.pct_change(
119-
freq=freq, fill_method=fill_method, limit=limit
120-
)
121-
with tm.assert_produces_warning(FutureWarning, match=msg):
122-
rs_periods = datetime_frame.pct_change(
123-
periods, fill_method=fill_method, limit=limit
124-
)
84+
rs_freq = datetime_frame.pct_change(freq=freq)
85+
rs_periods = datetime_frame.pct_change(periods)
12586
tm.assert_frame_equal(rs_freq, rs_periods)
12687

12788
empty_ts = DataFrame(index=datetime_frame.index, columns=datetime_frame.columns)
128-
with tm.assert_produces_warning(FutureWarning, match=msg):
129-
rs_freq = empty_ts.pct_change(
130-
freq=freq, fill_method=fill_method, limit=limit
131-
)
132-
with tm.assert_produces_warning(FutureWarning, match=msg):
133-
rs_periods = empty_ts.pct_change(
134-
periods, fill_method=fill_method, limit=limit
135-
)
89+
rs_freq = empty_ts.pct_change(freq=freq)
90+
rs_periods = empty_ts.pct_change(periods)
13691
tm.assert_frame_equal(rs_freq, rs_periods)
13792

13893

139-
@pytest.mark.parametrize("fill_method", ["pad", "ffill", None])
140-
def test_pct_change_with_duplicated_indices(fill_method):
94+
def test_pct_change_with_duplicated_indices():
14195
# GH30463
14296
data = DataFrame(
14397
{0: [np.nan, 1, 2, 3, 9, 18], 1: [0, 1, np.nan, 3, 9, 18]}, index=["a", "b"] * 3
14498
)
14599

146-
warn = None if fill_method is None else FutureWarning
147-
msg = (
148-
"The 'fill_method' keyword being not None and the 'limit' keyword in "
149-
"DataFrame.pct_change are deprecated"
150-
)
151-
with tm.assert_produces_warning(warn, match=msg):
152-
result = data.pct_change(fill_method=fill_method)
100+
result = data.pct_change()
153101

154-
if fill_method is None:
155-
second_column = [np.nan, np.inf, np.nan, np.nan, 2.0, 1.0]
156-
else:
157-
second_column = [np.nan, np.inf, 0.0, 2.0, 2.0, 1.0]
102+
second_column = [np.nan, np.inf, np.nan, np.nan, 2.0, 1.0]
158103
expected = DataFrame(
159104
{0: [np.nan, np.nan, 1.0, 0.5, 2.0, 1.0], 1: second_column},
160105
index=["a", "b"] * 3,
161106
)
162107
tm.assert_frame_equal(result, expected)
163108

164109

165-
def test_pct_change_none_beginning_no_warning():
110+
def test_pct_change_none_beginning():
166111
# GH#54481
167112
df = DataFrame(
168113
[

0 commit comments

Comments
 (0)