Skip to content

Commit

Permalink
API / CoW: Add ChainedAssignmentError for inplace ops (pandas-dev#54313)
Browse files Browse the repository at this point in the history
  • Loading branch information
phofl authored Aug 11, 2023
1 parent 0e196b0 commit cd9d391
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 5 deletions.
6 changes: 6 additions & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ Copy-on-Write improvements
- DataFrame.update / Series.update
- DataFrame.fillna / Series.fillna
- DataFrame.replace / Series.replace
- DataFrame.clip / Series.clip
- DataFrame.where / Series.where
- DataFrame.mask / Series.mask
- DataFrame.interpolate / Series.interpolate
- DataFrame.ffill / Series.ffill
- DataFrame.bfill / Series.bfill

.. _whatsnew_210.enhancements.map_na_action:

Expand Down
55 changes: 55 additions & 0 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7385,6 +7385,15 @@ def ffill(
dtype: float64
"""
downcast = self._deprecate_downcast(downcast, "ffill")
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)

return self._pad_or_backfill(
"ffill",
Expand Down Expand Up @@ -7523,6 +7532,15 @@ def bfill(
3 4.0 7.0
"""
downcast = self._deprecate_downcast(downcast, "bfill")
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)
return self._pad_or_backfill(
"bfill",
axis=axis,
Expand Down Expand Up @@ -8047,6 +8065,16 @@ def interpolate(
raise ValueError("downcast must be either None or 'infer'")

inplace = validate_bool_kwarg(inplace, "inplace")

if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)

axis = self._get_axis_number(axis)

if self.empty:
Expand Down Expand Up @@ -8619,6 +8647,15 @@ def clip(
"""
inplace = validate_bool_kwarg(inplace, "inplace")

if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)

axis = nv.validate_clip_with_axis(axis, (), kwargs)
if axis is not None:
axis = self._get_axis_number(axis)
Expand Down Expand Up @@ -10500,6 +10537,15 @@ def where(
3 True True
4 True True
"""
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)
other = common.apply_if_callable(other, self)
return self._where(cond, other, inplace, axis, level)

Expand Down Expand Up @@ -10558,6 +10604,15 @@ def mask(
level: Level | None = None,
) -> Self | None:
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
ChainedAssignmentError,
stacklevel=2,
)

cond = common.apply_if_callable(cond, self)

# see gh-21891
Expand Down
13 changes: 13 additions & 0 deletions pandas/tests/copy_view/test_clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,16 @@ def test_clip_no_op(using_copy_on_write):
assert np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
else:
assert not np.shares_memory(get_array(df2, "a"), get_array(df, "a"))


def test_clip_chained_inplace(using_copy_on_write):
df = DataFrame({"a": [1, 4, 2], "b": 1})
df_orig = df.copy()
if using_copy_on_write:
with tm.raises_chained_assignment_error():
df["a"].clip(1, 2, inplace=True)
tm.assert_frame_equal(df, df_orig)

with tm.raises_chained_assignment_error():
df[["a"]].clip(1, 2, inplace=True)
tm.assert_frame_equal(df, df_orig)
14 changes: 14 additions & 0 deletions pandas/tests/copy_view/test_interp_fillna.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,17 @@ def test_fillna_chained_assignment(using_copy_on_write):
with tm.raises_chained_assignment_error():
df[["a"]].fillna(100, inplace=True)
tm.assert_frame_equal(df, df_orig)


@pytest.mark.parametrize("func", ["interpolate", "ffill", "bfill"])
def test_interpolate_chained_assignment(using_copy_on_write, func):
df = DataFrame({"a": [1, np.nan, 2], "b": 1})
df_orig = df.copy()
if using_copy_on_write:
with tm.raises_chained_assignment_error():
getattr(df["a"], func)(inplace=True)
tm.assert_frame_equal(df, df_orig)

with tm.raises_chained_assignment_error():
getattr(df[["a"]], func)(inplace=True)
tm.assert_frame_equal(df, df_orig)
14 changes: 14 additions & 0 deletions pandas/tests/copy_view/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,20 @@ def test_where_mask_noop_on_single_column(using_copy_on_write, dtype, val, func)
tm.assert_frame_equal(df, df_orig)


@pytest.mark.parametrize("func", ["mask", "where"])
def test_chained_where_mask(using_copy_on_write, func):
df = DataFrame({"a": [1, 4, 2], "b": 1})
df_orig = df.copy()
if using_copy_on_write:
with tm.raises_chained_assignment_error():
getattr(df["a"], func)(df["a"] > 2, 5, inplace=True)
tm.assert_frame_equal(df, df_orig)

with tm.raises_chained_assignment_error():
getattr(df[["a"]], func)(df["a"] > 2, 5, inplace=True)
tm.assert_frame_equal(df, df_orig)


def test_asfreq_noop(using_copy_on_write):
df = DataFrame(
{"a": [0.0, None, 2.0, 3.0]},
Expand Down
21 changes: 16 additions & 5 deletions pandas/tests/frame/methods/test_interpolate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import pytest

from pandas.errors import ChainedAssignmentError
import pandas.util._test_decorators as td

from pandas import (
Expand Down Expand Up @@ -370,21 +371,31 @@ def test_interp_inplace(self, using_copy_on_write):
expected = DataFrame({"a": [1.0, 2.0, 3.0, 4.0]})
expected_cow = df.copy()
result = df.copy()
return_value = result["a"].interpolate(inplace=True)
assert return_value is None

if using_copy_on_write:
with tm.raises_chained_assignment_error():
return_value = result["a"].interpolate(inplace=True)
assert return_value is None
tm.assert_frame_equal(result, expected_cow)
else:
return_value = result["a"].interpolate(inplace=True)
assert return_value is None
tm.assert_frame_equal(result, expected)

result = df.copy()
msg = "The 'downcast' keyword in Series.interpolate is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
return_value = result["a"].interpolate(inplace=True, downcast="infer")
assert return_value is None

if using_copy_on_write:
with tm.assert_produces_warning(
(FutureWarning, ChainedAssignmentError), match=msg
):
return_value = result["a"].interpolate(inplace=True, downcast="infer")
assert return_value is None
tm.assert_frame_equal(result, expected_cow)
else:
with tm.assert_produces_warning(FutureWarning, match=msg):
return_value = result["a"].interpolate(inplace=True, downcast="infer")
assert return_value is None
tm.assert_frame_equal(result, expected.astype("int64"))

def test_interp_inplace_row(self):
Expand Down

0 comments on commit cd9d391

Please sign in to comment.