Skip to content

Commit 8399185

Browse files
CoW: Warn for inplace replace (#56297)
Co-authored-by: Joris Van den Bossche <[email protected]>
1 parent 657da07 commit 8399185

File tree

4 files changed

+65
-14
lines changed

4 files changed

+65
-14
lines changed

pandas/core/internals/base.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,16 @@ def replace(self, to_replace, value, inplace: bool) -> Self:
252252
value=value,
253253
inplace=inplace,
254254
using_cow=using_copy_on_write(),
255+
already_warned=_AlreadyWarned(),
255256
)
256257

257258
@final
258259
def replace_regex(self, **kwargs) -> Self:
259260
return self.apply_with_block(
260-
"_replace_regex", **kwargs, using_cow=using_copy_on_write()
261+
"_replace_regex",
262+
**kwargs,
263+
using_cow=using_copy_on_write(),
264+
already_warned=_AlreadyWarned(),
261265
)
262266

263267
@final
@@ -278,6 +282,7 @@ def replace_list(
278282
inplace=inplace,
279283
regex=regex,
280284
using_cow=using_copy_on_write(),
285+
already_warned=_AlreadyWarned(),
281286
)
282287
bm._consolidate_inplace()
283288
return bm

pandas/core/internals/blocks.py

+45
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ def replace(
830830
# mask may be pre-computed if we're called from replace_list
831831
mask: npt.NDArray[np.bool_] | None = None,
832832
using_cow: bool = False,
833+
already_warned=None,
833834
) -> list[Block]:
834835
"""
835836
replace the to_replace value with value, possible to create new
@@ -874,6 +875,20 @@ def replace(
874875
# and rest?
875876
blk = self._maybe_copy(using_cow, inplace)
876877
putmask_inplace(blk.values, mask, value)
878+
if (
879+
inplace
880+
and warn_copy_on_write()
881+
and already_warned is not None
882+
and not already_warned.warned_already
883+
):
884+
if self.refs.has_reference():
885+
warnings.warn(
886+
COW_WARNING_GENERAL_MSG,
887+
FutureWarning,
888+
stacklevel=find_stack_level(),
889+
)
890+
already_warned.warned_already = True
891+
877892
if not (self.is_object and value is None):
878893
# if the user *explicitly* gave None, we keep None, otherwise
879894
# may downcast to NaN
@@ -934,6 +949,7 @@ def _replace_regex(
934949
inplace: bool = False,
935950
mask=None,
936951
using_cow: bool = False,
952+
already_warned=None,
937953
) -> list[Block]:
938954
"""
939955
Replace elements by the given value.
@@ -968,6 +984,20 @@ def _replace_regex(
968984

969985
replace_regex(block.values, rx, value, mask)
970986

987+
if (
988+
inplace
989+
and warn_copy_on_write()
990+
and already_warned is not None
991+
and not already_warned.warned_already
992+
):
993+
if self.refs.has_reference():
994+
warnings.warn(
995+
COW_WARNING_GENERAL_MSG,
996+
FutureWarning,
997+
stacklevel=find_stack_level(),
998+
)
999+
already_warned.warned_already = True
1000+
9711001
nbs = block.convert(copy=False, using_cow=using_cow)
9721002
opt = get_option("future.no_silent_downcasting")
9731003
if (len(nbs) > 1 or nbs[0].dtype != block.dtype) and not opt:
@@ -992,6 +1022,7 @@ def replace_list(
9921022
inplace: bool = False,
9931023
regex: bool = False,
9941024
using_cow: bool = False,
1025+
already_warned=None,
9951026
) -> list[Block]:
9961027
"""
9971028
See BlockManager.replace_list docstring.
@@ -1048,6 +1079,20 @@ def replace_list(
10481079
else:
10491080
rb = [self if inplace else self.copy()]
10501081

1082+
if (
1083+
inplace
1084+
and warn_copy_on_write()
1085+
and already_warned is not None
1086+
and not already_warned.warned_already
1087+
):
1088+
if self.refs.has_reference():
1089+
warnings.warn(
1090+
COW_WARNING_GENERAL_MSG,
1091+
FutureWarning,
1092+
stacklevel=find_stack_level(),
1093+
)
1094+
already_warned.warned_already = True
1095+
10511096
opt = get_option("future.no_silent_downcasting")
10521097
for i, ((src, dest), mask) in enumerate(zip(pairs, masks)):
10531098
convert = i == src_len # only convert once at the end

pandas/tests/copy_view/test_chained_assignment_deprecation.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,9 @@ def test_methods_iloc_warn(using_copy_on_write):
4545
def test_methods_iloc_getitem_item_cache(
4646
func, args, using_copy_on_write, warn_copy_on_write
4747
):
48-
df = DataFrame({"a": [1.5, 2, 3], "b": 1.5})
48+
df = DataFrame({"a": [1, 2, 3], "b": 1})
4949
ser = df.iloc[:, 0]
50-
# TODO(CoW-warn) should warn about updating a view for all methods
51-
with tm.assert_cow_warning(
52-
warn_copy_on_write and func not in ("replace", "fillna")
53-
):
50+
with tm.assert_cow_warning(warn_copy_on_write and func == "replace"):
5451
getattr(ser, func)(*args, inplace=True)
5552

5653
# parent that holds item_cache is dead, so don't increase ref count

pandas/tests/copy_view/test_replace.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ def test_replace(using_copy_on_write, replace_kwargs):
4848
tm.assert_frame_equal(df, df_orig)
4949

5050

51-
def test_replace_regex_inplace_refs(using_copy_on_write):
51+
def test_replace_regex_inplace_refs(using_copy_on_write, warn_copy_on_write):
5252
df = DataFrame({"a": ["aaa", "bbb"]})
5353
df_orig = df.copy()
5454
view = df[:]
5555
arr = get_array(df, "a")
56-
df.replace(to_replace=r"^a.*$", value="new", inplace=True, regex=True)
56+
with tm.assert_cow_warning(warn_copy_on_write):
57+
df.replace(to_replace=r"^a.*$", value="new", inplace=True, regex=True)
5758
if using_copy_on_write:
5859
assert not np.shares_memory(arr, get_array(df, "a"))
5960
assert df._mgr._has_no_reference(0)
@@ -202,11 +203,12 @@ def test_replace_inplace(using_copy_on_write, to_replace):
202203

203204

204205
@pytest.mark.parametrize("to_replace", [1.5, [1.5]])
205-
def test_replace_inplace_reference(using_copy_on_write, to_replace):
206+
def test_replace_inplace_reference(using_copy_on_write, to_replace, warn_copy_on_write):
206207
df = DataFrame({"a": [1.5, 2, 3]})
207208
arr_a = get_array(df, "a")
208209
view = df[:]
209-
df.replace(to_replace=to_replace, value=15.5, inplace=True)
210+
with tm.assert_cow_warning(warn_copy_on_write):
211+
df.replace(to_replace=to_replace, value=15.5, inplace=True)
210212

211213
if using_copy_on_write:
212214
assert not np.shares_memory(get_array(df, "a"), arr_a)
@@ -354,12 +356,13 @@ def test_replace_list_none(using_copy_on_write):
354356
assert not np.shares_memory(get_array(df, "a"), get_array(df2, "a"))
355357

356358

357-
def test_replace_list_none_inplace_refs(using_copy_on_write):
359+
def test_replace_list_none_inplace_refs(using_copy_on_write, warn_copy_on_write):
358360
df = DataFrame({"a": ["a", "b", "c"]})
359361
arr = get_array(df, "a")
360362
df_orig = df.copy()
361363
view = df[:]
362-
df.replace(["a"], value=None, inplace=True)
364+
with tm.assert_cow_warning(warn_copy_on_write):
365+
df.replace(["a"], value=None, inplace=True)
363366
if using_copy_on_write:
364367
assert df._mgr._has_no_reference(0)
365368
assert not np.shares_memory(arr, get_array(df, "a"))
@@ -431,15 +434,16 @@ def test_replace_listlike(using_copy_on_write):
431434
tm.assert_frame_equal(df, df_orig)
432435

433436

434-
def test_replace_listlike_inplace(using_copy_on_write):
437+
def test_replace_listlike_inplace(using_copy_on_write, warn_copy_on_write):
435438
df = DataFrame({"a": [1, 2, 3], "b": [1, 2, 3]})
436439
arr = get_array(df, "a")
437440
df.replace([200, 2], [10, 11], inplace=True)
438441
assert np.shares_memory(get_array(df, "a"), arr)
439442

440443
view = df[:]
441444
df_orig = df.copy()
442-
df.replace([200, 3], [10, 11], inplace=True)
445+
with tm.assert_cow_warning(warn_copy_on_write):
446+
df.replace([200, 3], [10, 11], inplace=True)
443447
if using_copy_on_write:
444448
assert not np.shares_memory(get_array(df, "a"), arr)
445449
tm.assert_frame_equal(view, df_orig)

0 commit comments

Comments
 (0)