Polars появился как ответ на ограничения pandas при работе с большими таблицами. Это библиотека для Python и Rust с колоночным хранением в Arrow, многопоточными вычислениями и ленивыми запросами. Для аналитика она выглядит знакомо: DataFrame, колонки, фильтры, группировки. Но внутри работает другая модель, и от неё зависит, получите вы ускорение в разы или те же секунды, что и раньше.
Чем Polars отличается от pandas
Polars не хранит данные построчно и не вызывает Python на каждую операцию. Выражения компилируются в план, который выполняется на нескольких ядрах. Ленивый режим позволяет оптимизатору переставить фильтры, убрать лишние колонки и прочитать только нужные части Parquet. В pandas аналогичный результат обычно требует ручной аккуратности и библиотек вроде pyarrow.
Вторая разница — строгие типы. Polars не любит смешивать строки и числа и явно сообщает о проблеме. Это неудобно при грязных данных, но полезно в продакшене: ошибка всплывает на входе. Третья разница — API выражений через pl.col вместо цепочек методов.
Установка и проверка окружения
Для начала достаточно самого пакета. pyarrow нужен для обмена с pandas и части форматов, поэтому его стоит поставить сразу. В проекте с uv или poetry добавьте зависимость в файл, а не в глобальное окружение: версия Polars меняется достаточно быстро.
pip install polars pyarrow
# или
uv add polars pyarrow
python -c "import polars as pl; print(pl.__version__)"
python -c "import polars as pl; print(pl.__version__, pl.build_info())"
Проверьте, что интерпретатор и пакет собраны под одну платформу. Колесо для Windows, macOS и Linux содержит нативный код; при установке из исходников понадобится Rust. Для типового анализа хватает готового wheel.
Чтение данных и ленивый запрос
Главное практическое правило: если данных много, начинайте не с read_csv, а с scan_csv или scan_parquet. Эти функции не читают файл целиком, а создают план. Фильтры и выбор колонок применяются на этапе выполнения, и Polars может прочитать меньше данных с диска.
import polars as pl
lf = (
pl.scan_parquet("data/events/*.parquet")
.filter(pl.col("event_date") >= pl.date(2026, 1, 1))
.select(["user_id", "event_date", "amount", "channel"])
)
result = lf.group_by("channel").agg(
pl.len().alias("events"),
pl.col("amount").sum().alias("revenue"),
pl.col("user_id").n_unique().alias("users"),
).sort("revenue", descending=True)
print(result.collect())
В ленивом режиме ничего не выполняется до collect. Это позволяет собрать конвейер и дать оптимизатору шанс переставить операции. Collect после каждого шага лишает преимущества.
Выражения вместо циклов
Выражения — основа Polars. Внутри select, with_columns и agg вы описываете, что сделать с колонкой, а не как перебирать строки. Условия задаются через pl.when, строковые операции — через .str, даты — через .dt. Такой код читается как декларация и выполняется нативно.
enriched = (
lf.with_columns(
pl.when(pl.col("amount") > 1000)
.then(pl.lit("large"))
.otherwise(pl.lit("regular"))
.alias("order_class"),
pl.col("event_date").dt.month().alias("month"),
pl.col("channel").str.to_lowercase().alias("channel_norm"),
)
.filter(pl.col("channel_norm").is_in(["web", "mobile"]))
)
Не тащите в Polars привычку писать apply с Python-функцией на каждую строку. map_elements вызывает Python и убивает производительность. Сначала поищите готовое выражение: очистка строк, разбор дат и условная логика уже покрыты.
Соединения и агрегации
Join в Polars явно указывает, какие колонки соединяются, и не любит неоднозначные имена. Это заставляет назвать ключи заранее и избежать сюрпризов. Для больших таблиц полезно сначала уменьшить обе стороны: отфильтровать по дате и выбрать только нужные колонки, а уже потом соединять.
users = pl.scan_parquet("data/users.parquet").select(
["user_id", "segment", "region"]
)
joined = (
lf.join(users, on="user_id", how="left")
.group_by(["segment", "region"])
.agg(
pl.col("amount").mean().alias("avg_amount"),
pl.len().alias("orders"),
)
)
Для проверки качества соединения сравните число строк до и после. Если оно выросло, вероятно, в правой таблице есть дубликаты ключа. Join many-to-many может размножить данные, поэтому проверяйте уникальность заранее.
Потоковая обработка больших файлов
Ленивый план ещё не значит, что данные поместятся в память. Для файлов, которые больше RAM, используйте потоковый движок: он обрабатывает данные порциями. В Polars 1.x это включается параметром engine=»streaming» при collect. Результат обычно меньше исходных данных, но промежуточные агрегации всё равно требуют памяти.
summary = (
pl.scan_parquet("data/events/*.parquet")
.group_by("channel")
.agg(pl.col("amount").sum().alias("revenue"))
.collect(engine="streaming")
)
print(summary)
Следите за размером групп: если уникальных ключей миллионы, агрегация построит большую таблицу. Помогает фильтрация, разбиение по датам или запись частями.
Обмен с pandas и другими инструментами
Переход не обязан быть одномоментным. Polars умеет отдавать и принимать pandas DataFrame, но конвертация копирует данные и стоит времени. Держите границу обмена как можно уже: например, читайте и чистите данные в Polars, а на pandas оставьте только тот участок, где нужна специфическая библиотека.
import pandas as pd
pdf = result.collect().to_pandas()
back = pl.from_pandas(pdf)
result.collect().write_parquet("out/summary.parquet", compression="zstd")
Для Parquet используйте сжатие zstd: оно обычно даёт хороший баланс между размером и скоростью чтения. CSV оставляйте для обмена с внешними системами, а внутри конвейера храните колонки в Parquet. Это уменьшает объём и сохраняет типы.
Диагностика и типичные ошибки
Если запрос работает медленно, посмотрите план через explain. Он показывает, какие операции будут выполнены и где оптимизатор не смог помочь. Частая причина — чтение всех колонок или фильтр после агрегации. Другая причина — Python-функция в map_elements. Третья — слишком мелкие файлы: тысячи маленьких Parquet замедляют планирование и чтение.
query = lf.group_by("channel").agg(pl.len())
print(query.explain())
# Проверка схемы и типов до запуска
print(lf.collect_schema())
print(lf.head(5).collect())
Не сравнивайте скорость на одном запуске: первый вызов включает прогрев и чтение метаданных. Делайте несколько итераций и смотрите на медиану. Для честного теста используйте одинаковые данные, одинаковые типы и одинаковый набор колонок.
Чек-лист миграции
- Данные хранятся в Parquet, а не в CSV на каждом шаге.
- Конвейер начинается с scan_*, а не с read_*.
- Фильтры и выбор колонок стоят как можно раньше.
- Вместо apply используются нативные выражения.
- Ключи join проверены на уникальность.
- Для больших объёмов включён потоковый движок.
- Обмен с pandas ограничен одним узким местом.
- План проверен через explain, а скорость — на нескольких прогонах.
Итог
Polars стоит пробовать там, где pandas упирается в память или время. Начните с одного отчёта: переведите чтение на scan_parquet, перепишите преобразования на выражения и сравните результат. Если выигрыш есть, двигайтесь дальше по конвейеру.