Введение
В настоящее время включенный по умолчанию режим 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. Если избегать указанных шаблонов, процесс обновления пройдет достаточно гладко.
Читайте также:
- Pandas 2.0.0 — геймчейнджер в работе дата-сайентистов?
- Pandas: взгляд изнутри
- Пакетная обработка 22 ГБ данных о транзакциях с помощью Pandas
Читайте нас в Telegram, VK и Дзен
Перевод статьи Patrick Hoefler: Deep Dive into Pandas Copy-on-Write Mode — Part III