Многофункциональная и универсальная библиотека pandas заняла достойное место в сердце каждого дата-сайентиста.

Практически невозможно представить себе работу с данными  —  начиная с их ввода/вывода до очистки и преобразования  —  без import pandas as pd. И теперь прежняя pandas уступила место новой и прорывной pandas 2.0!

Интересный факт: этот релиз готовился в течение 3 лет.

Так что же нового предлагает pandas 2.0? Окунемся в эту версию с головой.

1. Производительность, скорость и эффективность использования памяти

Как известно, pandas была создана на основе библиотеки numpy, которая не была специально разработана как бэкенд для библиотек датафреймов. По этой причине одним из основных слабых мест pandas стала обработка больших массивов данных в памяти.

В этом релизе значительные изменения связаны с появлением бэкенда Apache Arrow для данных pandas.

Arrow  —  это стандартизованный формат столбцовых данных in-memory (с хранением в оперативной памяти) с доступными библиотеками для нескольких языков программирования (C, C++, R, Python и другие). Для Python создан пакет PyArrow, основанный на реализации Arrow в C++, а значит, быстрый!

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

Сравним скорость чтения данных без бэкенда pyarrow и с ним на примере набора данных Hacker News размером около 650 МБ (лицензия CC BY-NC-SA 4.0):

%timeit df = pd.read_csv("data/hn.csv")
# 12 с ± 304 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

%timeit df_arrow = pd.read_csv("data/hn.csv", engine='pyarrow', dtype_backend='pyarrow')
# 329 мс ± 65 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

При использовании нового бэкенда чтение данных происходит почти в 35 раз быстрее. Следует отметить и другие моменты:

  • Без бэкенда pyarrow каждый столбец/признак хранится как собственный уникальный тип данных: числовые признаки хранятся как int64 или float64, а строковые значения  —  как объекты.
  • При использовании pyarrow все признаки применяют dtypes Arrow: обратите внимание на аннотацию [pyarrow]и различные типы данных: int64, float64, string, timestamp и double.
df = pd.read_csv("data/hn.csv")
df.info()

# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 3885799 записей, от 0 до 3885798
# Столбцы данных (всего 8 столбцов):
# # Column Dtype
# --- ------ -----
# 0 Object ID int64
# 1 Title object
# 2 Post Type object
# 3 Author object
# 4 Created At object
# 5 URL object
# 6 Points int64
# 7 Number of Comments float64
# dtypes: float64(1), int64(2), object(5)
# использование памяти: 237.2+ MB

df_arrow = pd.read_csv("data/hn.csv", dtype_backend='pyarrow', engine='pyarrow')
df_arrow.info()

# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 3885799 записей, от 0 до 3885798
# Столбцы данных (всего 8 столбцов):
# # Column Dtype
# --- ------ -----
# 0 Object ID int64[pyarrow]
# 1 Title string[pyarrow]
# 2 Post Type string[pyarrow]
# 3 Author string[pyarrow]
# 4 Created At timestamp[s][pyarrow]
# 5 URL string[pyarrow]
# 6 Points int64[pyarrow]
# 7 Number of Comments double[pyarrow]
# dtypes: double[pyarrow](1), int64[pyarrow](2), string[pyarrow](4), timestamp[s][pyarrow](1)
# memory usage: 660.2 MB

2. Типы данных arrow и индексы numpy

Помимо чтения данных, что является простейшим случаем, можно ожидать дополнительных улучшений для ряда других операций, особенно для строковых, поскольку реализация типа данных string в pyarrow достаточно эффективна:

%timeit df["Author"].str.startswith('phy')
# 851 мс ± 7.89 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

%timeit df_arrow["Author"].str.startswith('phy')
# 27.9 мс ± 538 µс на цикл (среднее ± стандартное отклонение 7 прогонов, по 10 циклов в каждом)

На самом деле Arrow имеет большую (и лучшую, чем у numpy) поддержку типов данных, необходимых за пределами научных (числовых) рамок: даты и время, длительность, двоичные, десятичные числа, списки и карты. Беглый просмотр особенностей эквивалентности между типами данных бэкенда pyarrow и numpy может оказаться неплохим упражнением на случай, если вам понадобится их использовать.

Также появилась возможность хранить в индексах больше числовых типов numpy. Традиционные int64, uint64 и float64 освободили место для всех числовых dtypes-Index-значений numpy, и можно, например, указать их 32-битную версию в качестве альтернативы:

pd.Index([1, 2, 3])
# Index([1, 2, 3], dtype='int64')

pd.Index([1, 2, 3], dtype=np.int32)
# Index([1, 2, 3], dtype='int32')

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


3. Упрощение обработки пропущенных значений

Будучи построенной на базе numpy, pandas не может гибко и без проблем обрабатывать пропущенные значения, поскольку numpy не поддерживает значения null для некоторых типов данных.

Например, целые числа автоматически преобразуются в числа с плавающей точкой, что не является идеальным вариантом:

df = pd.read_csv("data/hn.csv")

points = df["Points"]
points.isna().sum()
# 0

points[0:5]
# 0 61
# 1 16
# 2 7
# 3 5
# 4 7
# Name: Points, dtype: int64

# Установка первой позиции в None
points.iloc[0] = None

points[0:5]
# 0 NaN
# 1 16.0
# 2 7.0
# 3 5.0
# 4 7.0
# Name: Points, dtype: float64

Обратите внимание, как points автоматически меняется с int64 на float64 после введения единственного значения None.

Нет ничего хуже для потока данных, чем неправильные наборы типов, особенно в парадигме ориентированного на данные ИИ.

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

Например, в сообществе Data-Centric AI мы сейчас работаем над проектом, посвященным синтетическим данным для обеспечения конфиденциальности информации. Одна из характеристик, NOC (количество детей), имеет пропущенные значения, поэтому при загрузке данных она автоматически преобразуется в float. При передаче данных в генеративную модель в виде float мы можем получить на выходе десятичные значения, например 2,5. Если вы не математик с двумя детьми, новорожденным младенцем и своеобразным чувством юмора, то иметь 2,5 ребенка  —  это не нормально.

В pandas 2.0 можно использовать dtype = 'numpy_nullable', где пропущенные значения учитываются без каких-либо изменений dtype, что позволяет сохранить исходные типы данных (в данном случае int64):

df_null = pd.read_csv("data/hn.csv", dtype_backend='numpy_nullable')

points_null = df_null["Points"]
points_null.isna().sum()
# 0

points_null[0:5]
# 0 61
# 1 16
# 2 7
# 3 5
# 4 7
# Name: Points, dtype: Int64

points_null.iloc[0] = None

points_null[0:5]
# 0 <NA>
# 1 16
# 2 7
# 3 5
# 4 7
# Name: Points, dtype: Int64

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


4. Оптимизации Copy-on-Write

В Pandas 2.0.0. также добавлен новый механизм ленивого копирования, который откладывает копирование объектов DataFrames и Series до тех пор, пока они не будут изменены.

Это означает, что при включении функции Copy-on-Write (копирование при записи) определенные методы будут возвращать представления, а не копии, что повышает эффективность использования памяти за счет минимизации ненужного дублирования данных.

Это также означает, что нужно быть особенно внимательным при использовании цепочечных присвоений.

При включении режима Copy-on-Write цепочечные присвоения не будут работать, поскольку они указывают на временный объект, являющийся результатом операции индексирования (в режиме Copy-on-Write ведет себя как копирование).

Если функция copy_on_write отключена, то при изменении нового датафрейма такие операции, как нарезка, могут изменить исходный df:

pd.options.mode.copy_on_write = False # disable copy-on-write (default in pandas 2.0)

df = pd.read_csv("data/hn.csv")
df.head()

# Выбрасывает предупреждение 'SettingWithCopy'
# SettingWithCopyWarning:
# Попытка установить значение на копию фрагмента из DataFrame
df["Points"][0] = 2000

df.head() # <---- df меняется

Если функция copy_on_write включена, то при присвоении создается копия, и поэтому исходный датафрейм никогда не изменяется. Во избежание неявных багов, в Pandas 2.0. в таких ситуациях будет выдаваться ошибка ChainedAssignmentError:

pd.options.mode.copy_on_write = True

df = pd.read_csv("data/hn.csv")
df.head()

# Выбрасывает ошибку ChainedAssignmentError
df["Points"][0] = 2000

# ChainedAssignmentError: попытка установить значение для копии DataFrame
# или Series через цепочечное присвоение. При использовании режима Copy-on-Write
# такое цепочечное присвоение никогда не изменит исходный DataFrame
# или Series, поскольку промежуточный объект, для которого мы устанавливаем
# значения, всегда ведет себя как копия.
# Попробуйте вместо этого использовать '.loc[row_indexer, col_indexer] = value',
# чтобы выполнить присвоение в один шаг.

df.head() # <---- df не меняется

5. Опциональные зависимости

При использовании pip версия 2.0. предоставляет возможность устанавливать опциональные зависимости, что является плюсом с точки зрения настройки и оптимизации ресурсов.

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

Кроме того, эта фича избавляет от проблем с зависимостями, снижая вероятность возникновения проблем совместимости и конфликтов с другими пакетами, используемыми в средах разработки:

pip install "pandas[postgresql, aws, spss]>=2.0.0"

Новый релиз на практике

И все же у меня оставались сомнения: действительно ли новый релиз оправдывает возлагаемые на него надежды? Хотелось посмотреть, применимы ли значительные улучшения pandas 2.0 к ряду пакетов, которые я использую ежедневно: ydata-profiling, matplotlib, seaborn и scikit-learn.

Я решил попробовать ydata-profiling, в который уже добавили поддержку pandas 2.0. Используя новый релиз, юзеры могут быть уверены: их конвейеры не сломаются, если они будут задействовать pandas 2.0, а это уже большой плюс.

Честно говоря, ydata-profiling  —  один из моих любимых инструментов для разведочного анализа данных. К тому же это эффективный и быстрый бенчмарк  —  1 строка кода на моей стороне, а “под капотом” полно вычислений, которые мне, как дата-сайентисту, необходимо отрабатывать  —  описательная статистика, построение гистограмм, анализ корреляций и так далее.

Что же может быть лучше, чем проверить движок pyarrow на всем этом сразу и с минимальными усилиями?

import pandas as pd
from ydata_profiling import ProfileReport

# Использование pandas 1.5.3 и ydata-profiling 4.2.0
%timeit df = pd.read_csv("data/hn.csv")
# 10.1 с ± 215 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

%timeit profile = ProfileReport(df, title="Pandas Profiling Report")
# 4.85 мс ± 77.9 µс на цикл (среднее ± стандартное отклонение 7 прогонов, по 100 циклов в каждом)

%timeit profile.to_file("report.html")
# 18.5 мс ± 2.02 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

# Использование pandas 2.0.2 и ydata-profiling 4.3.1
%timeit df_arrow = pd.read_csv("data/hn.csv", engine='pyarrow')
# 3.27 с ± 38.1 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

%timeit profile_arrow = ProfileReport(df_arrow, title="Pandas Profiling Report")
# 5.24 мс ± 448 µс на цикл (среднее ± стандартное отклонение 7 прогонов, по 100 циклов в каждом)

%timeit profile_arrow.to_file("report.html")
# 19 мс ± 1.87 мс на цикл (среднее ± стандартное отклонение 7 прогонов, по 1 циклу в каждом)

Опять же, чтение данных однозначно лучше на движке pyarrow, хотя создание профиля данных по скорости существенно не изменилось.

Однако различия могут заключаться в эффективности использования памяти, для чего необходимо провести другой анализ. Кроме того, можно было бы дополнительно изучить тип проводимого над данными анализа: для некоторых операций разница между версиями 1.5.2 и 2.0 кажется незначительной.

А вот главное, на что я обратил внимание: ydata-profiling пока не использует типы данных pyarrow. Это обновление может оказать огромное влияние как на скорость, так и на использование памяти, и я с нетерпением жду будущих разработок.


Вердикт: производительность, гибкость и совместимость

Новый релиз pandas 2.0 обеспечивает большую гибкость и оптимизацию производительности за счет неочевидных, но очень важных изменений “под капотом”.

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

Подводя итог, выделим основные преимущества новой версии.

  • Оптимизация производительности. За счет внедрения бэкенда Apache Arrow, большего количества dtype-индексов numpy и режима Copy-on-Write.
  • Дополнительная гибкость и настройка. Возможность управления опциональными зависимостями и использование преимуществ типов данных Apache Arrow (включая null-допустимость с самого начала).
  • Интероперабельность. Это, пожалуй, наименее “громкое” преимущество новой версии, но оно имеет огромное значение. Поскольку Arrow не зависит от языка, данные in-memory можно передавать между программами, построенными не только на Python, но и на R, Spark и других, использующих бэкенд Apache Arrow.

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

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


Перевод статьи Miriam Santos: Pandas 2.0: A Game-Changer for Data Scientists?

Предыдущая статья5 недооцененных возможностей JavaScript
Следующая статьяПромпт-инжиниринг: как использовать LLM для создания приложений