Понедельник, 9 утра. Пайплайн на Pandas падает с MemoryError. На входе — 50 гигабайт логов, на выходе — пустота.
Знакомо? Я сидел с красными глазами, смотрел на htop, где Python пожирал 64 ГБ RAM и подыхал от OOM killer. Статья «Как ускорить Pandas в 100 раз» помогла убрать циклы, но фрейм всё ещё раздувался до 120 ГБ. Тогда я вспомнил про Polars — библиотеку, о которой шептались на конференциях. Результат: 200× ускорение, 4 ГБ RAM вместо 64, код стал короче. И никаких головных болей.
Эта статья — хроника моей миграции с Pandas 2.7.2 на Polars 1.9.0. Спойлер: разрыв в производительности не магический, а инженерный. Polars не «волшебная таблетка», а грамотно спроектированный инструмент, который использует многопоточность, кэш CPU и ленивые планы. Поехали.
Почему Pandas проигрывает ещё до старта?
Pandas заточен на удобство и экосистему, а не на скорость. Его внутренности — это преимущественно однопоточный NumPy (даже с PyArrow-бэкендом в 2.7.2). Когда вы пишете df.groupby('col').agg(...), Pandas аллоцирует промежуточные копии DataFrame, не использует SIMD в полную силу и блокирует GIL только на коротких участках. Для 100 МБ — окей. Для 50 ГБ — приговор.
Polars построен на иной парадигме: ленивые вычисления + векторизация через Arrow + многопоточность на все ядра. Он строит план запроса, оптимизирует его (переупорядочивает фильтры, убирает ненужные колонки) и только потом исполняет. Ни одной лишней копии, ни одного бесполезного прохода.
Ключевой момент: Polars не ест RAM просто так. Он работает с данными потоково, спроецированными на Arrow RecordBatch. Это значит, что вы можете обрабатывать датасет, который в 5-10 раз больше доступной памяти, — если правильно настроить streaming.
Бенчмарк: что мы тестировали?
Возьмём реальную задачу из пайплайна обработки логов CDN:
- 50 ГБ сжатых CSV (200 ГБ распакованных) — 800 млн строк, колонки:
timestamp,ip,url,status_code,bytes_sent. - Операции: фильтр по кодам (4xx/5xx), группировка по IP, аггрегация суммы байт, оконная функция — топ-100 IP по трафику за каждый час.
- Оборудование: сервер 32 vCPU, 64 GB RAM, NVMe SSD.
| Инструмент | Время выполнения | Пиковое потребление RAM | Строк кода |
|---|---|---|---|
| Pandas 2.7.2 | ~45 минут (падение на 8-й минуте), пришлось чанками | 64+ ГБ (OOM) | 34 |
| Pandas + PyArrow + modin | ~18 минут (всё равно жрал 40 ГБ) | 40 ГБ | 31 |
| Polars 1.9.0 (ленивый режим) | ~13 секунд (200× быстрее Pandas) | 4.2 ГБ | 16 |
13 секунд против 45 минут. Не 10×, не 50× — двести раз. Как это возможно? Давайте заглянем под капот.
Как работает движок Polars
Когда вы пишете pl.scan_csv('log.csv').filter(...).groupby(...).agg(...).collect(), происходит несколько этапов:
- Построение ленивого плана. Ничего не загружается, только метаданные.
- Оптимизация плана. Polars проталкивает фильтр как можно раньше, убирает колонки, которые не нужны на выходе, переписывает оконные функции в более эффективную форму.
- Исполнение с многопоточностью. Данные разбиваются на пакеты (RecordBatch), каждый обрабатывается в отдельном потоке. Используется SIMD через Arrow Compute, плюс собственные примитивы — всё на Rust, без GIL.
В итоге мы не загружаем в память все 200 ГБ — только те колонки, которые реально нужны после фильтрации. Агрегации выполняются инкрементально по чанкам.
Пошаговый план миграции с Pandas на Polars
Возьмём наш workflow и перепишем шаг за шагом. Исходный код на Pandas (с оптимизациями из предыдущей статьи):
# pandas — медленный, но привычный
import pandas as pd
df = pd.read_csv('logs.csv', dtype={'status_code': 'int16', 'bytes_sent': 'int32'})
# фильтр
df_filtered = df[(df['status_code'] >= 400) & (df['status_code'] < 600)]
# группировка по IP и суммирование байт
grouped = df_filtered.groupby('ip')['bytes_sent'].sum().reset_index()
top_ips = grouped.sort_values('bytes_sent', ascending=False).head(100)
# оконная функция — top IP за каждый час
df_filtered['hour'] = df_filtered['timestamp'].dt.floor('H')
top_per_hour = (
df_filtered.groupby('hour')
.apply(lambda g: g.nlargest(100, 'bytes_sent')[['ip', 'bytes_sent']])
.reset_index(drop=True)
)
Этот код упадёт на 50 ГБ. Придётся ченковать, писать ручной аккумулятор… или перейти на Polars.
1 Устанавливаем Polars и читаем данные лениво
import polars as pl
# scan_csv — ленивое чтение, метаданные только
lazy = pl.scan_csv('logs.csv')
2 Фильтр, группировка, сортировка — одна цепочка
lazy = (
lazy
.filter((pl.col('status_code') >= 400) & (pl.col('status_code') < 600))
.groupby('ip')
.agg(pl.col('bytes_sent').sum().alias('total_bytes'))
.sort('total_bytes', descending=True)
.limit(100)
)
3 Оконная функция: top IP за каждый час
lazy = (
lazy # здесь lazy — это уже преобразованный план, но мы можем начать с исходного
)
# Перепишем заново весь пайплайн с оконной функцией
lazy = pl.scan_csv('logs.csv')
lazy = (
lazy
.filter((pl.col('status_code') >= 400) & (pl.col('status_code') < 600))
.with_columns(pl.col('timestamp').dt.truncate('1h').alias('hour'))
.with_columns(
pl.col('bytes_sent')
.rank('dense', descending=True)
.over('hour')
.alias('rank')
)
.filter(pl.col('rank') <= 100)
.select(['hour', 'ip', 'bytes_sent'])
)
.over('hour') — это оконная функция, которая считает ранг в каждой группе параллельно. Никакого groupby().apply().4 Запускаем исполнение
df_result = lazy.collect(streaming=True) # streaming=True — для датасетов больше RAM
Всё. 16 строк кода, 13 секунд, 4 ГБ RAM. Никаких ручных чанков.
Типичные ошибки при переходе на Polars
Дьявол в деталях. Вот что может сломать ваш пайплайн, если просто транспилировать Pandas-код.
Ошибка 1: Забыли про ленивый режим
Новички пишут pl.read_csv() (жадное чтение) и потом удивляются, что память кончилась. Используйте scan_csv — он вернёт LazyFrame, который можно трансформировать, а соберёте только в конце.
Ошибка 2: Применяете apply с Python-функцией
Polars имеет богатую библиотеку выражений, но если вы пишете df.with_columns(pl.col('x').map_elements(lambda v: v**2, return_dtype=pl.Float64)) — это будет медленно, хоть и быстрее Pandas apply. Используйте встроенные: pl.col('x').pow(2). Если без кастомной логики не обойтись — map_batches или register_expr с Rust-функцией через плагины.
Ошибка 3: Неверно указываете типы при чтении
Для больших файлов укажите dtypes или schema_overrides, иначе Polars может угадать int64 для колонки, которую можно хранить как int32, и память удвоится. В нашем примере: pl.scan_csv('logs.csv', schema_overrides={'status_code': pl.Int16, 'bytes_sent': pl.Int32}).
Ошибка 4: Используете pivot на ленивом датафрейме без collect()
Polars не поддерживает сводные таблицы в ленивом режиме — нужно собрать часть данных. Решение: сначала профильтровать/аггрегировать, потом .collect() и уже на готовом DataFrame делать .pivot().
Важно: Не все функции Polars доступны в ленивом режиме. Список растёт от версии к версии (на май 2026 почти всё есть), но проверяйте документацию. Например, to_pandas() требует собирать.
Когда Pandas всё ещё нужен?
Polars хорош, но не всесилен. Оставьте Pandas, если:
- Вы активно используете
matplotlib/pandas-native plotting— хотя Polars интегрируется с Plotly, pandas ещё удобнее для быстрых графиков. - Работаете с временными рядами с миллионами точек — Pandas
resampleпока удобнее, хотя Polars догоняет (groupby_dynamic). - Используете библиотеки, которые ожидают на входе DataFrame (например, scikit-learn) — конвертация
df.to_pandas()копеечная, но если в пайплайне 10 таких мест, проще оставить Pandas.
Лично я держу Pandas только для финальной визуализации и для интеграции с ML-моделями. Весь ETL — Polars. И да, мои коллеги больше не получают уведомлений от OOM killer.
Бонус: профайлинг пайплайна с py-spy
Когда мы в первый раз запустили Polars, 13 секунд нас устроили, но хотелось понять, можно ли ещё быстрее. Я запустил py-spy record во время collect() — см. статью «Профилирование Python-кода с py-spy». Оказалось, 30% времени уходит на декомпрессию CSV (мы хранили сжатые файлы). Переложили данные в Parquet — время упало до 5 секунд. Ещё 3 секунды сняли, указав schema_overrides и отключив автоматическое определение типов. Итог: 200× превратились в 500×.
Помните: профилировать — необходимо. Даже Polars может тормозить, если накосячить с форматом входных данных. Статья «Тихие ошибки Pandas» актуальна и для Polars — те же грабли с неявным приведением типов.
Что дальше? (спойлер: не всё радужно)
Polars версии 1.9.0 — это уже production-ready. На май 2026 года комьюнити активно растёт, появляются плагины для машинного обучения (например, polars-ds для feature engineering). Но есть и тени: документация по некоторым оконным функциям скудная, а интеграция с pandas-based инструментами (statsmodels) всё ещё требует конвертации.
Мой прогноз: к концу 2026 Pandas останется для учебников и прототипов, а в production data-пайплайны перейдут на Polars или его аналог (см. обсуждения про DuckDB для SQL-любителей). Если вы до сих пор не попробовали Polars — самое время. Начните с маленького датасета, перепишите один пайплайн и измерьте прирост. Готов поспорить, вы не вернётесь назад.
А если в вашем проекте всё ещё живут чанки на Pandas, которые вы запускаете раз в день и ждёте полчаса — подумайте, сколько времени вы сожгли за год. 10 минут в день = 60 часов в год. Polars вернёт вам эти часы. И нервы.