Skip to content

DEPR: replace without passing value #58040

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 5 commits into from
Apr 1, 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
- :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
- 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`)
- Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)
- Disallow non-standard (``np.ndarray``, :class:`Index`, :class:`ExtensionArray`, or :class:`Series`) to :func:`isin`, :func:`unique`, :func:`factorize` (:issue:`52986`)
- Disallow passing a pandas type to :meth:`Index.view` (:issue:`55709`)
- Disallow units other than "s", "ms", "us", "ns" for datetime64 and timedelta64 dtypes in :func:`array` (:issue:`53817`)
Expand Down
7 changes: 0 additions & 7 deletions pandas/core/arrays/_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,6 @@ def __getitem__(
result = self._from_backing_data(result)
return result

def _fill_mask_inplace(
self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
) -> None:
# (for now) when self.ndim == 2, we assume axis=0
func = missing.get_fill_func(method, ndim=self.ndim)
func(self._ndarray.T, limit=limit, mask=mask.T)

def _pad_or_backfill(
self,
*,
Expand Down
19 changes: 0 additions & 19 deletions pandas/core/arrays/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2111,25 +2111,6 @@ def _where(self, mask: npt.NDArray[np.bool_], value) -> Self:
result[~mask] = val
return result

# TODO(3.0): this can be removed once GH#33302 deprecation is enforced
def _fill_mask_inplace(
self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
) -> None:
"""
Replace values in locations specified by 'mask' using pad or backfill.
See also
--------
ExtensionArray.fillna
"""
func = missing.get_fill_func(method)
npvalues = self.astype(object)
# NB: if we don't copy mask here, it may be altered inplace, which
# would mess up the `self[mask] = ...` below.
func(npvalues, limit=limit, mask=mask.copy())
new_values = self._from_sequence(npvalues, dtype=self.dtype)
self[mask] = new_values[mask]

def _rank(
self,
*,
Expand Down
57 changes: 13 additions & 44 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7319,17 +7319,8 @@ def replace(
inplace: bool = False,
regex: bool = False,
) -> Self | None:
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
f"{type(self).__name__}.replace without 'value' and with "
"non-dict-like 'to_replace' is deprecated "
"and will raise in a future version. "
"Explicitly specify the new values instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
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 not (
is_scalar(to_replace)
Expand All @@ -7342,6 +7333,15 @@ def replace(
f"{type(to_replace).__name__!r}"
)

if value is lib.no_default and not (
is_dict_like(to_replace) or is_dict_like(regex)
):
raise ValueError(
# GH#33302
f"{type(self).__name__}.replace must specify either 'value', "
"a dict-like 'to_replace', or dict-like 'regex'."
)

inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY:
Expand All @@ -7352,41 +7352,10 @@ def replace(
stacklevel=2,
)

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:
# 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.

# passing a single value that is scalar like
# when value is None (GH5319), for compat
if not is_dict_like(to_replace) and not is_dict_like(regex):
to_replace = [to_replace]

if isinstance(to_replace, (tuple, list)):
# TODO: Consider copy-on-write for non-replaced columns's here
if isinstance(self, ABCDataFrame):
from pandas import Series

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

if not is_dict_like(to_replace):
if not is_dict_like(regex):
raise TypeError(
'If "to_replace" and "value" are both None '
'and "to_replace" is not a list, then '
"regex must be a mapping"
)
# In this case we have checked above that
# 1) regex is dict-like and 2) to_replace is None
to_replace = regex
regex = True

Expand Down
35 changes: 0 additions & 35 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@
algorithms,
base,
common as com,
missing,
nanops,
ops,
roperator,
Expand Down Expand Up @@ -5116,40 +5115,6 @@ def info(
show_counts=show_counts,
)

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

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

@overload
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, 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()

values = result._values
mask = missing.mask_missing(values, to_replace)

if isinstance(values, ExtensionArray):
# dispatch to the EA's _pad_mask_inplace method
values._fill_mask_inplace(method, limit, mask)
else:
fill_f = missing.get_fill_func(method)
fill_f(values, limit=limit, mask=mask)

if inplace:
return None
return result

def memory_usage(self, index: bool = True, deep: bool = False) -> int:
"""
Return the memory usage of the Series.
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 @@ -608,24 +608,7 @@
4 None
dtype: object
When ``value`` is not explicitly passed and `to_replace` is a scalar, list
or tuple, `replace` uses the method parameter (default 'pad') to do the
replacement. So this is why the 'a' values are being replaced by 10
in rows 1 and 2 and 'b' in row 4 in this case.
>>> s.replace('a')
0 10
1 10
2 10
3 b
4 b
dtype: object
.. deprecated:: 2.1.0
The 'method' parameter and padding behavior are deprecated.
On the other hand, if ``None`` is explicitly passed for ``value``, it will
be respected:
If ``None`` is explicitly passed for ``value``, it will be respected:
>>> s.replace('a', None)
0 10
Expand Down
7 changes: 1 addition & 6 deletions pandas/tests/frame/methods/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,13 +1264,8 @@ def test_replace_invalid_to_replace(self):
r"Expecting 'to_replace' to be either a scalar, array-like, "
r"dict or None, got invalid type.*"
)
msg2 = (
"DataFrame.replace without 'value' and with non-dict-like "
"'to_replace' is deprecated"
)
with pytest.raises(TypeError, match=msg):
with tm.assert_produces_warning(FutureWarning, match=msg2):
df.replace(lambda x: x.strip())
df.replace(lambda x: x.strip())

@pytest.mark.parametrize("dtype", ["float", "float64", "int64", "Int64", "boolean"])
@pytest.mark.parametrize("value", [np.nan, pd.NA])
Expand Down
41 changes: 14 additions & 27 deletions pandas/tests/series/methods/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,15 @@ def test_replace_gh5319(self):
# API change from 0.12?
# GH 5319
ser = pd.Series([0, np.nan, 2, 3, 4])
expected = ser.ffill()
msg = (
"Series.replace without 'value' and with non-dict-like "
"'to_replace' is deprecated"
"Series.replace must specify either 'value', "
"a dict-like 'to_replace', or dict-like 'regex'"
)
with tm.assert_produces_warning(FutureWarning, match=msg):
result = ser.replace([np.nan])
tm.assert_series_equal(result, expected)
with pytest.raises(ValueError, match=msg):
ser.replace([np.nan])

ser = pd.Series([0, np.nan, 2, 3, 4])
expected = ser.ffill()
with tm.assert_produces_warning(FutureWarning, match=msg):
result = ser.replace(np.nan)
tm.assert_series_equal(result, expected)
with pytest.raises(ValueError, match=msg):
ser.replace(np.nan)

def test_replace_datetime64(self):
# GH 5797
Expand Down Expand Up @@ -182,19 +177,16 @@ def test_replace_timedelta_td64(self):

def test_replace_with_single_list(self):
ser = pd.Series([0, 1, 2, 3, 4])
msg2 = (
"Series.replace without 'value' and with non-dict-like "
"'to_replace' is deprecated"
msg = (
"Series.replace must specify either 'value', "
"a dict-like 'to_replace', or dict-like 'regex'"
)
with tm.assert_produces_warning(FutureWarning, match=msg2):
result = ser.replace([1, 2, 3])
tm.assert_series_equal(result, pd.Series([0, 0, 0, 0, 4]))
with pytest.raises(ValueError, match=msg):
ser.replace([1, 2, 3])

s = ser.copy()
with tm.assert_produces_warning(FutureWarning, match=msg2):
return_value = s.replace([1, 2, 3], inplace=True)
assert return_value is None
tm.assert_series_equal(s, pd.Series([0, 0, 0, 0, 4]))
with pytest.raises(ValueError, match=msg):
s.replace([1, 2, 3], inplace=True)

def test_replace_mixed_types(self):
ser = pd.Series(np.arange(5), dtype="int64")
Expand Down Expand Up @@ -483,13 +475,8 @@ def test_replace_invalid_to_replace(self):
r"Expecting 'to_replace' to be either a scalar, array-like, "
r"dict or None, got invalid type.*"
)
msg2 = (
"Series.replace without 'value' and with non-dict-like "
"'to_replace' is deprecated"
)
with pytest.raises(TypeError, match=msg):
with tm.assert_produces_warning(FutureWarning, match=msg2):
series.replace(lambda x: x.strip())
series.replace(lambda x: x.strip())

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