C.W.K.
Stream
Lesson 04 of 06 · published

Polars — When Pandas Hits a Wall

~13 min · polars, performance, lazy

Level 0Curious Reader
0 XP0/47 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The Rust-backed alternative

Polars (1.39.3 as of April 2026) is a DataFrame library written in Rust with Python bindings. It uses Arrow as its in-memory format, executes queries in parallel by default, and has both an eager API (familiar to Pandas users) and a lazy API (queries are built up as a plan, optimized, and executed in one pass — like SQL).

When to reach for it over Pandas

  • Your data is bigger than memory, but you don't need a distributed system. pl.scan_* with lazy eval streams.
  • You're doing many transformations and Pandas is taking minutes. Polars's parallel execution often closes that to seconds.
  • You want a query planner. Polars optimizes; Pandas executes literally what you wrote.
  • You're running on a many-core machine and Pandas is leaving cores idle.

When to stick with Pandas

  • Tons of ecosystem libraries (statsmodels, sklearn, plotnine) accept Pandas DataFrames natively.
  • The data fits comfortably in memory and is small enough that the speedup doesn't matter.
  • You're doing exploratory work and need to print things mid-chain — Polars's lazy plans don't give you intermediate results without .collect().

Code

Polars eager API — feels like Pandas, but parallel by default·python
import polars as pl

df = pl.read_parquet('orders.parquet')

monthly = (
    df.filter(pl.col('status') == 'completed')
      .with_columns(pl.col('order_date').dt.truncate('1mo').alias('month'))
      .group_by('month')
      .agg([
          pl.col('amount_usd').sum().alias('revenue'),
          pl.col('order_id').n_unique().alias('orders'),
      ])
      .sort('month')
)
Polars lazy API — build a query, optimize, execute in one pass·python
import polars as pl

monthly = (
    pl.scan_parquet('warehouse/orders/year=2026/**/*.parquet')
      .filter(pl.col('status') == 'completed')
      .with_columns(pl.col('order_date').dt.truncate('1mo').alias('month'))
      .group_by('month')
      .agg([
          pl.col('amount_usd').sum().alias('revenue'),
          pl.col('order_id').n_unique().alias('orders'),
      ])
      .sort('month')
      .collect(streaming=True)   # streams in chunks; bigger-than-memory works
)

# Inspect the optimized plan that Polars actually ran
print(
    pl.scan_parquet('warehouse/orders/year=2026/**/*.parquet')
      .filter(pl.col('status') == 'completed')
      .explain(optimized=True)
)

External links

Exercise

Take the same monthly-revenue computation you've been writing in Pandas and rewrite it in Polars (lazy). Run both with %timeit-style timing. Notice that Polars wins by more on bigger files — and that the lazy plan, when you .explain() it, looks like a query plan from a real database.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.