Skip to content

Commit a9359ba

Browse files
DEPR: replace without passing value (pandas-dev#58040)
* DEPR: replace without passing value * update doctest * Update doc/source/whatsnew/v3.0.0.rst Co-authored-by: Matthew Roeschke <[email protected]> --------- Co-authored-by: Matthew Roeschke <[email protected]>
1 parent a6d41de commit a9359ba

File tree

8 files changed

+30
-156
lines changed

8 files changed

+30
-156
lines changed

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ Removal of prior version deprecations/changes
207207
- :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
208208
- All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`)
209209
- All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`)
210+
- Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)
210211
- Disallow non-standard (``np.ndarray``, :class:`Index`, :class:`ExtensionArray`, or :class:`Series`) to :func:`isin`, :func:`unique`, :func:`factorize` (:issue:`52986`)
211212
- Disallow passing a pandas type to :meth:`Index.view` (:issue:`55709`)
212213
- Disallow units other than "s", "ms", "us", "ns" for datetime64 and timedelta64 dtypes in :func:`array` (:issue:`53817`)

pandas/core/arrays/_mixins.py

-7
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,6 @@ def __getitem__(
296296
result = self._from_backing_data(result)
297297
return result
298298

299-
def _fill_mask_inplace(
300-
self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
301-
) -> None:
302-
# (for now) when self.ndim == 2, we assume axis=0
303-
func = missing.get_fill_func(method, ndim=self.ndim)
304-
func(self._ndarray.T, limit=limit, mask=mask.T)
305-
306299
def _pad_or_backfill(
307300
self,
308301
*,

pandas/core/arrays/base.py

-19
Original file line numberDiff line numberDiff line change
@@ -2111,25 +2111,6 @@ def _where(self, mask: npt.NDArray[np.bool_], value) -> Self:
21112111
result[~mask] = val
21122112
return result
21132113

2114-
# TODO(3.0): this can be removed once GH#33302 deprecation is enforced
2115-
def _fill_mask_inplace(
2116-
self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
2117-
) -> None:
2118-
"""
2119-
Replace values in locations specified by 'mask' using pad or backfill.
2120-
2121-
See also
2122-
--------
2123-
ExtensionArray.fillna
2124-
"""
2125-
func = missing.get_fill_func(method)
2126-
npvalues = self.astype(object)
2127-
# NB: if we don't copy mask here, it may be altered inplace, which
2128-
# would mess up the `self[mask] = ...` below.
2129-
func(npvalues, limit=limit, mask=mask.copy())
2130-
new_values = self._from_sequence(npvalues, dtype=self.dtype)
2131-
self[mask] = new_values[mask]
2132-
21332114
def _rank(
21342115
self,
21352116
*,

pandas/core/generic.py

+13-44
Original file line numberDiff line numberDiff line change
@@ -7319,17 +7319,8 @@ def replace(
73197319
inplace: bool = False,
73207320
regex: bool = False,
73217321
) -> Self | None:
7322-
if value is lib.no_default and not is_dict_like(to_replace) and regex is False:
7323-
# case that goes through _replace_single and defaults to method="pad"
7324-
warnings.warn(
7325-
# GH#33302
7326-
f"{type(self).__name__}.replace without 'value' and with "
7327-
"non-dict-like 'to_replace' is deprecated "
7328-
"and will raise in a future version. "
7329-
"Explicitly specify the new values instead.",
7330-
FutureWarning,
7331-
stacklevel=find_stack_level(),
7332-
)
7322+
if not is_bool(regex) and to_replace is not None:
7323+
raise ValueError("'to_replace' must be 'None' if 'regex' is not a bool")
73337324

73347325
if not (
73357326
is_scalar(to_replace)
@@ -7342,6 +7333,15 @@ def replace(
73427333
f"{type(to_replace).__name__!r}"
73437334
)
73447335

7336+
if value is lib.no_default and not (
7337+
is_dict_like(to_replace) or is_dict_like(regex)
7338+
):
7339+
raise ValueError(
7340+
# GH#33302
7341+
f"{type(self).__name__}.replace must specify either 'value', "
7342+
"a dict-like 'to_replace', or dict-like 'regex'."
7343+
)
7344+
73457345
inplace = validate_bool_kwarg(inplace, "inplace")
73467346
if inplace:
73477347
if not PYPY:
@@ -7352,41 +7352,10 @@ def replace(
73527352
stacklevel=2,
73537353
)
73547354

7355-
if not is_bool(regex) and to_replace is not None:
7356-
raise ValueError("'to_replace' must be 'None' if 'regex' is not a bool")
7357-
73587355
if value is lib.no_default:
7359-
# GH#36984 if the user explicitly passes value=None we want to
7360-
# respect that. We have the corner case where the user explicitly
7361-
# passes value=None *and* a method, which we interpret as meaning
7362-
# they want the (documented) default behavior.
7363-
7364-
# passing a single value that is scalar like
7365-
# when value is None (GH5319), for compat
7366-
if not is_dict_like(to_replace) and not is_dict_like(regex):
7367-
to_replace = [to_replace]
7368-
7369-
if isinstance(to_replace, (tuple, list)):
7370-
# TODO: Consider copy-on-write for non-replaced columns's here
7371-
if isinstance(self, ABCDataFrame):
7372-
from pandas import Series
7373-
7374-
result = self.apply(
7375-
Series._replace_single,
7376-
args=(to_replace, inplace),
7377-
)
7378-
if inplace:
7379-
return None
7380-
return result
7381-
return self._replace_single(to_replace, inplace)
7382-
73837356
if not is_dict_like(to_replace):
7384-
if not is_dict_like(regex):
7385-
raise TypeError(
7386-
'If "to_replace" and "value" are both None '
7387-
'and "to_replace" is not a list, then '
7388-
"regex must be a mapping"
7389-
)
7357+
# In this case we have checked above that
7358+
# 1) regex is dict-like and 2) to_replace is None
73907359
to_replace = regex
73917360
regex = True
73927361

pandas/core/series.py

-35
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@
9797
algorithms,
9898
base,
9999
common as com,
100-
missing,
101100
nanops,
102101
ops,
103102
roperator,
@@ -5116,40 +5115,6 @@ def info(
51165115
show_counts=show_counts,
51175116
)
51185117

5119-
@overload
5120-
def _replace_single(self, to_replace, inplace: Literal[False]) -> Self: ...
5121-
5122-
@overload
5123-
def _replace_single(self, to_replace, inplace: Literal[True]) -> None: ...
5124-
5125-
@overload
5126-
def _replace_single(self, to_replace, inplace: bool) -> Self | None: ...
5127-
5128-
# TODO(3.0): this can be removed once GH#33302 deprecation is enforced
5129-
def _replace_single(self, to_replace, inplace: bool) -> Self | None:
5130-
"""
5131-
Replaces values in a Series using the fill method specified when no
5132-
replacement value is given in the replace method
5133-
"""
5134-
limit = None
5135-
method = "pad"
5136-
5137-
result = self if inplace else self.copy()
5138-
5139-
values = result._values
5140-
mask = missing.mask_missing(values, to_replace)
5141-
5142-
if isinstance(values, ExtensionArray):
5143-
# dispatch to the EA's _pad_mask_inplace method
5144-
values._fill_mask_inplace(method, limit, mask)
5145-
else:
5146-
fill_f = missing.get_fill_func(method)
5147-
fill_f(values, limit=limit, mask=mask)
5148-
5149-
if inplace:
5150-
return None
5151-
return result
5152-
51535118
def memory_usage(self, index: bool = True, deep: bool = False) -> int:
51545119
"""
51555120
Return the memory usage of the Series.

pandas/core/shared_docs.py

+1-18
Original file line numberDiff line numberDiff line change
@@ -608,24 +608,7 @@
608608
4 None
609609
dtype: object
610610
611-
When ``value`` is not explicitly passed and `to_replace` is a scalar, list
612-
or tuple, `replace` uses the method parameter (default 'pad') to do the
613-
replacement. So this is why the 'a' values are being replaced by 10
614-
in rows 1 and 2 and 'b' in row 4 in this case.
615-
616-
>>> s.replace('a')
617-
0 10
618-
1 10
619-
2 10
620-
3 b
621-
4 b
622-
dtype: object
623-
624-
.. deprecated:: 2.1.0
625-
The 'method' parameter and padding behavior are deprecated.
626-
627-
On the other hand, if ``None`` is explicitly passed for ``value``, it will
628-
be respected:
611+
If ``None`` is explicitly passed for ``value``, it will be respected:
629612
630613
>>> s.replace('a', None)
631614
0 10

pandas/tests/frame/methods/test_replace.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -1264,13 +1264,8 @@ def test_replace_invalid_to_replace(self):
12641264
r"Expecting 'to_replace' to be either a scalar, array-like, "
12651265
r"dict or None, got invalid type.*"
12661266
)
1267-
msg2 = (
1268-
"DataFrame.replace without 'value' and with non-dict-like "
1269-
"'to_replace' is deprecated"
1270-
)
12711267
with pytest.raises(TypeError, match=msg):
1272-
with tm.assert_produces_warning(FutureWarning, match=msg2):
1273-
df.replace(lambda x: x.strip())
1268+
df.replace(lambda x: x.strip())
12741269

12751270
@pytest.mark.parametrize("dtype", ["float", "float64", "int64", "Int64", "boolean"])
12761271
@pytest.mark.parametrize("value", [np.nan, pd.NA])

pandas/tests/series/methods/test_replace.py

+14-27
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,15 @@ def test_replace_gh5319(self):
137137
# API change from 0.12?
138138
# GH 5319
139139
ser = pd.Series([0, np.nan, 2, 3, 4])
140-
expected = ser.ffill()
141140
msg = (
142-
"Series.replace without 'value' and with non-dict-like "
143-
"'to_replace' is deprecated"
141+
"Series.replace must specify either 'value', "
142+
"a dict-like 'to_replace', or dict-like 'regex'"
144143
)
145-
with tm.assert_produces_warning(FutureWarning, match=msg):
146-
result = ser.replace([np.nan])
147-
tm.assert_series_equal(result, expected)
144+
with pytest.raises(ValueError, match=msg):
145+
ser.replace([np.nan])
148146

149-
ser = pd.Series([0, np.nan, 2, 3, 4])
150-
expected = ser.ffill()
151-
with tm.assert_produces_warning(FutureWarning, match=msg):
152-
result = ser.replace(np.nan)
153-
tm.assert_series_equal(result, expected)
147+
with pytest.raises(ValueError, match=msg):
148+
ser.replace(np.nan)
154149

155150
def test_replace_datetime64(self):
156151
# GH 5797
@@ -182,19 +177,16 @@ def test_replace_timedelta_td64(self):
182177

183178
def test_replace_with_single_list(self):
184179
ser = pd.Series([0, 1, 2, 3, 4])
185-
msg2 = (
186-
"Series.replace without 'value' and with non-dict-like "
187-
"'to_replace' is deprecated"
180+
msg = (
181+
"Series.replace must specify either 'value', "
182+
"a dict-like 'to_replace', or dict-like 'regex'"
188183
)
189-
with tm.assert_produces_warning(FutureWarning, match=msg2):
190-
result = ser.replace([1, 2, 3])
191-
tm.assert_series_equal(result, pd.Series([0, 0, 0, 0, 4]))
184+
with pytest.raises(ValueError, match=msg):
185+
ser.replace([1, 2, 3])
192186

193187
s = ser.copy()
194-
with tm.assert_produces_warning(FutureWarning, match=msg2):
195-
return_value = s.replace([1, 2, 3], inplace=True)
196-
assert return_value is None
197-
tm.assert_series_equal(s, pd.Series([0, 0, 0, 0, 4]))
188+
with pytest.raises(ValueError, match=msg):
189+
s.replace([1, 2, 3], inplace=True)
198190

199191
def test_replace_mixed_types(self):
200192
ser = pd.Series(np.arange(5), dtype="int64")
@@ -483,13 +475,8 @@ def test_replace_invalid_to_replace(self):
483475
r"Expecting 'to_replace' to be either a scalar, array-like, "
484476
r"dict or None, got invalid type.*"
485477
)
486-
msg2 = (
487-
"Series.replace without 'value' and with non-dict-like "
488-
"'to_replace' is deprecated"
489-
)
490478
with pytest.raises(TypeError, match=msg):
491-
with tm.assert_produces_warning(FutureWarning, match=msg2):
492-
series.replace(lambda x: x.strip())
479+
series.replace(lambda x: x.strip())
493480

494481
@pytest.mark.parametrize("frame", [False, True])
495482
def test_replace_nonbool_regex(self, frame):

0 commit comments

Comments
 (0)