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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Комментарии (0)