How to Migrate a pandas Pipeline to Polars

Polars' lazy API defers all computation until .collect(), letting its query planner fuse operations and parallelize across cores. Migrating a pandas join-groupby-rolling pipeline typically takes under 50 lines and pays back 5-30x in wall-clock time.

Data Engineerpolarspandasperformance

Why Polars is faster than pandas

pandas executes eagerly row by row using a single thread. Polars builds a lazy query plan - every scan_csv, join, group_by, and with_columns call adds a node to the plan without touching data. When you call .collect(), the Rust-based query planner optimizes the order, pushes predicates down, and runs the physical plan parallelized across all cores.

For join-heavy ETL on 100k-1M rows, Polars is typically 5-30x faster than pandas with minimal code changes.

The pandas starting point

import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["created_at"])
customers = pd.read_csv("customers.csv")

merged = orders.merge(customers, left_on="customer_id", right_on="id")
merged["week"] = merged["created_at"].dt.isocalendar().week
weekly = merged.groupby(["region", "week"])["amount"].sum().reset_index(name="revenue")
weekly = weekly.sort_values(["region", "week"])
weekly["revenue_4w_avg"] = (
    weekly.groupby("region")["revenue"].transform(lambda s: s.rolling(4, min_periods=1).mean())
)
weekly.to_csv("revenue.csv", index=False)

The Polars lazy port

Replace every pandas call with its lazy Polars equivalent. The structure is almost identical - the key differences are scan_csv (lazy) instead of read_csv (eager), column expressions inside .agg(), and .over() for per-group window functions.

import polars as pl

orders = pl.scan_csv("orders.csv", try_parse_dates=True)
customers = pl.scan_csv("customers.csv")

result = (
    orders
    .join(customers, left_on="customer_id", right_on="id", how="inner")
    .with_columns(pl.col("created_at").dt.week().alias("week"))
    .group_by(["region", "week"])
    .agg(pl.col("amount").sum().alias("revenue"))
    .sort(["region", "week"])
    .with_columns(
        pl.col("revenue")
          .rolling_mean(window_size=4, min_samples=1)
          .over("region")
          .alias("revenue_4w_avg")
    )
)

df = result.collect()   # query plan executes here
df.write_csv("revenue.csv")

Key migration patterns

When to reach for Polars

Polars is the right default for single-machine ETL in the 1 MB-50 GB range. Below 1 MB, pandas is fine. Above 50 GB, add result.lazy().collect(streaming=True) for constant-memory processing - or reach for a distributed system.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I migrate a pandas pipeline to Polars?

Replace pd.read_csv with pl.scan_csv (lazy), use pl.col() expressions inside .agg() instead of Series methods, replace groupby+transform window functions with .rolling_mean().over('region'), and call .collect() at the end to execute the plan.

Why is Polars faster than pandas for joins and groupbys?

Polars builds a lazy query plan and executes it with a Rust query planner that parallelizes across all CPU cores and fuses operations. pandas executes eagerly in a single thread using NumPy, which cannot take advantage of multiple cores.

What is the Polars lazy API and when should I use it?

The lazy API (scan_csv, LazyFrame, .collect()) defers execution so the query planner can optimize and parallelize the full pipeline. Use it whenever you join, filter, or aggregate - essentially all ETL work. Only use pl.read_csv (eager) for small exploratory loads.

Keep learning

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →