Copy-on-Write (CoW)#

Примечание

Copy-on-Write станет стандартом в pandas 3.0. Мы рекомендуем включение сейчас чтобы воспользоваться всеми улучшениями.

Copy-on-Write впервые был представлен в версии 1.5.0. Начиная с версии 2.0 большинство оптимизаций, которые становятся возможными благодаря CoW, реализованы и поддерживаются. Все возможные оптимизации поддерживаются начиная с pandas 2.1.

CoW будет включен по умолчанию в версии 3.0.

CoW приведет к более предсказуемому поведению, поскольку невозможно обновить более одного объекта одним оператором, например, операции индексирования или методы не будут иметь побочных эффектов. Кроме того, за счет максимальной отсрочки копирования средняя производительность и использование памяти улучшатся.

Предыдущее поведение#

поведение индексирования в pandas сложно понять. Некоторые операции возвращают представления, в то время как другие возвращают копии. В зависимости от результата операции, изменение одного объекта может случайно изменить другой:

In [1]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [2]: subset = df["foo"]

In [3]: subset.iloc[0] = 100

In [4]: df
Out[4]: 
   foo  bar
0  100    4
1    2    5
2    3    6

Изменение subset, например, обновление его значений также обновляет df. Точное поведение трудно предсказать. Copy-on-Write решает проблему случайного изменения более одного объекта, явно запрещая это. При включенном CoW, df не изменяется:

In [5]: pd.options.mode.copy_on_write = True

In [6]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [7]: subset = df["foo"]

In [8]: subset.iloc[0] = 100

In [9]: df
Out[9]: 
   foo  bar
0    1    4
1    2    5
2    3    6

Следующие разделы объяснят, что это означает и как это влияет на существующие приложения.

Миграция на Copy-on-Write#

Copy-on-Write будет режимом по умолчанию и единственным режимом в pandas 3.0. Это означает, что пользователям необходимо адаптировать свой код для соответствия правилам CoW.

Режим по умолчанию в pandas будет выдавать предупреждения для определённых случаев, которые активно изменяют поведение и, следовательно, изменяют поведение, задуманное пользователем.

Мы добавили другой режим, например.

pd.options.mode.copy_on_write = "warn"

которая будет предупреждать о каждой операции, которая изменит поведение с CoW. Мы ожидаем, что этот режим будет очень шумным, поскольку многие случаи, которые, как мы не ожидаем, повлияют на пользователей, также будут выдавать предупреждение. Мы рекомендуем проверить этот режим и проанализировать предупреждения, но не обязательно устранять все эти предупреждения. Первые два пункта из следующего списка — это единственные случаи, которые нужно исправить, чтобы существующий код работал с CoW.

Следующие несколько пунктов описывают видимые пользователю изменения:

Цепное присваивание никогда не будет работать

loc следует использовать как альтернативу. Проверьте раздел цепочечного присваивания для получения дополнительной информации.

Доступ к базовому массиву объекта pandas вернет представление только для чтения

In [10]: ser = pd.Series([1, 2, 3])

In [11]: ser.to_numpy()
Out[11]: array([1, 2, 3])

Этот пример возвращает массив NumPy, который является представлением объекта Series. Это представление может быть изменено и, таким образом, также изменить объект pandas. Это не соответствует правилам CoW. Возвращаемый массив устанавливается в режим только для чтения, чтобы защититься от этого поведения. Создание копии этого массива позволяет вносить изменения. Вы также можете снова сделать массив доступным для записи, если вас больше не волнует объект pandas.

См. раздел о только для чтения массивы NumPy для получения дополнительной информации.

Только один объект pandas обновляется за раз

Следующий фрагмент кода обновляет оба df и subset без CoW:

In [12]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [13]: subset = df["foo"]

In [14]: subset.iloc[0] = 100

In [15]: df
Out[15]: 
   foo  bar
0    1    4
1    2    5
2    3    6

Это больше не будет возможно с CoW, поскольку правила CoW явно запрещают это. Это включает обновление одного столбца как Series и полагаясь на то, что изменение распространится обратно на родительский DataFrame. Это утверждение можно переписать в одно утверждение с loc или iloc если это поведение необходимо. DataFrame.where() является ещё одной подходящей альтернативой для этого случая.

Обновление столбца, выбранного из DataFrame с методом inplace также больше не будет работать.

In [16]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [17]: df["foo"].replace(1, 5, inplace=True)

In [18]: df
Out[18]: 
   foo  bar
0    1    4
1    2    5
2    3    6

Это еще одна форма цепочечного присваивания. Обычно это можно переписать в 2 различных формах:

In [19]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [20]: df.replace({"foo": {1: 5}}, inplace=True)

In [21]: df
Out[21]: 
   foo  bar
0    5    4
1    2    5
2    3    6

Другой альтернативой было бы не использовать inplace:

In [22]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [23]: df["foo"] = df["foo"].replace(1, 5)

In [24]: df
Out[24]: 
   foo  bar
0    5    4
1    2    5
2    3    6

Конструкторы теперь по умолчанию копируют массивы NumPy

Конструкторы Series и DataFrame теперь по умолчанию копируют массив NumPy, если не указано иное. Это было изменено, чтобы избежать изменения объекта pandas при изменении массива NumPy на месте вне pandas. Вы можете установить copy=False чтобы избежать этого копирования.

Описание#

CoW означает, что любой DataFrame или Series, производный от другого любым способом, всегда ведет себя как копия. Как следствие, мы можем изменять значения объекта только путем модификации самого объекта. CoW запрещает обновление DataFrame или Series, которые делят данные с другим объектом DataFrame или Series, на месте.

Это позволяет избежать побочных эффектов при изменении значений, и поэтому большинство методов могут избежать фактического копирования данных и запускать копирование только при необходимости.

Следующий пример будет работать на месте с CoW:

In [25]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [26]: df.iloc[0, 0] = 100

In [27]: df
Out[27]: 
   foo  bar
0  100    4
1    2    5
2    3    6

Объект df не разделяет никакие данные с любым другим объектом и, следовательно, копирование не запускается при обновлении значений. В отличие от этого, следующая операция запускает копирование данных при CoW:

In [28]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [29]: df2 = df.reset_index(drop=True)

In [30]: df2.iloc[0, 0] = 100

In [31]: df
Out[31]: 
   foo  bar
0    1    4
1    2    5
2    3    6

In [32]: df2
Out[32]: 
   foo  bar
0  100    4
1    2    5
2    3    6

reset_index возвращает ленивую копию с CoW, в то время как копирует данные без CoW. Поскольку оба объекта, df и df2 используют одни и те же данные, копирование срабатывает при изменении df2. Объект df всё ещё имеет те же значения, что и изначально, в то время как df2 был изменён.

Если объект df больше не нужен после выполнения reset_index операция, вы можете эмулировать операцию на месте через присвоение вывода reset_index в ту же переменную:

In [33]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [34]: df = df.reset_index(drop=True)

In [35]: df.iloc[0, 0] = 100

In [36]: df
Out[36]: 
   foo  bar
0  100    4
1    2    5
2    3    6

Исходный объект выходит из области видимости, как только результат reset_index переназначается и, следовательно, df не делит данные с другими объектами. Копирование не требуется при изменении объекта. Это верно для всех методов, перечисленных в Оптимизации Copy-on-Write.

Ранее, при работе с представлениями, изменялись и представление, и родительский объект:

In [37]: with pd.option_context("mode.copy_on_write", False):
   ....:     df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
   ....:     view = df[:]
   ....:     df.iloc[0, 0] = 100
   ....: 

In [38]: df
Out[38]: 
   foo  bar
0  100    4
1    2    5
2    3    6

In [39]: view
Out[39]: 
   foo  bar
0  100    4
1    2    5
2    3    6

CoW запускает копирование, когда df изменено, чтобы избежать мутации view также:

In [40]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [41]: view = df[:]

In [42]: df.iloc[0, 0] = 100

In [43]: df
Out[43]: 
   foo  bar
0  100    4
1    2    5
2    3    6

In [44]: view
Out[44]: 
   foo  bar
0    1    4
1    2    5
2    3    6

Цепное присваивание#

Цепное присваивание ссылается на технику, при которой объект обновляется через две последовательные операции индексирования, например.

In [45]: with pd.option_context("mode.copy_on_write", False):
   ....:     df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
   ....:     df["foo"][df["bar"] > 5] = 100
   ....:     df
   ....: 

Столбец foo обновляется там, где столбец bar больше 5. Это нарушает принципы CoW, потому что потребуется изменить представление df["foo"] и df за один шаг. Следовательно, цепочечное присваивание никогда не будет работать и вызовет ChainedAssignmentError предупреждение с включенным CoW:

In [46]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})

In [47]: df["foo"][df["bar"] > 5] = 100

С копированием при записи это можно сделать с помощью loc.

In [48]: df.loc[df["bar"] > 5, "foo"] = 100

Массивы NumPy только для чтения#

Доступ к базовому массиву NumPy DataFrame вернёт массив только для чтения, если массив разделяет данные с исходным DataFrame:

Массив является копией, если исходный DataFrame состоит более чем из одного массива:

In [49]: df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})

In [50]: df.to_numpy()
Out[50]: 
array([[1. , 1.5],
       [2. , 2.5]])

Массив разделяет данные с DataFrame, если DataFrame состоит только из одного массива NumPy:

In [51]: df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})

In [52]: df.to_numpy()
Out[52]: 
array([[1, 3],
       [2, 4]])

Этот массив доступен только для чтения, что означает, что его нельзя изменить на месте:

In [53]: arr = df.to_numpy()

In [54]: arr[0, 0] = 100
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[54], line 1
----> 1 arr[0, 0] = 100

ValueError: assignment destination is read-only

То же самое верно для Series, поскольку Series всегда состоит из одного массива.

Существует два возможных решения этой проблемы:

  • Вручную запустите копирование, если хотите избежать обновления DataFrames, которые используют общую память с вашим массивом.

  • Сделать массив доступным для записи. Это более производительное решение, но обходит правила Copy-on-Write, поэтому его следует использовать с осторожностью.

In [55]: arr = df.to_numpy()

In [56]: arr.flags.writeable = True

In [57]: arr[0, 0] = 100

In [58]: arr
Out[58]: 
array([[100,   3],
       [  2,   4]])

Шаблоны, которых следует избегать#

Защитная копия не будет выполнена, если два объекта используют одни и те же данные, пока вы изменяете один объект на месте.

In [59]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

In [60]: df2 = df.reset_index(drop=True)

In [61]: df2.iloc[0, 0] = 100

Это создает два объекта, которые используют общие данные, поэтому операция setitem вызовет копирование. Это не требуется, если исходный объект df больше не требуется. Простое переназначение той же переменной сделает недействительной ссылку, удерживаемую объектом.

In [62]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

In [63]: df = df.reset_index(drop=True)

In [64]: df.iloc[0, 0] = 100

В этом примере копирование не требуется. Создание нескольких ссылок сохраняет ненужные ссылки активными и, следовательно, ухудшит производительность при Copy-on-Write.

Оптимизации Copy-on-Write#

Новый механизм ленивого копирования, который откладывает копирование до тех пор, пока объект не будет изменён, и только если этот объект разделяет данные с другим объектом. Этот механизм был добавлен к методам, которые не требуют копирования базовых данных. Популярные примеры: DataFrame.drop() для axis=1 и DataFrame.rename().

Эти методы возвращают представления при включенной копировании при записи, что обеспечивает значительное повышение производительности по сравнению с обычным выполнением.

Как включить CoW#

Copy-on-Write может быть включен через опцию конфигурации copy_on_write. Опцию можно включить __глобально__ одним из следующих способов:

In [65]: pd.set_option("mode.copy_on_write", True)

In [66]: pd.options.mode.copy_on_write = True