
If you are still using Pandas for multi-gigabyte data transformations in Python, you are paying a massive "Pandas tax": single-threaded CPU execution, eager in-memory copies, and memory bloat requiring 5x to 10x the raw dataset size in RAM before throwing an OutOfMemoryError.
In modern data pipelines, Polars and DuckDB have replaced Pandas as the standard toolkit for high-performance single-node analytics. Both leverage columnar storage, vectorized SIMD execution, and Apache Arrow, delivering 10x to 50x speedups while processing datasets larger than physical RAM.
Here is how they work, how they compare, and how to use them together in production.
Dataset on Disk (Parquet/CSV): 2.0 GB
Pandas in RAM: 12.0 GB - 18.0 GB (Peak during merge/groupby)
Polars in RAM: 2.3 GB (Zero-copy Arrow + streaming)
DuckDB in RAM: 0.8 GB (Chunked vectorized execution engine)
Pandas was designed in 2008 around NumPy 1D arrays. It has three fundamental bottlenecks:
NaN to float64, doubling memory usage.Polars is written from scratch in Rust and built directly on the Apache Arrow columnar memory format. It processes data using all available CPU cores without Python GIL restrictions.
In production, you should almost always use Polars' Lazy API (LazyFrame). Instead of running computations immediately, Polars constructs a Logical Plan, optimizes it (predicate pushdown, projection pushdown, slice pushdown), and executes it in parallel:
import polars as pl
# Construct lazy query plan - zero disk I/O occurs here
lazy_query = (
pl.scan_parquet("s3://analytics-bucket/events/*.parquet")
.filter(pl.col("timestamp") >= pl.date(2026, 1, 1))
.filter(pl.col("event_type").is_in(["purchase", "subscription"]))
.with_columns([
(pl.col("amount_cents") / 100.0).alias("amount_usd"),
pl.col("user_id").n_unique().over("country").alias("unique_users_per_country")
])
.group_by(["country", "event_type"])
.agg([
pl.col("amount_usd").sum().alias("total_revenue"),
pl.col("amount_usd").mean().alias("avg_order_value"),
pl.len().alias("transaction_count")
])
.sort("total_revenue", descending=True)
)
# Inspect the optimized query plan
print(lazy_query.explain())
# Execute optimized plan in parallel across all CPU cores
result_df = lazy_query.collect(streaming=True)
print(result_df)
When you call .scan_parquet() with filters:
timestamp >= 2026-01-01 without reading the data from disk.timestamp, event_type, amount_cents, country), ignoring the remaining 50 columns in the file.streaming=True): Processes data in streaming micro-batches, allowing transformations on datasets that exceed your machine's physical RAM.While Polars provides a DataFrame API, DuckDB is an embedded in-process SQL OLAP database. It runs inside your Python process with zero external server dependencies, zero network latency, and native SQL dialect support.
DuckDB can execute SQL queries directly against compressed Parquet, CSV, or JSON files on disk or remote S3 without loading them into database tables first:
import duckdb
# Connect to in-process DuckDB instance (or persist to 'analytics.duckdb')
con = duckdb.connect()
# Enable S3 / HTTP filesystem extension
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("""
SET s3_region='us-east-1';
SET s3_access_key_id='YOUR_KEY';
SET s3_secret_access_key='YOUR_SECRET';
""")
# Query 100GB of remote Parquet files using Vectorized SQL
query = """
SELECT
country,
event_type,
COUNT(DISTINCT user_id) AS unique_users,
ROUND(SUM(amount_cents) / 100.0, 2) AS total_revenue_usd,
ROUND(AVG(amount_cents) / 100.0, 2) AS aov_usd
FROM read_parquet('s3://analytics-bucket/events/year=2026/*/*.parquet')
WHERE event_type IN ('purchase', 'subscription')
GROUP BY country, event_type
HAVING total_revenue_usd > 10000
ORDER BY total_revenue_usd DESC
LIMIT 20;
"""
# Execute and fetch directly to Arrow, Polars, or Python dictionaries
results = con.execute(query).pl() # Returns native Polars DataFrame
print(results)
Because both Polars and DuckDB use Apache Arrow for memory representation, you can pass data between them with zero memory serialization or copy overhead:
import polars as pl
import duckdb
# 1. Load and clean data with Polars
df = pl.DataFrame({
"user_id": [101, 102, 103, 104],
"scores": [88.5, 92.0, 79.5, 95.0],
"tier": ["gold", "platinum", "gold", "platinum"]
})
# 2. Run complex analytical window SQL in DuckDB directly on the Polars DataFrame
con = duckdb.connect()
# DuckDB can reference the 'df' Python variable directly in the FROM clause!
sql_result = con.execute("""
SELECT
user_id,
scores,
tier,
RANK() OVER (PARTITION BY tier ORDER BY scores DESC) as rank_in_tier,
AVG(scores) OVER (PARTITION BY tier) as tier_avg
FROM df
""").arrow() # Zero-copy Arrow Table
# 3. Convert back to Polars instantaneously
final_df = pl.from_arrow(sql_result)
print(final_df)
Aggregating and filtering a 10M-row dataset on an 8-core, 16GB RAM developer laptop:
| Operation | Pandas 2.2 | Polars (Eager) | Polars (Lazy) | DuckDB (SQL) |
|---|---|---|---|---|
| Read Parquet + Filter | 4.82s | 0.82s | 0.29s | 0.31s |
| Group By + 4 Aggs | 3.15s | 0.41s | 0.28s | 0.24s |
| Window Function | 2.40s | 0.35s | 0.26s | 0.21s |
| Peak RAM Usage | ~4.6 GB | ~1.4 GB | ~0.7 GB | ~0.4 GB |
Dataset Scale & Use Case
│
┌─────────────────────────────────┴─────────────────────────────────┐
▼ ▼
Single Machine (< 500GB) Distributed Cluster (> 1TB)
│ │
┌────┴──────────────────────────┐ ┌─────────┴─────────┐
▼ ▼ ▼ ▼
SQL-Heavy / S3 Parquet DataFrame Transformations Batch Pipeline Real-time OLAP
▼ ▼ ▼ ▼
DuckDB Polars Apache Spark ClickHouse
(Embedded SQL) (Rust Multi-thread) / Ray / StarRocks
A data engineer is processing a 50GB Parquet dataset on a machine with 16GB RAM. They need to calculate the sum of 'sales_amount' for each 'product_category' and then filter for categories with total sales exceeding $1,000,000. Which Polars approach is most appropriate to avoid `OutOfMemoryError` and ensure efficient execution?
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Columnar data formats benchmarked: disk compression with Parquet, in-memory zero-copy with Arrow, and vectorized query execution with DuckDB.
Read more
Pragmatic guide to Python's high-performance data structures: collections.deque, Counter, defaultdict, heapq, and slots dataclasses for 10x faster code.
Read more
Master SQLite in high-throughput production environments. Learn Write-Ahead Logging (WAL), busy timeout tuning, concurrent reader/writer limits, and pragmatic benchmarks.
Read more