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, перепишите преобразования на выражения и сравните результат. Если выигрыш есть, двигайтесь дальше по конвейеру.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте