Многофункциональная и универсальная библиотека 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.
Читайте также:
- Pandas: взгляд изнутри
- 8 ключевых команд для управления средами Conda
- 8 структур данных, которые должен знать каждый дата-сайентист
Читайте нас в Telegram, VK и Дзен
Перевод статьи Miriam Santos: Pandas 2.0: A Game-Changer for Data Scientists?