Библиотека Polars - это невероятно быстрая библиотека DataFrame для управления структурированными данными. Ядро написано на Rust и доступно для Python.
Документация Polars: https://docs.pola.rs/user-guide/getting-started/
# создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # ставим модуль `Polars` (VirtualEnv):~$ python3 -m pip install -U polars
Используя приведенную выше команду, установится ядро Polars. Однако в зависимости от варианта использования, можно установить дополнительные зависимости. Они сделаны необязательными, чтобы минимизировать занимаемую память.
Пример установки Polars с зависимостями:
(VirtualEnv):~$ python3 -m pip install -U 'polars[numpy,fsspec]'
Список доступных зависимостей Polars:
Зависимость | Описание |
all | Все дополнительные зависимости |
pandas | Установит вместе с Pandas для преобразования данных в и из Pandas Dataframes/Series. |
numpy | Установит с numpy для преобразования данных в массивы numpy и обратно. |
pyarrow | Чтение форматов данных с помощью PyArrow |
fsspec | Поддержка чтения из удаленных файловых систем |
connectorx | Поддержка чтения из баз данных SQL |
xlsx2csv | Поддержка чтения из файлов Excel |
deltalake | Поддержка чтения из таблиц Delta Lake |
plot | Поддержка построения изображений Dataframes |
timezone | Поддержка часового пояса необходима только в том случае, если используется Python 3.9 и/или используется Windows, в противном случае зависимости не будут установлены. |
Здесь изложены ключевые моменты, которые должен знать каждый, кто имеет опыт общения с pandas и хочет попробовать Polars. Включены как различия в концепциях, на которых построены библиотеки, так и различия в том, как следует писать код Polars по сравнению с кодом Pandas.
Index
/MultiIndex
Библиотека pandas присваивает каждой строке метку с индексом. Polars не использует индекс, и каждая строка индексируется по ее целочисленной позиции в таблице.
Целью Polars является получение предсказуемых результатов и читаемых запросов, поэтому считается, что индекс не поможет достичь этой цели. Разработчики Polars считают, что семантика запроса не должна меняться в зависимости от состояния индекса или вызова reset_index
.
В Polars DataFrame
всегда будет двумерной таблицей с разнородными типами данных. Типы данных могут иметь вложенность, но сама таблица - нет. Такие операции, как повторная выборка, будут выполняться специализированными функциями или методами, которые действуют как "глаголы" в таблице, явно указывая столбцы, с которыми работает этот "глагол". Таким образом, отсутствие индексов делает вещи более простыми, более явными, более читабельными и менее подверженными ошибкам.
Обратите внимание, что "индексная" структура данных, известная в базах данных, будет использоваться Polars в качестве метода оптимизации.
Внимание! В данное время pandas тестирует представление данных в памяти с помощью Apache Arrow. С версии Pandas 3.0 этот способ будет доступен всем пользователям.
Polars представляет данные в памяти с помощью массивов Apache Arrow, а pandas (в данное время) представляет данные в памяти с помощью массивов NumPy. Apache Arrow - это новый стандарт столбчатой аналитики в памяти, который может ускорить загрузку данных, сократить использование памяти и ускорить вычисления.
Polars может конвертировать данные в формат NumPy с помощью метода df.to_numpy()
.
Polars использует сильную поддержку параллелизма в Rust для параллельного выполнения множества операций. Хотя некоторые операции в pandas являются многопоточными, ядро библиотеки является однопоточным, и для распараллеливания операций необходимо использовать дополнительную библиотеку, такую как Dask.
Оперативная оценка - это когда код оценивается сразу после запуска. Отложенная оценка - это когда выполнение строки кода означает, что базовая логика добавляется в план запроса, а не оценивается.
Polars поддерживает оперативную оценку и отложенную оценку, в то время как pandas поддерживает только оперативную оценку. Режим отложенной оценки эффективен, поскольку Polars выполняет автоматическую оптимизацию запроса, когда проверяет план запроса и ищет способы ускорить выполнение запроса или уменьшить использование памяти.
Dask также поддерживает отложенную оценку при создании плана запроса. Однако Dask не выполняет оптимизацию запроса в плане запроса.
Внимание! Во всех примерах демонстрируется абстрактный код.
Пользователям, пришедшим из pandas, как правило, нужно знать одну вещь... Что polars != pandas
Если код Polars выглядит так, как будто это может быть код pandas, он может работать, но, скорее всего, работает медленнее, чем следовало бы.
Далее пройдемся по некоторому типичному коду pandas и посмотрим, как мы могли бы переписать его в Polars.
Так как в Polars нет индекса, в Polars нет метода .loc
или iloc
- и в Polars также нет SettingWithCopyWarning
.
Лучший способ выбрать данные в Polars - это использовать expression API. Например, если необходимо выбрать столбец в pandas, то можно выполнить одно из следующих действий:
df['a'] df.loc[:,'a']
но в Polars необходимо использовали метод df.select()
:
df.select('a')
Если нужно выбрать строки на основе значений, то в Polars использует метод фильтрации df.filter()
:
df.filter(pl.col('a') < 10)
Как отмечено в разделе о выражениях ниже, Polars могут выполнять операции в df.select()
и df.filter()
параллельно, и могут выполнять оптимизацию запросов по полному набору критериев выбора данных.
Работа в режиме отложенной оценки проста и должна использоваться по умолчанию в Polars, т.к. отложенный режим позволяет Polars выполнять оптимизацию запросов.
Можно работать в отложенном режиме, либо используя неявно отложенную функцию (такую как pl.scan_csv()
), либо явно используя отложенный метод.
Возьмем следующий простой пример, в котором считывает CSV-файл с диска и выполняет операцию df.group_by()
. Просто сгруппируем по одному из столбцов идентификатора (id1
), а затем суммировать по столбцу значения (v1
). В pandas это было бы:
df = pd.read_csv(csv_file, usecols=['id1','v1']) grouped_df = df.loc[:,['id1','v1']].groupby('id1').sum('v1')
В Polars можно создать этот запрос в отложенном режиме с оптимизацией запроса и оценить его, заменив функцию pandas pd.read_csv()
на неявно отложенную функцию Polars pl.scan_csv()
:
df = pl.scan_csv(csv_file) grouped_df = df.group_by('id1').agg(pl.col('v1').sum()).collect()
Polars оптимизирует этот запрос, определяя, что только столбцы id1
и v1
являются релевантными, и поэтому будет считывать только эти столбцы из CSV. Вызывая метод .collect
в конце второй строки, даем указание Polars внимательно оценить запрос.
Если нужно запустить этот запрос в режиме ожидания, то можно просто заменить pl.scan_csv
на pl.read_csv
в коде Polars.
Альтернативный способ получить доступ к lazy API - это вызвать метод .lazy
для фрейма данных, который уже создан в памяти.
q3 = pl.DataFrame({"foo": ["a", "b", "c"], "bar": [0, 1, 2]}).lazy()
Типичный скрипт pandas состоит из нескольких преобразований данных, которые выполняются последовательно. В Polars эти преобразования могут выполняться параллельно с использованием выражений.
Допустим есть фрейм данных df
со столбцом под названием value
. Необходимо добавить два новых столбца, столбец с именем tenXValue
, где столбец значений умножается на 10, и столбец с именем hunderdXValue
, где столбец значений умножается на 100.
В pandas это будет:
df.assign( tenXValue=lambda df_: df_.value * 10, hundredXValue=lambda df_: df_.value * 100 )
Эти назначения столбцов выполняются последовательно.
В Polars добавляются столбцы с помощью метода df.with_columns()
:
df.with_columns( tenXValue=pl.col("value") * 10, hundredXValue=pl.col("value") * 100, )
Эти назначения столбцов выполняются параллельно.
В данном случае используем фрейм данных df
со столбцами a
, b
и c
. Необходимо повторно назначить значения в столбце a
на основе условия. Когда значение в столбце c
равно 2, заменить значение в a
значением из b
.
В pandas это будет:
df.assign(a=lambda df_: df_.a.where(df_.c != 2, df_.b))
в то время как в Polars это будет:
df.with_columns( pl.when(pl.col("c") == 2) .then(pl.col("b")) .otherwise(pl.col("a")).alias("a") )
Polars может вычислять каждую ветвь if-then-иначе
параллельно. Это ценно, когда вычисление ветвей становится более затратным.
Нужно отфильтровать фрейм данных df
с данными о жилье на основе некоторых критериев.
В pandas фильтруется фрейм данных, передавая логические выражения в метод запроса df.query()
:
df.query("m2_living > 2500 and price < 300000")
или путем прямой логической индексации:
df[(df["m2_living"] > 2500) & (df["price"] < 300000)]
В Polars вызывается метод фильтра df.filter()
:
df.filter( (pl.col("m2_living") > 2500) & (pl.col("price") < 300000) )
Оптимизатор запросов в Polars также может определить, что пишется несколько фильтров отдельно и объединяет их в один фильтр в оптимизированном плане.
Документация pandas демонстрирует операцию над группой с помощью преобразования. Например, есть dataframe
, и необходимо добавить новый столбец, показывающий количество строк в каждой группе.
В pandas:
df = pd.DataFrame({ "c": [1, 1, 1, 2, 2, 2, 2], "type": ["m", "n", "o", "m", "m", "n", "n"], }) df["size"] = df.groupby("c")["type"].transform(len) # ВЫВОД # c type size # 0 1 m 3 # 1 1 n 3 # 2 1 o 3 # 3 2 m 4 # 4 2 m 4 # 5 2 n 4 # 6 2 n 4
Здесь pandas выполняет группировку по 'c'
, берет столбец 'type'
, вычисляет длину группы, а затем присоединяет результат обратно к исходному фрейму данных.
В Polars то же самое может быть достигнуто с помощью оконных функций:
df.with_columns( pl.col("type").count().over("c").alias("size") ) # ВЫВОД # shape: (7, 3) # ┌─────┬──────┬──────┐ # │ c ┆ type ┆ size │ # │ --- ┆ --- ┆ --- │ # │ i64 ┆ str ┆ u32 │ # ╞═════╪══════╪══════╡ # │ 1 ┆ m ┆ 3 │ # │ 1 ┆ n ┆ 3 │ # │ 1 ┆ o ┆ 3 │ # │ 2 ┆ m ┆ 4 │ # │ 2 ┆ m ┆ 4 │ # │ 2 ┆ n ┆ 4 │ # │ 2 ┆ n ┆ 4 │ # └─────┴──────┴──────┘
Так как можно сохранить всю операцию в одном выражении, то можно комбинировать несколько оконных функций и даже объединять разные группы!
Polars будут кэшировать оконные выражения, которые применяются к одной и той же группе, поэтому хранить их в одном with_columns
удобно и оптимально. В следующем примере рассмотрим случай, когда дважды вычисляем групповую статистику по 'c'
:
df.with_columns( pl.col("c").count().over("c").alias("size"), pl.col("c").sum().over("type").alias("sum"), pl.col("type").reverse().over("c").alias("reverse_type") ) # ВЫВОД # shape: (7, 5) # ┌─────┬──────┬──────┬─────┬──────────────┐ # │ c ┆ type ┆ size ┆ sum ┆ reverse_type │ # │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ # │ i64 ┆ str ┆ u32 ┆ i64 ┆ str │ # ╞═════╪══════╪══════╪═════╪══════════════╡ # │ 1 ┆ m ┆ 3 ┆ 5 ┆ o │ # │ 1 ┆ n ┆ 3 ┆ 5 ┆ n │ # │ 1 ┆ o ┆ 3 ┆ 1 ┆ m │ # │ 2 ┆ m ┆ 4 ┆ 5 ┆ n │ # │ 2 ┆ m ┆ 4 ┆ 5 ┆ n │ # │ 2 ┆ n ┆ 4 ┆ 5 ┆ m │ # │ 2 ┆ n ┆ 4 ┆ 5 ┆ m │ # └─────┴──────┴──────┴─────┴──────────────┘
Pandas использует значения NaN
и/или None
для указания пропущенных значений в зависимости от dtype
столбца. Кроме того, поведение в pandas варьируется в зависимости от того, используются dtypes
по умолчанию или необязательные массивы с нулевым значением. В Polars отсутствующим данным соответствует нулевое значение для всех типов данных.
Для столбцов с плавающей запятой Polars разрешает использовать значения NaN
. Эти значения NaN
считаются не отсутствующими данными, а специальным значением с плавающей запятой.
В pandas целочисленный столбец с пропущенными значениями преобразуется в столбец с плавающей точкой со значениями NaN
для пропущенных значений (если не используются необязательные целочисленные dtypes
с нулевым значением). В Polars любые пропущенные значения в целочисленном столбце являются просто нулевыми значениями, и столбец остается целочисленным столбцом.
Обычным использованием в pandas является использование канала для применения некоторой функции к фрейму данных. Копирование этого стиля кодирования в Polars является однотипным и приводит к неоптимальным планам запросов.
Приведенный ниже фрагмент показывает общую закономерность у pandas.
def add_foo(df: pd.DataFrame) -> pd.DataFrame: df["foo"] = ... return df def add_bar(df: pd.DataFrame) -> pd.DataFrame: df["bar"] = ... return df def add_ham(df: pd.DataFrame) -> pd.DataFrame: df["ham"] = ... return df (df .pipe(add_foo) .pipe(add_bar) .pipe(add_ham) )
Если сделать это в polars, то создается 3 контекста with_columns
, которые заставят Polars запускать 3 канала последовательно, используя нулевой параллелизм.
Способ получить аналогичные абстракции в polars - это создать функции, создающие выражения. Приведенный ниже фрагмент создает 3 выражения, которые выполняются в одном контексте и, следовательно, могут выполняться параллельно.
def get_foo(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("foo") def get_bar(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("bar") def get_ham(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("ham") # This single context will run all 3 expressions in parallel df.with_columns( get_ham("col_a"), get_bar("col_b"), get_foo("col_c"), )
Если в функциях необходима схема, генерирующая выражения, то можно использовать один канал:
from collections import OrderedDict def get_foo(input_column: str, schema: OrderedDict) -> pl.Expr: if "some_col" in schema: # branch_a ... else: # branch b ... def get_bar(input_column: str, schema: OrderedDict) -> pl.Expr: if "some_col" in schema: # branch_a ... else: # branch b ... def get_ham(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("ham") # Use pipe (just once) to get hold of the schema of the LazyFrame. lf.pipe(lambda lf: lf.with_columns( get_ham("col_a"), get_bar("col_b", lf.schema), get_foo("col_c", lf.schema), )
Еще одним преимуществом написания функций, возвращающих выражения, является то, что эти функции являются составными, т.к. выражения могут быть объединены в цепочки и частично применены, что приводит к гораздо большей гибкости в дизайне.