Spec-Zone.ru › pandas 2

Копирование при записи (CoW)

Примечание

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

Копирование при записи было впервые представлено в версии 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. Точное поведение трудно предсказать. Копирование при записи решает проблему случайного изменения более чем одного объекта, оно явно запрещает это. При включённом 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

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

Миграция на копирование при записи

Копирование при записи будет значением по умолчанию и единственным режимом в 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

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

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 изменяется inplace вне pandas. Вы можете установить copy=False , чтобы избежать этой копии.

Описание

CoW означает, что любой DataFrame или Series, полученный от другого любым способом, всегда ведёт себя как копия. Вследствие этого, мы можем изменить значения объекта только путем изменения самого объекта. CoW не позволяет обновлять DataFrame или Series, которые совместно используют данные с другим DataFrame или Series объектом inplace.

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

Следующий пример будет работать inplace с 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, вы можете эмулировать операцию inplace, присвоив результат 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 не разделяет данные ни с каким другим объектом. При изменении объекта не требуется копирование. Это в целом верно для всех методов, перечисленных в оптимизациях копирования при записи.

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

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]])

Этот массив только для чтения, что означает, что он не может быть изменён inplace:

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 всегда состоит из одного массива.

Есть два потенциальных решения для этого:

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

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

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]])

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

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

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

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

END_OF_DOCUMENT_MARKER

Оптимизации копирования при записи

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

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

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

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

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

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

© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/2.2.2/user_guide/copy_on_write.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API