Когда я впервые столкнулся с датасетом на 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+ часов. Умножьте на стоимость часа аналитика - получится приличная сумма.
Что же с этим делать?
Есть три подхода:
- Многопоточный DataFrame - Polars. Написан на Rust, параллелит операции на все ядра
- SQL-движок в процессе - DuckDB. Пишете SQL, а он оптимизирует и выполняет
- 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.
| Операция | pandas | Polars | DuckDB | chDB* |
|---|---|---|---|---|
| Загрузка | 0.70 | 0.16 | 0.63 | 5.16 |
| Фильтрация | 0.23 | 0.04 | 0.33 | 0.18 |
| GROUP BY + 4 агрегации | 0.41 | 0.43 | 0.04 | 0.08 |
| Сортировка (3 столбца) | 36.92 | 3.05 | 4.10 | 1.09 |
| Оконная функция (RANK) | 11.29 | 1.01 | 4.38 | 1.10 |
| Pipeline (filter→group→sort→top10) | 6.00 | 0.32 | 0.08 | 0.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. Переход не требует переписывания проекта - достаточно заменить самый медленный участок.