Глубокое погружение в режим Copy-on-Write в pandas. Часть 3

Первая и вторая части статьи.

Введение

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

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

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

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

import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3]})

df["x"][df["x"] > 1] = 100

Первая операция выбирает столбец "x", а вторая ограничивает количество строк. Существует множество различных комбинаций этих операций (например, в сочетании с loc и iloc). Впрочем, ни одна из этих комбинаций не будет работать при активированном CoW. Вместо того чтобы бездействовать, система просто выдаст предупреждение ChainedAssignmentError для удаления данных шаблонов.

В качестве замены можно использовать loc:

df.loc[df["x"] > 1, "x"] = 100

Первое измерение loc всегда соответствует row-indexer. Это означает, что можно выбрать подмножество строк. Второе измерение соответствует column-indexer, что позволяет выбрать подмножество столбцов.

В целом использование loc помогает ускорить работу, когда требуется установить значения в подмножество строк, что позволяет очистить код и повысить производительность.

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

df["x"].replace(1, 100)

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

df = df.replace({"x": 1}, {"x": 100})

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

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

df2 = df.reset_index()
df2.iloc[0, 0] = 100

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

df = df.reset_index()
df.iloc[0, 0] = 100

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

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

df = df.reset_index().drop(...)

При этом сохраняется только одна ссылка.

Доступ к базовому массиву NumPy

В настоящее время pandas предоставляет доступ к базовому массиву NumPy через to_numpy или .values. Возвращаемый массив является копией, если DataFrame состоит из различных типов данных, например:

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

[[1. 1.5]
[2. 2.5]]

DataFrame поддерживается двумя массивами, которые необходимо объединить в один. Это запускает копирование.

Другой случай  —  DataFrame, который поддерживается только одним массивом NumPy. Например:

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

[[1 3]
[2 4]]

Мы можем напрямую обратиться к массиву и получить представление вместо копии. Это гораздо быстрее, чем копирование всех данных. Теперь мы имеем возможность оперировать с массивом NumPy и потенциально изменять его в памяти, что также приведет к обновлению DataFrame и, возможно, всех других DataFrame, имеющих общие данные. В режиме Copy-on-Write процедура становится гораздо сложнее, поскольку мы удалили множество защитных копий. Теперь куда большее число DataFrame будут совместно использовать память.

Поэтому to_numpy и .values вернут массив, доступный только для чтения. Таким образом, результирующий массив нельзя будет записать.

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

arr[0, 0] = 1

Это запускает ValueError:

ValueError: assignment destination is read-only

Такой ситуации можно избежать двумя способами.

  • Запустите копирование вручную, если не хотите, чтобы обновлялись DataFrame, которые имеют общую память с массивом.
  • Сделайте массив доступным для записи. Это более производительное решение, но оно обходит правила Copy-on-Write, поэтому его следует использовать с осторожностью.
arr.flags.writeable = True

Бывают случаи, когда это невозможно. Чаще всего такое происходит, если вы обращаетесь к одному столбцу, который поддерживался PyArrow:

ser = pd.Series([1, 2], dtype="int64[pyarrow]")
arr = ser.to_numpy()
arr.flags.writeable = True

Возвращается ValueError:

ValueError: cannot set WRITEABLE flag to True of this array

Массивы Arrow неизменяемы, поэтому сделать массив NumPy доступным для записи не представляется возможным. В таком случае преобразование из Arrow в NumPy выполняется с нулевым копированием.

Заключение

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Patrick Hoefler: Deep Dive into Pandas Copy-on-Write Mode — Part III

Предыдущая статьяКомпиляция TypeScript в нативный код
Следующая статьяLangChain + Streamlit + LlaMA: установка диалогового бота с ИИ на локальный компьютер