Обработка данных в Python без Pandas: Polars, DuckDB и chDB

Обработка данных в Python без Pandas: Polars, DuckDB и chDB

Когда я впервые столкнулся с датасетом на 10 миллионов строк, pandas задумался на 37 секунд - просто чтобы отсортировать табличку. А потом сожрал полтора гигабайта оперативки. Я был уверен, что проблема в моём коде, но оказалось - проблема в самом pandas.

Он грузит все данные в память, обрабатывает в один поток и на каждой операции создаёт копию DataFrame. На маленьких данных это незаметно, но когда строк миллионы - всё разваливается.

Я протестировал три альтернативы - Polars, DuckDB и chDB - на реальных задачах и получил ускорение от 5x до 77x. Без переписывания всего проекта. В статье - конкретные цифры бенчмарка, примеры кода и рекомендации, когда что использовать.

Почему pandas не справляется с большими данными

Один поток и eager execution

Pandas работает в один поток. Неважно, сколько ядер в процессоре - 4, 8 или 16 - pandas использует одно. Это архитектурное ограничение, а не баг.

Но есть кое-что похуже: каждая операция выполняется немедленно - это называется eager execution. Написали df.sort_values() - pandas тут же сортирует все 10 миллионов строк, даже если вам нужен только топ-10. Написали цепочку df.filter().groupby().sort() - каждый шаг создаёт промежуточный DataFrame в памяти.

Polars и DuckDB работают иначе: они подобно Apache Spark строят план запроса, оптимизируют и только потом выполняют. Это lazy execution - поэтому они быстрее в десятки раз.

Out of Memory - знакомо?

Типичная ситуация: CSV-файлик на 5 ГБ. Pandas грузит его в память, и DataFrame раздувается до 12-15 ГБ - потому что внутренний формат удобен для операций, но по памяти крайне расточителен. Если у вас 16 ГБ оперативки - всё, OOM и перезагрузка.

Анонсы всех видео, статей и полезностей - в нашем Telegram-канале🔥
Присоединяйтесь, обсуждайте и автоматизируйте!

Даже если памяти хватает, pandas создаёт копии при каждой трансформации. Сортировка 10 миллионов строк? Ещё 350 МБ сверху. Оконная функция? Ещё столько же. Потребление памяти растёт лавинообразно.

Представьте: ваш скрипт крутится 37 секунд вместо 3. Запускаете его 50 раз в день - теряете полчаса на ожидание. За месяц это 10+ часов. Умножьте на стоимость часа аналитика - получится приличная сумма.

Что же с этим делать?

Есть три подхода: 

  1. Многопоточный DataFrame - Polars. Написан на Rust, параллелит операции на все ядра 
  2. SQL-движок в процессе - DuckDB. Пишете SQL, а он оптимизирует и выполняет 
  3. Embedded OLAP - chDB. Движок ClickHouse прямо в вашем Python-процессе

Давайте разберём каждый и посмотрим на реальные цифры.

Альтернативы pandas для обработки больших данных в Python

Polars - быстрый DataFrame на Rust

Polars - библиотека для работы с данными на Rust. По синтаксису похожа на pandas, но работает иначе: параллелит операции на все ядра и поддерживает lazy execution.

Что тут важно знать - Polars не обёртка над pandas. Это новый движок с нуля на Apache Arrow - нулевое копирование при передаче данных между библиотеками.

import polars as pl

# Загрузка - 4x быстрее pandas
df = pl.read_parquet("taxi_10m.parquet")

# Фильтрация - 5.5x быстрее
result = df.filter(
    (pl.col("trip_distance") > 2) & (pl.col("tip_amount") > 5)
)

# Lazy pipeline - 19x быстрее
result = (
    df.lazy()
    .filter(pl.col("trip_distance") > 1)
    .group_by("PULocationID")
    .agg(pl.col("fare_amount").mean().alias("avg_fare"))
    .sort("avg_fare", descending=True)
    .head(10)
    .collect()
)

Из плюсов - похож на pandas, крайне быстрый, отличная документация. Из минусов - API всё-таки отличается, так что код придётся править.

DuckDB - SQL-аналитика без сервера

Если вы знаете SQL - даже учить ничего не надо. DuckDB - это встроенная аналитическая СУБД, которая читает Parquet, CSV и JSON прямо из файлов, без загрузки в память.

Фишка DuckDB - он оптимизирует SQL-запросы как полноценная база: pushdown предикатов, параллельное выполнение, колоночное хранение. Ставится через pip install duckdb и не требует сервера.

import duckdb

# Работает прямо с файлом - не грузит всё в память
result = duckdb.sql("""
    SELECT PULocationID,
           AVG(fare_amount) as avg_fare,
           COUNT(*) as trips
    FROM read_parquet('taxi_10m.parquet')
    WHERE trip_distance > 1 AND fare_amount > 0
    GROUP BY PULocationID
    HAVING COUNT(*) > 100
    ORDER BY avg_fare DESC
    LIMIT 10
""").fetchdf()

На 10 миллионах строк - 0.08 секунды. Pandas на той же задаче - 6 секунд. В 77 раз дольше 🙀

SQL - знакомый язык, агрегации летают, памяти ест минимум. Единственный нюанс - для пошаговых трансформаций DataFrame он не так удобен, как Polars.

chDB - ClickHouse у вас в Python

А вот это интересно: chDB - движок ClickHouse, скомпилированный как Python-библиотека. ClickHouse - одна из самых быстрых аналитических СУБД в мире. chDB даёт доступ к нему через pip install chdb. Без серверов, без Docker, без настройки.

В чём отличие от DuckDB? ClickHouse проектировался для OLAP - аналитических запросов на миллиардах строк. Колоночное хранение, векторизованное выполнение, сжатие - всё в chDB из коробки.

import chdb

# Запрос прямо из Parquet - как DuckDB, но на движке ClickHouse
result = chdb.query("""
    SELECT PULocationID,
           avg(fare_amount) as avg_fare,
           count() as trips
    FROM file('taxi_10m.parquet', Parquet)
    WHERE trip_distance > 1 AND fare_amount > 0
    GROUP BY PULocationID
    HAVING count() > 100
    ORDER BY avg_fare DESC
    LIMIT 10
""", "DataFrame")

Но самая крутая фишка - персистентное хранилище. Создаёте локальную базу с таблицами MergeTree, загружаете данные один раз - и дальше запросы летают:

from chdb import dbapi

conn = dbapi.connect(path="./my_analytics_db")
cur = conn.cursor()

# Создаём таблицу - данные сжимаются и индексируются
cur.execute("""
    CREATE TABLE IF NOT EXISTS taxi
    ENGINE = MergeTree() ORDER BY PULocationID AS
    SELECT * FROM file('taxi_10m.parquet', Parquet)
""")

# Повторные запросы - мгновенные
cur.execute("SELECT count() FROM taxi")
print(cur.fetchone())

Один раз закинули данные - и работаете как с полноценной базой. Плюс S3, PostgreSQL, MySQL из коробки.

Из минусов - chDB не работает на Windows (только Linux и macOS) и весит около 160 МБ - больше, чем DuckDB.

Кстати, у chDB есть экспериментальный DataStore API (chdb-ds) - pandas-совместимый интерфейс с lazy execution. Пишете как в pandas, а под капотом генерируется ClickHouse SQL. Пока в v0.1.0 и на сложных цепочках бывают баги, но направление перспективное. Следить можно на GitHub.

Бенчмарк: pandas vs Polars vs DuckDB vs chDB

Хватит теории - давайте к цифрам. Датасет со структурой NYC Yellow Taxi: 10 миллионов строк, 19 столбцов, 255 МБ в Parquet. Машина - 4 ядра / 8 потоков, 16 ГБ RAM, Python 3.11.

ОперацияpandasPolarsDuckDBchDB*
Загрузка0.700.160.635.16
Фильтрация0.230.040.330.18
GROUP BY + 4 агрегации0.410.430.040.08
Сортировка (3 столбца)36.923.054.101.09
Оконная функция (RANK)11.291.014.381.10
Pipeline (filter→group→sort→top10)6.000.320.080.12

Секунды, меньше - лучше. Polars и DuckDB работают с Parquet напрямую. chDB - через MergeTree: данные загружаются один раз (5.16с), дальше повторные запросы летают без перечитывания файла.

Сортировка - самый показательный пример: pandas потратил 37 секунд, Polars справился за 3, а chDB MergeTree - за 1.09. Индексированные данные решают.

Pipeline: pandas 6 секунд, DuckDB 0.08, chDB 0.12. DuckDB чуть быстрее, но chDB хранит данные на диске - перезапустили скрипт, и запросы сразу работают без повторной загрузки.

Выводы

Pandas никуда не денется - для данных до миллиона строк он по-прежнему удобен. Но дальше - пора выбирать:

  • Polars - если привыкли к DataFrame и хотите «тот же pandas, только быстрее». Ускорение 5-12x, похожий API, отличная документация.
  • DuckDB - если знаете SQL. Агрегации в 10-77x быстрее pandas, работает прямо с файлами, не грузит всё в память.
  • chDB - если нужно хранилище. Загружаете данные в MergeTree один раз - повторные запросы за 0.12с вместо 6с. Плюс S3, PostgreSQL, MySQL из коробки.

Все три ставятся через pip install, не требуют сервера и не исключают друг друга - DuckDB читает Polars DataFrame, chDB экспортирует в Arrow. Переход не требует переписывания проекта - достаточно заменить самый медленный участок.