Время прочтения: 6 мин.
Да кто такой этот ваш Polars?!
Polars – это open-source библиотека для работы с данными, очень похожая на Pandas. Только новее и быстрее. За два года существования Polars набрал популярность благодаря своей молниеносной производительности. Это обусловлено тем, что он написан на Rust и использует в своей основе Apache Arrow, который ускоряет загрузку данных и вычисления, уменьшает использование памяти.
Самая интересная, на мой взгляд, особенность Polars – возможность работы в двух режимах:
— Eager mode (нетерпеливый режим) – операции выполняются немедленно, результат выполнения доступен в памяти. Для каждой операции требуется выделить фрейм данных, что не очень хорошо для памяти.
— Lazy mode (ленивый режим) – при запуске кода операции не выполняются сразу же, а добавляются в план запроса. Polars автоматически оптимизирует запрос, то есть считывает последовательность действий и ищет способы ускорить запрос и уменьшить использование памяти. В lazy mode есть возможность параллельного выполнения.
Pandas, в отличие от Polars, работает только в eager mode. Это означает, что он менее производительный и использует память не самым оптимальным образом.
Polars != Pandas
Работать в Polars после Pandas будет легко, потому что синтаксис у этих библиотек практически одинаковый. Но есть небольшие различия, которые я рассмотрю далее.
Импортирую Pandas и Polars.
import pandas as pd
import polars as pl
Для демонстрации возможностей библиотек был взят набор данных, содержащий информацию о продаже автомобилей. Датасет состоит из 500 000 строк и 17 столбцов, ссылка на датасет тут.
Считываю данные из csv файла с помощью Pandas…
df_pd = pd.read_csv('cars.csv', encoding='utf-8')
df_pd.head(5)
…и с помощью Polars:
df_pl = pl.read_csv('cars.csv',
encoding='utf8',
dtypes={'engineDisplacement': str})
df_pl.head(5)
Сразу можно отметить два отличия DataFrame в Polars:
— тип данных каждого столбца указывается под его заголовком
— нет индексов у строк
Вопреки отсутствию индексации в Polars, к данным всё-таки можно обращаться по индексам в квадратных скобках вместо привычной для Pandas функции iloc.
Синтаксис для Pandas:
df_pd.iloc[15]
df_pd.iloc[10:15]
На мой взгляд, синтаксис для Polars проще:
df_pl[15, :]
df_pl[10:15, :]
Несмотря на то, что можно обращаться к данным по индексу, этот функционал в Polars считается антипаттерном и, возможно, будет удалён в будущих версиях библиотеки. А в lazy mode он вообще не предусмотрен. Отсутствие индексации, по мнению создателей Polars, значительно упрощает работу с данными.
Лучший способ обращения к данным в Polars – это использование выражений с select (для выбора столбцов) и filter (для выбора строк).
Выбираю определенные столбцы из DataFrame.
Синтаксис для Polars:
df_pl.select(['brand', 'color', 'mileage', 'price']).head(5)
Синтаксис для Pandas:
df_pd[['brand', 'color', 'mileage', 'price']].head(5)
В select() можно указывать не только названия столбцов, но и выражения, а также назначать алиасы.
Синтаксис для Polars:
df_pl.select([
pl.col('price').max().alias('price_max'),
pl.col('brand').n_unique().alias('brand_names_count'),
pl.col('mileage').min().alias('mileage_min')
])
Аналогичная операция в Pandas требует создания нового DataFrame, что сложнее по синтаксису и занимает дополнительную память.
df_pd_select = pd.DataFrame(columns=['price_max', 'brand_names_count',
'mileage_min'])
df_pd_select['price_max'] = [df_pd['price'].max()]
df_pd_select['brand_names_count'] = [df_pd['brand'].nunique()]
df_pd_select['mileage_min'] = [df_pd['mileage'].min()]
df_pd_select
Выбираю строки, в которых содержится информация о продаже автомобилей Toyota, которые были выпущены после 2015 года, со стоимостью от 4 млн рублей.
Синтаксис для Polars:
df_pl.filter((pl.col('brand') == 'Toyota')
& (pl.col('year') > 2015)
& (pl.col('price') >= 4000000)).sort('price').limit(3)
Синтаксис для Pandas:
df_pd.loc[(df_pd['brand'] == 'Toyota')
& (df_pd['year'] > 2015)
& (df_pd['price'] >= 4000000)].sort_values('price').head(3)
Сила в лени
Если быть точными, скорость в лени. Как уже говорилось, в Polars есть lazy mode. Рассмотрю, насколько быстро происходит обработка данных в этом режиме.
Из набора данных необходимо выбрать чёрные Mercedes-Benz Седан, выпущенные после 2010 года.
Для того, чтобы переключиться в lazy mode, необходимо у DataFrame вызвать метод lazy(). И он превратится… в LazyFrame. Далее можно выполнять необходимые операции, но они не будут запущены, пока не будет вызван метод collect().
q = (
df_pl.lazy()
.drop_nulls()
.filter(
(pl.col('brand') == 'Mercedes-Benz')
& (pl.col('color') == 'Черный')
& (pl.col('bodyType') == 'Седан')
& (pl.col('year') > 2010))
.sort(['year', 'price'])
.limit(5)
)
%time q.collect()
Выполню этот же запрос без преобразования изначального DataFrame в LazyFrame. Можно заметить, что выполнился он медленнее.
%time df_pl.drop_nulls()
.filter(
(pl.col('brand') == 'Mercedes-Benz')
& (pl.col('color') == 'Черный')
& (pl.col('bodyType') == 'Седан')
& (pl.col('year') > 2010))
.sort(['year', 'price'])
.limit(5)
Создатели библиотеки рекомендуют пользоваться преимуществами lazy mode и считывать файлы сразу в LazyFrame, заменив метод read_csv() на scan_csv().
lazy_df_pl = pl.scan_csv('cars.csv',
encoding='utf8',
dtypes={'engineDisplacement': str})
lazy_df_pl
В таком случае данные не будут загружены в память полностью, Polars просканирует их и выдаст объект LazyFrame. Извлечь данные из LazyFrame можно, применив метод fetch(n), который покажет n строк.
lazy_df_pl.fetch(5)
Выполню запрос, который был приведен выше, к полученному LazyFrame. Можно отметить, что несмотря на свою «ленивость», он выполнялся дольше, чем предыдущие эксперименты.
q1 = (
lazy_df_pl.drop_nulls()
.filter(
(pl.col('brand') == 'Mercedes-Benz')
& (pl.col('color') == 'Черный')
& (pl.col('bodyType') == 'Седан')
& (pl.col('year') > 2010))
.sort(['year', 'price'])
.limit(5)
)
%time q1.collect()
Pandas аналогичную операцию выполняет значительно медленнее, чем Polars в любом режиме:
%time df_pd_merc = df_pd.dropna()
.loc[(df_pd['brand'] == 'Mercedes-Benz')
& (df_pd['color'] == 'Черный')
& (df_pd['bodyType'] == 'Седан')
& (df_pd['year'] > 2010)]
.sort_values(['year', 'price'])
df_pd_merc.head(5)
Итак, сделаю выводы. Отсутствие индексации в Polars никак не влияет на получение данных из DataFrame. А главным отличием и преимуществом Polars является lazy mode, который выдает хорошие показатели производительности. Его можно использовать для более быстрой работы с тяжёлыми файлами. Надеюсь, моя публикация вдохновит вас попробовать Polars в своих задачах.