Learning Platform
Глоссарий Troubleshooting
Урок 11.02 · 25 мин
Средний
polarsLazyFrameDataFramelazy-evaluationquery-optimizerpredicate-pushdownprojection-pushdownexpression-syntaxArrow-backendRun-on-Your-Machinecross-course

Polars — lazy API, query optimizer, columnar Arrow backend

Polars (Polars User Guide, 2020+) — Rust-based DataFrame library с lazy API + query optimizer + columnar Arrow memory backend. Современная alternative pandas — designed для production ETL workloads (single-machine, до ~100GB), не для interactive REPL. M10 урок 01 показал pandas eager model; этот урок объясняет Polars lazy model + parallels к М05 урок 02 (generator lazy iteration).

В этом уроке:

  1. Why Polars — production контекст + design choices (Rust + Arrow + lazy).
  2. DataFrame vs LazyFrame — eager pl.read_csv vs lazy pl.scan_csv.
  3. Query optimizer — predicate pushdown + projection pushdown.
  4. Expression syntaxpl.col(...).filter(...).sum() — declarative AST.
  5. Eager vs lazy comparison table — pandas vs Polars side-by-side.
  6. Cross-link M05 урок 02 — generator lazy → Polars lazy at expression level.
  7. Run-on-Your-Machine #2 — Polars LazyFrame demo.
  8. Cross-course → DataFusion — parallel logical-plan optimizer architecture.

Why Polars — Rust + Arrow + lazy

pandas (2008) vs Polars (2020) design comparison:

AspectpandasPolars
Implementation languageC + Cython + PythonRust + PyO3 bindings
Memory modelNumPy ndarray (default) или Arrow (opt-in pandas 2.0+)Arrow ChunkedArray (always)
Evaluationeager (computes immediately)eager OR lazy (recommended lazy для production)
Optimizernonepredicate / projection pushdown, common-subexpression-elimination
Multi-threadingmostly single-threaded; GILmulti-threaded by default (Rust no GIL)
API stylerow+column with []expression-builder pl.col(...)

Three reasons production teams adopt Polars:

  1. Rust + Arrow — multi-threaded by default; Arrow columnar layout makes column projection trivial; ~5-10x faster than pandas for medium data.
  2. Lazy + optimizerscan_csv → filter → select → collect rewrites query plan to push filters к scan layer (read меньше bytes from disk).
  3. No GIL — single Polars query can saturate всех cores на single machine; pandas required dask or modin для parallelism.

Production positioning: pandas — interactive REPL; Polars — production ETL pipeline (single-machine). Distributed workloads — Spark / DataFusion (cross-course) territory.


DataFrame vs LazyFrame — read_* vs scan_*

import polars as pl

# Eager — DataFrame, computes immediately
df = pl.read_csv('large.csv')              # читает весь файл в memory
result = df.filter(pl.col('amount') > 0).select(['category', 'amount'])

# Lazy — LazyFrame, builds query plan
lf = pl.scan_csv('large.csv')              # ничего не читает — builds AST
result = (
    lf
    .filter(pl.col('amount') > 0)          # AST node — Filter
    .select(['category', 'amount'])        # AST node — Projection
    .collect()                             # NOW реально читает + executes optimized plan
)

Critical insight: в lazy mode, scan_csv + filter + select строят query plan AST (similar к SQL parser). .collect() triggers query optimizer + execution. До .collect() ничего не выполняется.

Why scan вместо read для production: позволяет optimizer переписать plan ПЕРЕД I/O — например, filter(amount > 0) push-down к scan layer means Polars читает только rows где amount > 0 (если файл columnar Parquet — skip целые row groups). Без lazy mode это невозможно.

Cite Polars User Guide — Lazy API.


Query optimizer — predicate / projection pushdown

import polars as pl

result = (
    pl.scan_csv('orders.csv')
    .select(['user_id', 'amount', 'date', 'category', 'currency'])  # projection
    .filter(pl.col('amount') > 100)                                  # predicate
    .filter(pl.col('category') == 'tech')                            # predicate
    .group_by('user_id')
    .agg(pl.col('amount').sum())
    .collect()
)

Optimizer rewrites plan:

  1. Predicate pushdownfilter(amount > 100) AND filter(category == 'tech') пушится к scan layer. Если orders.csv — Parquet, optimizer использует row-group min/max statistics (cross-link M09 урок 04 binary-formats-overview + Storage Formats M02) и читает только row groups где max(amount) > 100 AND category in unique_values.

  2. Projection pushdown — final result использует только user_id и amount (после group_by + agg). select декларирует 5 колонок, но date и currency дальше не используются. Optimizer drops их к scan layer — читаются только 2 колонки из CSV/Parquet.

  3. Common subexpression elimination — если pl.col('amount') * 1.2 появляется дважды, evaluator вычисляет один раз и кэширует.

  4. Type coercionpl.col('amount') > 100 (int) — auto-cast если amount — float64 (no manual cast).

Цена этих оптимизаций — 0 строк user-кода. Это и есть pedagogical insight: lazy позволяет машине переписывать ваш запрос, eager — нет.

Inspect optimized plan:

print(lf.explain())  # textual representation: filter pushdown, projection pruning visible

Cite Polars User Guide — Optimizations.


Expression syntax — pl.col(...).filter(...).sum()

Polars expressions — declarative AST nodes, built via method-chain:

import polars as pl

# Single column expression
expr1 = pl.col('amount')                          # column reference

# Method chain — filter + aggregation
expr2 = pl.col('amount').filter(pl.col('y') > 0).sum()

# Cross-column expression
expr3 = (pl.col('amount') * pl.col('quantity')).alias('total')

# Wildcard expression — apply к всем колонкам
expr4 = pl.col('*').exclude('id').mean()          # mean каждой колонки кроме 'id'

# Используется в select / with_columns / agg
df.select([
    pl.col('user_id'),
    expr2.alias('positive_sum'),
    expr3,
])

Key idea: expressions — values (можно сохранить в variable, передать в функцию, reuse). Не df['col'].filter(...) (как в pandas, где filter — method DataFrame), а pl.col('col').filter(...) (где filter — method expression). Это даёт type-safe + composable API.

Cross-link к Polars Expr API: docs.pola.rs/api/python/stable/reference/expressions.


Eager vs lazy — pandas vs Polars side-by-side

Aspectpandas (eager)Polars (lazy via scan_*)
When computedimmediatelyat .collect() call
Memory modelnumpy.ndarray backing (или Arrow opt-in 2.0+)Arrow ChunkedArray (always)
Optimizernone (executes as-written)predicate / projection pushdown, CSE, type coercion
Mutationdf['x'] = ... in-place commonimmutable + chain only
API styledf.method(args)df.method(pl.expr1, pl.expr2)
Multi-threadingmostly GIL-boundmulti-core by default (Rust)
Failure moderuntime errors (KeyError, TypeError per row)schema-validated at plan-build (early errors)
Best forinteractive REPL / small dataproduction ETL / medium-large data

Production rule: для production pipelines — Polars lazy (scan_* → … → .collect()); для interactive analysis в Jupyter — pandas (или Polars eager).


Concept ladder — lazy evaluation в Python:

LevelMechanismExampleM_module
1. Iteration-levelyield keyworddef chunks(): yield from fM05 урок 02 (generator climax)
2. Expression-levelquery plan ASTpl.col(...).filter(...)M10 урок 02 (this lesson)
3. Distributed planRDD lineage / DAGdf.filter(...).select(...) (Spark)Spark course (cross-course)

Same idea — defer computation; build representation; execute later — applied at three abstraction levels.

Cross-link М05 урок 02 (generators-pygen-frames) — PyGenObject сохраняет state между next() calls; computation defers per-step (lazy iteration). Polars LazyFrame делает то же самое на уровне query plan — defer execution до .collect().

Cross-link forward к DataFusion M02 architecture (logical-plan) — DataFusion — Rust-native query engine с той же logical-plan-optimizer architecture (DataFusion использует Apache Arrow + Datafusion-expr crate). Если поняли Polars lazy — поняли DataFusion 80%.


Run-on-Your-Machine — Polars LazyFrame demo

TIP

Run-on-Your-Machine: Polars LazyFrame demo

Установите локально (Polars Rust binary 30MB+; не bundled в Pyodide default):

pip install 'polars>=1.0,<2.0'

Создайте файл data.csv:

category,amount
food,10
food,20
tech,100
tech,200
travel,50

Создайте файл polars_demo.py:

import polars as pl

lf = pl.scan_csv('data.csv')  # LazyFrame — нет реального чтения
result = (
    lf
    .filter(pl.col('amount') > 0)
    .group_by('category')
    .agg(pl.col('amount').mean().alias('mean_amount'))
    .collect()  # NOW реально читает + выполняет optimized plan
)
print(result)

# Inspect optimized plan
print(lf.filter(pl.col('amount') > 0).explain())

Запустите:

python3 polars_demo.py

.explain() напечатает план выполнения — увидите predicate pushdown в действии (filter пушится к Csv SCAN). Version pin >=1.0,<2.0 (Pitfall 32) — Polars 1.x stable API; 2.x возможен с breaking changes.


Cross-course → DataFusion architecture + query optimization

DataFusion — Rust-native SQL/DataFrame query engine, использует Apache Arrow как in-memory representation. Architectural parallel к Polars:

  • DataFusion M02 architecture — query pipeline / logical plan / physical plan / catalog. Polars LazyFrame ≈ DataFusion logical plan. Если понимаете Polars .explain() output — поймёте DataFusion df.explain() output аналогично.

  • DataFusion M06 query-optimization — predicate pushdown / projection pushdown / join optimization / common subexpression elimination — те же rewrites которые Polars делает internally. DataFusion exposes них explicitly как optimizer rules в Rust API.

Cross-course bridge insight: Polars + DataFusion разделяют design DNA — оба Rust-based, оба Arrow-backed, оба lazy + optimizer. DataFusion (более низкоуровневый, designed для embeddable engine) — Polars (более высокоуровневый, designed для DataFrame DSL). Полезно прочитать DataFusion M02 чтобы понять “как работает” query optimizer внутри Polars.


Что в следующем уроке

М10 урок 03 — PyArrow (Apache Arrow Python bindings). Полный stack: pandas (eager) → Polars (lazy) → PyArrow (memory layer). Урок 03 покрывает Arrow memory model + zero-copy semantics + heavy cross-course → Storage Formats M07 (7 уроков deep dive в Arrow internals).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. **Apply scenario:** учащийся пишет `lf = pl.scan_csv('data.csv'); print(lf)`. Что выведет — содержимое файла или query plan?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7