Появление в библиотеке pandas режима Copy‑on‑Write (CoW, копирование при записи) — это изменение, нарушающее обратную совместимость, которое окажет некоторое воздействие на существующий код, использующий pandas. Мы разберёмся с тем, как адаптировать код к новым реалиям, сделать так, чтобы он работал бы без ошибок тогда, когда режим CoW будет включён по умолчанию. Сейчас сделать это планируется в версии pandas 3.0, выход которой ожидается в апреле 2024 года. В первом материале из этой серии мы разбирались с особенностями поведения CoW, во втором — говорили об оптимизации производительности, имеющей отношение к новому режиму работы pandas.
Мы планируем добавить в систему «тревожный режим», в котором она будет выдавать предупреждения при выполнении любой операции, поведение которой меняется при включении CoW. Эти предупреждения будут привлекать к себе очень много внимания пользователей, поэтому к возможности их появления стоит относиться с осторожностью. В этом материале рассматриваются некоторые типичные проблемы кода и то, как его можно адаптировать для того чтобы его поведение не изменилось бы после включения 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 оказывает влияние на код. Кроме того, 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
создаст срез данных, на которых основан объект df
. Результат выполнения этой операции присваивается новой переменной — df2
. Это означает, что два этих объекта совместно используют одни и те же данные. Это остаётся в силе до тех пор, пока объект df
не будет уничтожен в ходе сборки мусора. Операция setitem, в результате, вызовет копирование данных. В этом совершенно нет необходимости в том случае, если программисту больше не нужен исходный объект df
. Обычная перезапись значения одной переменной просто сделает недействительной ссылку, которая удерживается объектом.
df = df.reset_index()
df.iloc[0, 0] = 100
Подводя итог, можно сказать, что создание множества ссылок в одном и том же методе приводит к поддержанию в рабочем состоянии ненужных сущностей.
При этом временные ссылки, создаваемые при объединении нескольких методов в цепочку — это вполне нормально.
df = df.reset_index().drop(...)
При таком подходе в рабочем состоянии останется лишь одна ссылка.
Обращение к массиву NumPy, на котором основан объект DataFrame
Сейчас библиотека 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
, и тех объектов, которые используют те же данные, что и этот объект. Всё становится гораздо сложнее при применении CoW, так как это означает отсутствие множества защитных копий данных, существовавших ранее. Гораздо больше объектов 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
, которые совместно используют память, хранящую массив.Сделать массив пригодным для записи. Это решение отличается лучшей производительностью, но оно обходит правила CoW, поэтому им следует пользоваться с осторожностью.
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 — это пример операции, где копирования данных не происходит.
Итоги
Мы рассмотрели наиболее серьёзные изменения pandas, связанные с режимом Copy‑on‑Write. Этот режим станет стандартным в pandas 3.0. Мы, кроме того, поговорили о том, как можно адаптировать код к особенностям CoW, сделать так, чтобы включение этого режима не нарушило бы работу программ. Если вы сможете избежать антипаттернов, описанных в этом материале, это значит, что вы без проблем обновитесь до версии pandas, в которой режим копирования при записи будет включён по умолчанию.
О, а приходите к нам работать? ???? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.