Pandas может делать все. Практически все. Но (как бы ни хотелось, чтобы было иначе) ему не хватает скорости. Pandas просто не поспевает за темпами, соответствующими увеличению объемов и сложности современных наборов данных.

Создатель Pandas Уэс Маккинни утверждает, что придерживался следующего правила при разработке своей библиотеки:

Объем оперативной памяти должен в 5–10 раз превышать объем набора данных.

Возможно, вы игнорировали это правило, когда набор данных Iris только появился, но сегодня все иначе. Вы просто не сможете загрузить набор данных объемом 100 ГБ (что уже обычное дело в наше время), если ваша оперативная память строго ограничена 64 ГБ.

Конечно, есть отличные альтернативы, такие как Dask. Но Dask не реализует новую функциональность. Он растягивает синтаксис Pandas на несколько процессов (потоков) и игнорирует основные проблемы производительности и памяти.

Polars, о котором пойдет речь в этой статье, был написан на языке Rust с нуля, чтобы исправить все недостатки Pandas. Он уже быстрее, чем готовящийся к выпуску Pandas 2.0 с PyArrow-бекэндом.

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


0. Чтение/запись данных

CSV, несмотря на свою досадную медлительность, по-прежнему является одним из самых популярных форматов файлов для хранения данных. Итак, начнем с функции read_csv в Polars.

Кроме очевидного преимущества в скорости, она отличается от своего “собрата” из Pandas только количеством параметров (у Pandas в read_csv их 49) и синтаксисом.

Изображение автора

Названия параметров не должны быть проблемой, так как большинство современных IDE имеют функции заполнения вкладок или всплывающей документации (например, шорткат Shift + Tab в JupyterLab).

Если вы не знали, параметр dtype не позволяет Pandas устанавливать автоматические типы данных, предоставляя пользователю возможность задавать собственные, например тип string для cut и datetime для столбцов типа date.

Вы можете использовать то же поведение в Polars с помощью dtypes (обратите внимание на “s”), хотя он не позволяет задавать типы через строки. Вам придется предоставить либо встроенные типы Python, либо типы Polars, такие как pl.Boolean, pl.Categorical, pl.DateTime, pl.Int64 и pl.Null для отсутствующих значений. Полный список можно посмотреть, вызвав dir(pl).

Чтение и запись файлов Parquet, которые намного быстрее и экономнее расходуют память, чем CSV, также поддерживаются в Polars с помощью функций read_parquet и write_parquet.

Изображение автора

1. Создание структур данных Series и DataFrames

Чтение данных из файлов применяется не всегда. Как и в Pandas, в Polars можно создавать DataFrames и Series с нуля, а синтаксис практически идентичен:

Изображение автора

В Polars также есть много имен и методов DataFrames, по поведению (почти) идентичных DataFrames в Pandas. Итак, встречайте:

  • apply (применение пользовательских функций к каждой строке DataFrame);
  • corr (корреляционная матрица);
  • describe (сводная статистика, сводка из 5 чисел);
  • drop (удаление столбцов из DataFrame);
  • explode (распаковка заданного столбца в длинный формат; когда ячейки содержат несколько значений, например [1, 2, 3]);
  • head, tail и sample(n) (получение различных представлений DataFrame: верхнее, нижнее, случайное);
  • iter_rows (возвращает итератор строк DataFrame, содержащих значения на языке Python);
  • max, mean, median, sum, std и обычный комплект распространенных математических и статистических функций.

Полный список методов DataFrame в Polars смотрите на этой странице документации.


2. Понимание выражений в Polars

В основе Polars лежит система обработки запросов, которая работает с пользовательскими выражениями. Система обработки запросов и выражения  —  два важнейших компонента, обеспечивающих молниеносную производительность, которой свойственен, как сказано в руководстве пользователя Polars, “поразительный параллелизм”.

Вы будете шокированы тем, насколько выражения Polars напоминают SQL, сохраняя при этом прочную связь со знакомым синтаксисом Pandas.

Подобно SQL-запросам, вы можете писать выражения для решения многих задач, включая:

  • создание новых столбцов из существующих;
  • получение представлений данных после некоторого преобразования;
  • сводная статистика;
  • обработка и очистка данных;
  • использование операторов GroupBy
df.filter(pl.col('column') == 'some_value')

В приведенном выше запросе выражением является pl.col('column)' == 'some_value', и оно, как вы догадались, фильтрует DataFrame по строкам, где column равен some_value.

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

type(pl.col("column") == "some_value")
polars.expr.expr.Expr

Это потому, что выражения оцениваются только в соответствии с определенными контекстами. В Polars есть три основных контекста:

  1. Выборка данных (Selecting data). В контексте select выражения применяются к столбцам и должны создавать столбцы одинаковой длины в результате. Это поведение должно быть знакомо вам еще со времен работы с SQL. Функция filter также привязана к этому контексту.
  2. Группирование данных (Grouping data). В контексте groupby выражения работают над группами, а результаты могут иметь любую длину, поскольку группа может иметь много членов.
  3. Добавление новых столбцов (Adding new columns). В этом контексте выражения используются для создания новых столбцов с нуля или из уже существующих.

Рассмотрим каждый контекст подробнее.


3. Выборка данных

Нотация скобок в Pandas уступает место выражениям в Polars для выбора столбцов.

Чтобы выбрать один столбец, можно использовать его литеральное имя внутри select или воспользоваться рекомендуемой функцией pl.col для ссылки на столбцы.

Для множественного выбора можно перечислить имена столбцов через запятую внутри pl.col или в виде списка pl.col-ссылок внутри select. Различия между этими синтаксисами рассмотрим чуть позже.

Изображение автора

Для выбора данных Polars предлагает функциональность, недоступную в таком объеме в Pandas. Например, в Polars можно исключить столбцы из выборки с помощью exclude:

df.select(pl.exclude("price")).head()
Изображение автора

Можно также использовать регулярные выражения между символами ^ и $. Ниже показано, как выбрать все столбцы, которые начинаются с буквы c:

df.select(pl.col("^c.+$")).head()
Изображение автора

Polars предоставляет возможность разбить на подмножества DataFrame на основе типа данных, что может напомнить вам select_dtypes из Pandas (слева):

Изображение автора

Чтобы выбрать все числовые столбцы, используем типы Int64 и Float64 внутри pl.col.


4. Фильтрация данных

В Polars можно использовать функцию filter для разбития DataFrames на подмножества с булевым индексированием. Например, использование функции is_between для столбца создает выражение для фильтрации числовых столбцов в определенном диапазоне.

Изображение автора

Вы можете объединить несколько условных выражений с помощью знакомых булевых операторов & (AND) и | (OR). В приведенном ниже примере выбираем строки, в которых столбец color (цвет) имеет значение либо ‘E’, либо ‘J’, а price (цена бриллиантов) ниже 500:

Изображение автора

Обратите также внимание, как используется is_in в Polars (справа).


5. Создание новых столбцов

В Polars можно создавать новые столбцы в контексте with_columns. В примере ниже new_col определяется с помощью pl.col('price') ** 2. Столбцу также присваивается псевдоним, и так он получает имя (та же функция, что и у ключевого слова as в SQL).

Изображение автора

Во втором примере объединяем два столбца (хотя это и не имеет смысла), чтобы продемонстрировать, как с помощью Polars можно объединять целочисленные и строковые столбцы. Вы можете использовать любые нативные функции и операторы Python или сторонних разработчиков для столбцов, на которые ссылается pl.col.

Если хотите, чтобы новый столбец был вставлен в DataFrame, переопределите исходную переменную df.

Строковые колонки в Polars имеют знакомый интерфейс .str для специальных функций работы с текстом, таких как contains и lengths. Полный список можно посмотреть здесь. Есть также интерфейсы .cat.dt и .arr для специализированных категориальных, временных функций и функций работы с массивами.


6. Функция groupby

Не думаю, что можно обойтись без упоминания операций Groupby:

Изображение автора

При использовании функции groupby в Polars не забудьте указать maintain_order=True, чтобы группы не отображались беспорядочно. Также, в отличие от Pandas, выражение groupby(col_name) работает только с заданным столбцом. Чтобы сгруппировать все столбцы по col_name, необходимо использовать контекст aggregation. Вот его синтаксис:

df.groupby(
"cut", maintain_order=True
).agg(pl.col("*").count())

После контекста groupby подключаете контекст aggregation и указываете, какие столбцы он затрагивает. Затем подключаете любую функцию к результату, например count.

Вот еще один пример, в котором показана группировка по качеству огранки бриллиантов и возвращается среднее числовое значение для каждой группы:

Изображение автора

Чтобы узнать больше о расширенных выражениях groupby в Polars, перейдите по этой ссылке.


7. Ленивый API в Polars

Одна из крутых особенностей Polars  —  ленивый API. В нем запросы не выполняются построчно, а обрабатываются системой обработки запросов по методу “из конца в конец”.

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

import polars as pl

df = pl.read_csv("data/diamonds.csv")

query = df.lazy().filter(
pl.col("cut") == "Ideal"
)

type(query)
polars.lazyframe.frame.LazyFrame

При добавлении функции lazy() перед подключением выражения к цепочке, DataFrame становится LazyFrame. В этот момент запрос не выполняется, и можно соединить в цепочку еще несколько выражений. Когда все готово, вызываете функцию collect(), чтобы получить результат:

query.collect().head()
Изображение автора

В режиме eager в Polars работа и так идет быстро, а ленивый режим добавляет системе обработки запросов дополнительное (тройное) ускорение.

Если хотите сделать ленивый API настройкой по умолчанию, используйте функции scan_* при чтении данных вместо read_*:

df = pl.scan_csv("data/diamonds.csv")

q1 = df.filter(
pl.col("cut") == "Ideal"
)

q1.collect().head()

Таким образом, вам не придется каждый раз писать функцию lazy().

Если набор данных превышает возможности вашей оперативной памяти, можно использовать потоковую передачу, при которой Polars будет обрабатывать данные партиями. Эта функция включается в lazy API установкой streaming=True внутри collect. Узнайте больше об этой функции здесь.


Заключение

Polars пока новичок (я имею в виду, что он только осваивается в мире программирования), но уже очень популярен. Только взгляните на его конкурентов в категории общедоступного ПО:

  1. Pandas, выпущенный в 2011 году, имеет 37,5 тыс. звезд на GitHub.
  2. Apache Spark, выпущенный в 2014 году, имеет 26,8 тыс. звезд.
  3. Vaex, выпущенный в 2017 году, имеет 7,9 тыс. звезд.
  4. Dask, выпущенный в 2015 году, имеет 10,9 тыс. звезд.
  5. Apache Arrow, выпущенный в 2016 году, имеет 11,4 тыс. звезд.

Для сравнения: Polars был выпущен в 2020 году и уже набрал 15,9 тыс. звезд, то есть уже находится на полпути к своим конкурентам.

Возможно, ситуация изменится, когда выйдет Pandas 2.0, но, на мой взгляд, Polars уже сейчас является достойным соперником Pandas.

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

Читайте нас в TelegramVK и Дзен


Перевод статьи Bex T.: 7 Easy Steps To Switch From Pandas to Lightning Fast Polars And Never Return

Предыдущая статьяЗагрузочные представления в SwiftUI
Следующая статьяКак развернуть GitLab с помощью Docker за 5 секунд