Skip to content

DEPR: replace method/limit keywords #58039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Removal of prior version deprecations/changes
- All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`)
- All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`)
- Removed "freq" keyword from :class:`PeriodArray` constructor, use "dtype" instead (:issue:`52462`)
- Removed deprecated "method" and "limit" keywords from :meth:`Series.replace` and :meth:`DataFrame.replace` (:issue:`53492`)
- Removed the "closed" and "normalize" keywords in :meth:`DatetimeIndex.__new__` (:issue:`52628`)
- Removed the "closed" and "unit" keywords in :meth:`TimedeltaIndex.__new__` (:issue:`52628`, :issue:`55499`)
- All arguments in :meth:`Index.sort_values` are now keyword only (:issue:`56493`)
Expand Down
1 change: 0 additions & 1 deletion pandas/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ def pytest_collection_modifyitems(items, config) -> None:
("is_categorical_dtype", "is_categorical_dtype is deprecated"),
("is_sparse", "is_sparse is deprecated"),
("DataFrameGroupBy.fillna", "DataFrameGroupBy.fillna is deprecated"),
("NDFrame.replace", "The 'method' keyword"),
("NDFrame.replace", "Series.replace without 'value'"),
("NDFrame.clip", "Downcasting behavior in Series and DataFrame methods"),
("Series.idxmin", "The behavior of Series.idxmin"),
Expand Down
48 changes: 6 additions & 42 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7285,9 +7285,7 @@ def replace(
value=...,
*,
inplace: Literal[False] = ...,
limit: int | None = ...,
regex: bool = ...,
method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ...,
) -> Self: ...

@overload
Expand All @@ -7297,9 +7295,7 @@ def replace(
value=...,
*,
inplace: Literal[True],
limit: int | None = ...,
regex: bool = ...,
method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ...,
) -> None: ...

@overload
Expand All @@ -7309,9 +7305,7 @@ def replace(
value=...,
*,
inplace: bool = ...,
limit: int | None = ...,
regex: bool = ...,
method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ...,
) -> Self | None: ...

@final
Expand All @@ -7326,32 +7320,9 @@ def replace(
value=lib.no_default,
*,
inplace: bool = False,
limit: int | None = None,
regex: bool = False,
method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = lib.no_default,
) -> Self | None:
if method is not lib.no_default:
warnings.warn(
# GH#33302
f"The 'method' keyword in {type(self).__name__}.replace is "
"deprecated and will be removed in a future version.",
FutureWarning,
stacklevel=find_stack_level(),
)
elif limit is not None:
warnings.warn(
# GH#33302
f"The 'limit' keyword in {type(self).__name__}.replace is "
"deprecated and will be removed in a future version.",
FutureWarning,
stacklevel=find_stack_level(),
)
if (
value is lib.no_default
and method is lib.no_default
and not is_dict_like(to_replace)
and regex is False
):
if value is lib.no_default and not is_dict_like(to_replace) and regex is False:
# case that goes through _replace_single and defaults to method="pad"
warnings.warn(
# GH#33302
Expand Down Expand Up @@ -7387,14 +7358,11 @@ def replace(
if not is_bool(regex) and to_replace is not None:
raise ValueError("'to_replace' must be 'None' if 'regex' is not a bool")

if value is lib.no_default or method is not lib.no_default:
if value is lib.no_default:
# GH#36984 if the user explicitly passes value=None we want to
# respect that. We have the corner case where the user explicitly
# passes value=None *and* a method, which we interpret as meaning
# they want the (documented) default behavior.
if method is lib.no_default:
# TODO: get this to show up as the default in the docs?
method = "pad"

# passing a single value that is scalar like
# when value is None (GH5319), for compat
Expand All @@ -7408,12 +7376,12 @@ def replace(

result = self.apply(
Series._replace_single,
args=(to_replace, method, inplace, limit),
args=(to_replace, inplace),
)
if inplace:
return None
return result
return self._replace_single(to_replace, method, inplace, limit)
return self._replace_single(to_replace, inplace)

if not is_dict_like(to_replace):
if not is_dict_like(regex):
Expand Down Expand Up @@ -7458,9 +7426,7 @@ def replace(
else:
to_replace, value = keys, values

return self.replace(
to_replace, value, inplace=inplace, limit=limit, regex=regex
)
return self.replace(to_replace, value, inplace=inplace, regex=regex)
else:
# need a non-zero len on all axes
if not self.size:
Expand Down Expand Up @@ -7524,9 +7490,7 @@ def replace(
f"or a list or dict of strings or regular expressions, "
f"you passed a {type(regex).__name__!r}"
)
return self.replace(
regex, value, inplace=inplace, limit=limit, regex=True
)
return self.replace(regex, value, inplace=inplace, regex=True)
else:
# dest iterable dict-like
if is_dict_like(value): # NA -> {'A' : 0, 'B' : -1}
Expand Down
18 changes: 6 additions & 12 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -5113,28 +5113,22 @@ def info(
)

@overload
def _replace_single(
self, to_replace, method: str, inplace: Literal[False], limit
) -> Self: ...
def _replace_single(self, to_replace, inplace: Literal[False]) -> Self: ...

@overload
def _replace_single(
self, to_replace, method: str, inplace: Literal[True], limit
) -> None: ...
def _replace_single(self, to_replace, inplace: Literal[True]) -> None: ...

@overload
def _replace_single(
self, to_replace, method: str, inplace: bool, limit
) -> Self | None: ...
def _replace_single(self, to_replace, inplace: bool) -> Self | None: ...

# TODO(3.0): this can be removed once GH#33302 deprecation is enforced
def _replace_single(
self, to_replace, method: str, inplace: bool, limit
) -> Self | None:
def _replace_single(self, to_replace, inplace: bool) -> Self | None:
"""
Replaces values in a Series using the fill method specified when no
replacement value is given in the replace method
"""
limit = None
method = "pad"

result = self if inplace else self.copy()

Expand Down
19 changes: 1 addition & 18 deletions pandas/core/shared_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,20 +429,11 @@
filled). Regular expressions, strings and lists or dicts of such
objects are also allowed.
{inplace}
limit : int, default None
Maximum size gap to forward or backward fill.

.. deprecated:: 2.1.0
regex : bool or same types as `to_replace`, default False
Whether to interpret `to_replace` and/or `value` as regular
expressions. Alternatively, this could be a regular expression or a
list, dict, or array of regular expressions in which case
`to_replace` must be ``None``.
method : {{'pad', 'ffill', 'bfill'}}
The method to use when for replacement, when `to_replace` is a
scalar, list or tuple and `value` is ``None``.

.. deprecated:: 2.1.0

Returns
-------
Expand Down Expand Up @@ -538,14 +529,6 @@
3 1 8 d
4 4 9 e

>>> s.replace([1, 2], method='bfill')
0 3
1 3
2 3
3 4
4 5
dtype: int64

**dict-like `to_replace`**

>>> df.replace({{0: 10, 1: 100}})
Expand Down Expand Up @@ -615,7 +598,7 @@
When one uses a dict as the `to_replace` value, it is like the
value(s) in the dict are equal to the `value` parameter.
``s.replace({{'a': None}})`` is equivalent to
``s.replace(to_replace={{'a': None}}, value=None, method=None)``:
``s.replace(to_replace={{'a': None}}, value=None)``:

>>> s.replace({{'a': None}})
0 10
Expand Down
42 changes: 0 additions & 42 deletions pandas/tests/frame/methods/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,48 +1171,6 @@ def test_replace_with_empty_dictlike(self, mix_abc):
tm.assert_frame_equal(df, df.replace({"b": {}}))
tm.assert_frame_equal(df, df.replace(Series({"b": {}})))

@pytest.mark.parametrize(
"to_replace, method, expected",
[
(0, "bfill", {"A": [1, 1, 2], "B": [5, np.nan, 7], "C": ["a", "b", "c"]}),
(
np.nan,
"bfill",
{"A": [0, 1, 2], "B": [5.0, 7.0, 7.0], "C": ["a", "b", "c"]},
),
("d", "ffill", {"A": [0, 1, 2], "B": [5, np.nan, 7], "C": ["a", "b", "c"]}),
(
[0, 2],
"bfill",
{"A": [1, 1, 2], "B": [5, np.nan, 7], "C": ["a", "b", "c"]},
),
(
[1, 2],
"pad",
{"A": [0, 0, 0], "B": [5, np.nan, 7], "C": ["a", "b", "c"]},
),
(
(1, 2),
"bfill",
{"A": [0, 2, 2], "B": [5, np.nan, 7], "C": ["a", "b", "c"]},
),
(
["b", "c"],
"ffill",
{"A": [0, 1, 2], "B": [5, np.nan, 7], "C": ["a", "a", "a"]},
),
],
)
def test_replace_method(self, to_replace, method, expected):
# GH 19632
df = DataFrame({"A": [0, 1, 2], "B": [5, np.nan, 7], "C": ["a", "b", "c"]})

msg = "The 'method' keyword in DataFrame.replace is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
result = df.replace(to_replace=to_replace, value=None, method=method)
expected = DataFrame(expected)
tm.assert_frame_equal(result, expected)

@pytest.mark.parametrize(
"replace_dict, final_data",
[({"a": 1, "b": 1}, [[3, 3], [2, 2]]), ({"a": 1, "b": 2}, [[3, 1], [2, 3]])],
Expand Down
12 changes: 0 additions & 12 deletions pandas/tests/frame/test_subclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,18 +742,6 @@ def test_equals_subclass(self):
assert df1.equals(df2)
assert df2.equals(df1)

def test_replace_list_method(self):
# https://github.com/pandas-dev/pandas/pull/46018
df = tm.SubclassedDataFrame({"A": [0, 1, 2]})
msg = "The 'method' keyword in SubclassedDataFrame.replace is deprecated"
with tm.assert_produces_warning(
FutureWarning, match=msg, raise_on_extra_warnings=False
):
result = df.replace([1, 2], method="ffill")
expected = tm.SubclassedDataFrame({"A": [0, 0, 0]})
assert isinstance(result, tm.SubclassedDataFrame)
tm.assert_frame_equal(result, expected)


class MySubclassWithMetadata(DataFrame):
_metadata = ["my_metadata"]
Expand Down
69 changes: 0 additions & 69 deletions pandas/tests/series/methods/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,6 @@ def test_replace_with_single_list(self):
assert return_value is None
tm.assert_series_equal(s, pd.Series([0, 0, 0, 0, 4]))

# make sure things don't get corrupted when fillna call fails
s = ser.copy()
msg = (
r"Invalid fill method\. Expecting pad \(ffill\) or backfill "
r"\(bfill\)\. Got crash_cymbal"
)
msg3 = "The 'method' keyword in Series.replace is deprecated"
with pytest.raises(ValueError, match=msg):
with tm.assert_produces_warning(FutureWarning, match=msg3):
return_value = s.replace([1, 2, 3], inplace=True, method="crash_cymbal")
assert return_value is None
tm.assert_series_equal(s, ser)

def test_replace_mixed_types(self):
ser = pd.Series(np.arange(5), dtype="int64")

Expand Down Expand Up @@ -550,62 +537,6 @@ def test_replace_extension_other(self, frame_or_series):
# should not have changed dtype
tm.assert_equal(obj, result)

def _check_replace_with_method(self, ser: pd.Series):
df = ser.to_frame()

msg1 = "The 'method' keyword in Series.replace is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg1):
res = ser.replace(ser[1], method="pad")
expected = pd.Series([ser[0], ser[0]] + list(ser[2:]), dtype=ser.dtype)
tm.assert_series_equal(res, expected)

msg2 = "The 'method' keyword in DataFrame.replace is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg2):
res_df = df.replace(ser[1], method="pad")
tm.assert_frame_equal(res_df, expected.to_frame())

ser2 = ser.copy()
with tm.assert_produces_warning(FutureWarning, match=msg1):
res2 = ser2.replace(ser[1], method="pad", inplace=True)
assert res2 is None
tm.assert_series_equal(ser2, expected)

with tm.assert_produces_warning(FutureWarning, match=msg2):
res_df2 = df.replace(ser[1], method="pad", inplace=True)
assert res_df2 is None
tm.assert_frame_equal(df, expected.to_frame())

def test_replace_ea_dtype_with_method(self, any_numeric_ea_dtype):
arr = pd.array([1, 2, pd.NA, 4], dtype=any_numeric_ea_dtype)
ser = pd.Series(arr)

self._check_replace_with_method(ser)

@pytest.mark.parametrize("as_categorical", [True, False])
def test_replace_interval_with_method(self, as_categorical):
# in particular interval that can't hold NA

idx = pd.IntervalIndex.from_breaks(range(4))
ser = pd.Series(idx)
if as_categorical:
ser = ser.astype("category")

self._check_replace_with_method(ser)

@pytest.mark.parametrize("as_period", [True, False])
@pytest.mark.parametrize("as_categorical", [True, False])
def test_replace_datetimelike_with_method(self, as_period, as_categorical):
idx = pd.date_range("2016-01-01", periods=5, tz="US/Pacific")
if as_period:
idx = idx.tz_localize(None).to_period("D")

ser = pd.Series(idx)
ser.iloc[-2] = pd.NaT
if as_categorical:
ser = ser.astype("category")

self._check_replace_with_method(ser)

def test_replace_with_compiled_regex(self):
# https://github.com/pandas-dev/pandas/issues/35680
s = pd.Series(["a", "b", "c"])
Expand Down