diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 2286a75f5d0c5..26dd6f83ad44a 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -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`) diff --git a/pandas/conftest.py b/pandas/conftest.py index 65410c3c09494..34489bb70575a 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -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"), diff --git a/pandas/core/generic.py b/pandas/core/generic.py index a7545fb8d98de..1b9d7f4c81c9f 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -7285,9 +7285,7 @@ def replace( value=..., *, inplace: Literal[False] = ..., - limit: int | None = ..., regex: bool = ..., - method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ..., ) -> Self: ... @overload @@ -7297,9 +7295,7 @@ def replace( value=..., *, inplace: Literal[True], - limit: int | None = ..., regex: bool = ..., - method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ..., ) -> None: ... @overload @@ -7309,9 +7305,7 @@ def replace( value=..., *, inplace: bool = ..., - limit: int | None = ..., regex: bool = ..., - method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ..., ) -> Self | None: ... @final @@ -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 @@ -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 @@ -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): @@ -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: @@ -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} diff --git a/pandas/core/series.py b/pandas/core/series.py index 0761dc17ab147..b0dc05fce7913 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -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() diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py index a2b5439f9e12f..2d8517693a2f8 100644 --- a/pandas/core/shared_docs.py +++ b/pandas/core/shared_docs.py @@ -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 ------- @@ -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}}) @@ -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 diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py index eb6d649c296fc..3b9c342f35a71 100644 --- a/pandas/tests/frame/methods/test_replace.py +++ b/pandas/tests/frame/methods/test_replace.py @@ -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]])], diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 355953eac9d51..7d18ef28a722d 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -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"] diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py index c7b894e73d0dd..09a3469e73462 100644 --- a/pandas/tests/series/methods/test_replace.py +++ b/pandas/tests/series/methods/test_replace.py @@ -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") @@ -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"])