pandas на 1M строк — комфортно. На 10M — тяжеловато. На 50M+ — постоянно OutOfMemory. Аналитики Wildberries, Yandex, Avito переходят на polars или DuckDB для middle-big data.
Этот гайд — про практическое сравнение polars vs pandas: чем отличаются под капотом, когда и как мигрировать, какие подводные.
Зачем polars: реальные проблемы pandas
Проблема 1. Single-thread → не использует все ядра
pandas написан на Python + NumPy. Каждый groupby / merge / apply идёт в один поток (GIL не отпускается).
import pandas as pd
import time
df = pd.read_csv('events_10M.csv') # 1.4 GB
start = time.time()
result = df.groupby(['user_id', 'event_type']).agg({'amount': 'sum'})
print(f"pandas: {time.time() - start:.2f}s")
# pandas: 18.4s (1 ядро)
Проблема 2. Memory-hungry
pandas хранит каждую колонку как NumPy array → object dtype для строк = огромный overhead. На 50M строк × 30 колонок легко 30+ GB RAM.
Проблема 3. Type coercion
int + NaN = float. Колонка с NULL автоматом конвертится в float64 → точность теряется на больших ID (int64 → float64 уже на 16M+).
Проблема 4. Eager evaluation
Каждая операция выполняется сразу. Нельзя optimizers like predicate pushdown — pandas не знает про следующую операцию.
df = pd.read_csv('huge.csv') # читает 5 GB целиком
df = df[df.region == 'RU'] # затем фильтрует до 50 MB
df.groupby('user_id').sum() # далее агрегирует
# 4.95 GB прочитано впустую
Внутреннее устройство polars
polars написан на Rust + Apache Arrow (columnar memory format). Это даёт:
- Multi-threaded из коробки (multiprocessing-style без overhead pickling)
- Columnar storage — операции на колонке = последовательный memory access (cache-friendly)
- Arrow string-type — компактные строки без object overhead
- Strict type system — никакого auto-coercion
Expression API
import polars as pl
df = pl.read_csv('events_10M.csv')
# Expressions описывают трансформацию декларативно
result = df.group_by(['user_id', 'event_type']).agg(
pl.col('amount').sum().alias('total'),
pl.col('amount').mean().alias('avg'),
pl.col('order_id').n_unique().alias('orders')
)
Под капотом polars компилирует expression в Rust-операции и выполняет parallel.
lazy vs eager evaluation: query optimizer
import polars as pl
# Eager — как pandas
df = pl.read_csv('huge.csv')
df = df.filter(pl.col('region') == 'RU')
result = df.group_by('user_id').agg(pl.col('amount').sum())
# Lazy — query optimizer
result = (
pl.scan_csv('huge.csv') # описание плана
.filter(pl.col('region') == 'RU') # описание фильтра
.group_by('user_id')
.agg(pl.col('amount').sum())
.collect() # ВЫПОЛНЕНИЕ
)
В lazy-mode polars анализирует весь pipeline перед выполнением и применяет:
- Predicate pushdown — фильтр RU выполняется при чтении CSV (не читать non-RU строки)
- Projection pushdown — читает только колонки которые нужны
- Common subexpression elimination — не пересчитывает одно дважды
- Predicate combining — объединяет фильтры
На 50M-строчном датасете lazy mode часто 3-10× быстрее eager.
EXPLAIN показывает план:
result.explain()
# FILTER [(col("region")) == ("RU")]
# FROM SCAN parquet/csv/...
# PROJECT [user_id, amount, region]
Шаг 1-5: классические pandas-операции → polars
Шаг 1. read_csv → scan_csv (lazy)
# pandas
df = pd.read_csv('events.csv')
# polars eager
df = pl.read_csv('events.csv')
# polars lazy (рекомендуется)
df = pl.scan_csv('events.csv') # план, не данные
Шаг 2. groupby
# pandas
df.groupby('user_id').agg({'amount': 'sum', 'order_id': 'count'})
# polars
df.group_by('user_id').agg(
pl.col('amount').sum(),
pl.col('order_id').count()
)
Шаг 3. merge / join
# pandas
df_orders.merge(df_users, on='user_id', how='left')
# polars
df_orders.join(df_users, on='user_id', how='left')
Шаг 4. window-функции
# pandas — через transform или rolling
df['avg_7d'] = df.groupby('user_id')['amount'].transform(
lambda x: x.rolling(7).mean()
)
# polars — over() expressions
df = df.with_columns(
pl.col('amount').rolling_mean(7).over('user_id').alias('avg_7d')
)
Шаг 5. pivot / melt
# pandas
df.pivot_table(index='date', columns='region', values='amount', aggfunc='sum')
# polars
df.pivot(values='amount', index='date', columns='region', aggregate_function='sum')
# melt (unpivot)
df.melt(id_vars='date', value_vars=['region_a', 'region_b'])
Многопоточность из коробки
import os
# pandas — нужен multiprocessing.Pool
from multiprocessing import Pool
def process_user(uid):
return df[df.user_id == uid].agg(...)
with Pool(os.cpu_count()) as p:
results = p.map(process_user, user_ids)
# Сложно, требует pickling, overhead 10-20%
# polars — встроено
result = (
pl.scan_parquet('huge.parquet')
.group_by('user_id')
.agg(pl.col('amount').sum())
.collect() # автоматически 8 ядер
)
polars использует work-stealing scheduler на Rust. Не нужен ни multiprocessing, ни Dask, ни joblib для базового parallelism.
Benchmarks: 1M / 10M / 50M строк
Тест: groupby(['user_id', 'event_type']).agg(sum, mean, count) на 4-core machine.
| Размер | pandas | polars eager | polars lazy | DuckDB |
|---|---|---|---|---|
| 1M | 0.4s | 0.2s | 0.15s | 0.18s |
| 10M | 18.4s | 2.1s | 1.4s | 1.8s |
| 50M | OOM | 12s | 8s | 9s |
| 200M | — | 60s+ | 30s | 35s |
Ключевой инсайт: на >10M polars дает 8-13× speedup vs pandas. На >50M pandas просто не справляется.
JOIN-bench (50M × 5M):
| Engine | Время | Память |
|---|---|---|
| pandas | OOM | — |
| polars eager | 38s | 18 GB |
| polars lazy | 22s | 10 GB |
| DuckDB | 26s | 8 GB |
Когда НЕ polars
sklearn / statsmodels input
# sklearn принимает pandas/numpy
from sklearn.ensemble import RandomForestRegressor
# Нужно конвертировать
X_pd = df_polars.to_pandas()
model = RandomForestRegressor().fit(X_pd, y_pd)
Конверсия polars → pandas через Arrow бесплатна, но в pipeline лишний шаг.
Маленькие данные (<100K строк)
Overhead на init polars context > выигрыш. pandas для ad-hoc analysis удобнее.
Mutation-heavy код
polars immutable (как Spark). Каждая операция возвращает new DataFrame. Если нужно много мутаций (df.loc[df.col == X, 'new_col'] = ...) — pandas синтаксис компактнее.
Команда не знает Rust-style API
polars API ближе к Spark / SQL. Migration требует переучивания: .col().alias() вместо df['col'].rename(). На команде джунов с pandas-опытом — учитывай curve.
DuckDB как промежуточное решение
DuckDB — embedded SQL-движок (как SQLite, но columnar + OLAP).
import duckdb
# Прямо на pandas DataFrame
df = pd.read_csv('events.csv')
result = duckdb.sql("""
SELECT user_id, SUM(amount) AS total
FROM df
WHERE region = 'RU'
GROUP BY user_id
HAVING total > 1000
""").df()
# Прямо на Parquet файлах (zero-copy)
result = duckdb.sql("""
SELECT * FROM read_parquet('huge.parquet')
WHERE region = 'RU'
""").df()
DuckDB vs polars
| DuckDB | polars | |
|---|---|---|
| API | SQL | DataFrame expressions |
| Performance JOIN | Чуть быстрее | Чуть медленнее |
| Performance groupby | Похожи | Похожи |
| Learning curve | SQL знают все | Rust-style API |
| Mutation | Через temp tables | Immutable expressions |
| Pandas интероп | .df() через Arrow | .to_pandas() через Arrow |
Real-world паттерн: ETL + heavy aggregations через polars/DuckDB, финальный анализ + ML через pandas. Между ними zero-copy через Arrow.
FAQ
Можно мигрировать pandas код инкрементно?
Да. polars поддерживает eager API (pl.from_pandas(df)) — можно переписывать функцию за функцией. Используй df.to_pandas() чтобы вернуться в sklearn/statsmodels.
Type system strictness — это плюс или минус?
Плюс на production. polars не позволит int + str молча → меньше silent bugs. Минус на ad-hoc data exploration — больше явных конверсий.
Memory model: Arrow vs pandas
Arrow — columnar, immutable, zero-copy между процессами. pandas — row-oriented numpy arrays, copy-on-write только с pandas 2.0+. На big data Arrow существенно эффективнее.
GPU support?
polars не имеет GPU backend (на 2026). Если GPU критичен → cuDF (RAPIDS, NVIDIA). polars compensates через efficient CPU multi-threading.
Интероп polars + pandas в одном проекте
Через Arrow zero-copy: pl.from_pandas(df_pd) и df_pl.to_pandas(). Конверсия бесплатна для большинства типов (для object dtype может быть копирование).
Что дальше
- 🐍 Python-тренажёр — 532+ задач через Pyodide
- 🧠 3000+ вопросов с собесов — много про pandas/polars
- 📚 pandas: оконные функции — глубже pandas API
- 🧪 A/B-тесты через scipy.stats — statistical analysis в Python
- 🚀 ClickHouse: MV + Projections — серверная аналитика
- 🔄 dbt Incremental Models — DuckDB используется как промежуточный движок
Источники
- docs.pola.rs/user-guide/concepts/lazy-vs-eager — официальный гайд
- pola.rs/posts/benchmarks — benchmarks команды polars
- duckdb.org/docs — DuckDB документация
- arrow.apache.org — Apache Arrow
polars не заменяет pandas полностью — это новый инструмент для big-data workloads. Открой Python-тренажёр и натренируй expression-based API на разных задачах — переход с pandas займёт пару недель, а speedup на 50M+ строк окупится сразу.