SettingWithCopyWarning и Copy-on-Write в pandas 2.x
Самое запутанное предупреждение pandas и то, как Copy-on-Write в версии 2.x окончательно его убирает — но требует менять привычки.
SettingWithCopyWarning — предупреждение о том, что вы пытаетесь изменить данные через объект, который может быть копией, и изменение «не дойдёт» до исходного DataFrame.
Корень проблемы: представление или копия
Когда вы берёте часть DataFrame, pandas иногда возвращает представление (view) — окно в те же данные в памяти, а иногда копию (copy) — независимый кусок памяти. Снаружи они выглядят одинаково, но запись в представление меняет исходные данные, а запись в копию — нет. До pandas 2.x было трудно предсказать, что именно вы получили, и отсюда росли загадочные баги «я присвоил значение, а оно не изменилось».
Классический сценарий предупреждения
Беда возникает при цепочечном индексировании (chained indexing) — когда вы делаете две операции подряд: сначала отбираете, потом присваиваете.
# ПЛОХО: цепочка [...][...] — pandas не знает, во что вы пишете
df[df["цена"] > 1000]["скидка"] = 0
# SettingWithCopyWarning: A value is trying to be set on a copy...
# результат: исходный df НЕ изменился — присваивание ушло "в никуда"
Здесь df[df["цена"] > 1000] сначала создаёт временный объект (часто копию), и присваивание ["скидка"] = 0 меняет именно его — а он тут же выбрасывается. Исходный df остаётся прежним. pandas честно предупреждает: «вы пишете в копию».
Правильный способ: одна операция через loc
Лекарство — выполнять отбор и присваивание в одной операции через loc, передав маску строк и имя столбца сразу:
# ХОРОШО: одна операция — pandas точно знает, куда писать
df.loc[df["цена"] > 1000, "скидка"] = 0
Здесь нет промежуточного объекта: loc получает и строки (по маске), и столбец, и пишет прямо в исходный df. Это работает корректно во всех версиях pandas и должно стать вашим рефлексом.
Почему именно цепочка опасна — модель на чистом Python
Покажем суть на вложенных списках: «срез», который возвращает новый список, — это копия, и запись в него не доходит до оригинала; а изменение по индексу прямо в оригинале — доходит.
import copy
original = [["мышь", 990], ["монитор", 18000], ["кабель", 300]]
# "цепочка": фильтр возвращает НОВЫЙ, независимый список — копию.
# Запись в копию не доходит до оригинала.
expensive = copy.deepcopy([row for row in original if row[1] > 1000])
expensive[0][1] = 0 # меняем копию
print("после записи в копию:", original[1]) # оригинал прежний
# правильно: находим позицию в оригинале и пишем прямо туда
for row in original:
if row[1] > 1000:
row[1] = 0 # запись в сам оригинал
print("после записи в оригинал:", original[1])
Вывод:
после записи в копию: ['монитор', 18000] после записи в оригинал: ['монитор', 0]
Первая запись ушла в отдельный список expensive и не изменила original — ровно как присваивание в результат df[...][...]. Вторая пишет прямо в исходные данные — аналог df.loc[маска, столбец] = ....
Copy-on-Write: новая модель в pandas 2.x
Чтобы закрыть этот класс ошибок, pandas вводит Copy-on-Write (CoW). Идея: любой объект, полученный из другого, ведёт себя как независимая копия, но физическое копирование откладывается до момента реальной записи — и только если данные действительно делятся между объектами. Включить в pandas 2.x можно так:
import pandas as pd
pd.options.mode.copy_on_write = True
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
print(df["foo"].tolist()) # [1, 2, 3] — исходный df НЕ изменился
Что меняется при CoW:
- SettingWithCopyWarning исчезает — предупреждать больше не о чем: любой производный объект и так ведёт себя как копия.
- Цепочечное присваивание тихо не работает и в новых версиях возбуждает
ChainedAssignmentErrorвместо предупреждения — ошибку нельзя пропустить. - Производительность не страдает: копия делается лениво, только при записи в разделяемые данные, поэтому лишних копий не возникает.
Важно: CoW станет поведением по умолчанию
В pandas 3.0 Copy-on-Write становится единственным режимом — отключить его будет нельзя. Это значит, что код, который полагался на «случайные» представления (изменил подмножество — изменился оригинал), сломается. Готовиться стоит уже сейчас: писать присваивания через loc и не рассчитывать на побочное изменение оригинала через срез.
Как подготовить существующий код заранее? В pandas 2.2+ есть «режим предупреждений»: pd.options.mode.copy_on_write = "warn". В нём библиотека работает по-старому, но печатает предупреждение в тех местах, где поведение изменится с приходом CoW. Прогнав так свои скрипты и тесты, вы получите список мест, требующих правки, ещё до обновления на 3.0. Это куда дешевле, чем ловить тихие баги в проде после мажорного апгрейда.
Краткая хронология
| Версия | Статус CoW |
| 1.5 | введён экспериментально (выключен) |
| 2.0–2.1 | оптимизации реализованы; включается опцией |
| 2.2 | доступен режим предупреждений "warn" |
| 3.0 | поведение по умолчанию и единственное |
Паттерны, которые надо обновить
| Было (рискованно) | Стало (правильно) |
df[m]["c"] = 0 | df.loc[m, "c"] = 0 |
df["c"].replace(1, 5, inplace=True) | df["c"] = df["c"].replace(1, 5) |
| опираться на view от среза | явный .copy(), если нужна независимая таблица |
| держать ссылку на подтаблицу и менять её | переприсвоить: df = df.reset_index(drop=True) |
Подводные камни
- Тихая потеря изменений. Самое коварное: цепочечное присваивание без CoW могло не выдать предупреждения и просто ничего не изменить.
- inplace на столбце.
df["c"].fillna(0, inplace=True)при CoW не изменит df; переприсваивайте:df["c"] = df["c"].fillna(0). - to_numpy() даёт read-only массив при разделяемых данных; для записи берите
df.to_numpy().copy(). - Ложная уверенность в view. Не пишите код, который сознательно меняет оригинал через срез — в pandas 3.0 это перестанет работать.
Лучшие практики
- Любое условное присваивание делайте одной операцией:
df.loc[маска, столбец] = значение. - Нужна независимая таблица — берите её явно через
.copy(). - Включите
pd.options.mode.copy_on_write = Trueуже сейчас, чтобы код был готов к pandas 3.0. - Избегайте
inplace=на отдельных столбцах — переприсваивайте результат.
Итог
- SettingWithCopyWarning предупреждает о записи в возможную копию через цепочку индексирования.
- Правильно — одна операция через
loc:df.loc[маска, столбец] = .... - Copy-on-Write делает любой производный объект «копией по поведению» с ленивым копированием.
- В pandas 3.0 CoW — единственный режим; цепочечное присваивание становится ошибкой.