Polars DataFrames
Overview
Polars is a high-performance DataFrame library for Python built on Apache Arrow with a Rust backend. It provides an expression-based API with lazy evaluation and automatic parallelization for efficient data processing, transformation, and analysis.
When to Use
- Processing tabular datasets from 100 MB to 100 GB that fit in RAM
- ETL pipelines requiring fast read/transform/write cycles
- Replacing pandas when performance matters (10–100x speedup typical)
- Lazy query pipelines with automatic optimization (predicate/projection pushdown)
- Joining, pivoting, and reshaping large tables
- Reading Parquet, CSV, JSON, or cloud-stored data efficiently
- Window functions and complex grouped aggregations
- For larger-than-RAM data, use Dask or Vaex instead
- For GPU-accelerated DataFrames, use cuDF instead
Prerequisites
pip install polars
# Optional extras:
pip install polars[all] # All I/O backends
pip install polars[pandas] # Pandas interop
pip install polars[numpy] # NumPy interop
pip install connectorx sqlalchemy # Database connectivity
Quick Start
import polars as pl
# Create DataFrame
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"dept": ["Sales", "Eng", "Sales", "Eng"],
"salary": [70000, 85000, 72000, 90000],
})
# Expression-based pipeline
result = (
df.filter(pl.col("salary") > 71000)
.with_columns(bonus=pl.col("salary") * 0.1)
.group_by("dept")
.agg(
pl.col("salary").mean().alias("avg_salary"),
pl.len().alias("count"),
)
)
print(result)
# shape: (2, 3)
# ┌───────┬────────────┬───────┐
# │ dept ┆ avg_salary ┆ count │
# ├───────┼────────────┼───────┤
# │ Eng ┆ 87500.0 ┆ 2 │
# │ Sales ┆ 72000.0 ┆ 1 │
# └───────┴────────────┴───────┘
Core API
1. DataFrame Operations
Select, filter, add/modify columns, sort, and sample rows.
import polars as pl
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5],
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age": [25, 30, 35, 28, 32],
"score": [88.5, 92.0, 76.3, 95.1, 84.7],
})
# Select columns (with computed expressions)
selected = df.select(
"name",
pl.col("age"),
(pl.col("score") / 100).alias("score_pct"),
)
print(selected.shape) # (5, 3)
# Filter rows (multiple conditions → implicit AND)
filtered = df.filter(
pl.col("age") > 27,
pl.col("score") > 80,
)
print(filtered.shape) # (3, 4) — Bob, Diana, Eve
# Add columns (preserves existing)
enriched = df.with_columns(
grade=pl.when(pl.col("score") >= 90).then(pl.lit("A"))
.when(pl.col("score") >= 80).then(pl.lit("B"))
.otherwise(pl.lit("C")),
age_months=pl.col("age") * 12,
)
print(enriched.columns)
# ['id', 'name', 'age', 'score', 'grade', 'age_months']
# Sort
df.sort("score", descending=True).head(3)
2. GroupBy & Aggregations
Group rows and compute summary statistics.
import polars as pl
sales = pl.DataFrame({
"region": ["East", "West", "East", "West", "East", "West"],
"product": ["A", "A", "B", "B", "A", "B"],
"revenue": [100, 150, 200, 180, 120, 210],
"units": [10, 15, 20, 18, 12, 21],
})
# Basic group_by
summary = sales.group_by("region").agg(
pl.col("revenue").sum().alias("total_rev"),
pl.col("revenue").mean().alias("avg_rev"),
pl.len().alias("n_transactions"),
)
print(summary)
# Multiple keys + conditional aggregation
by_rp = sales.group_by("region", "product").agg(
pl.col("revenue").sum(),
(pl.col("units") > 15).sum().alias("large_orders"),
)
print(by_rp)
# Window functions with over() — add group stats without collapsing rows
enriched = sales.with_columns(
region_avg=pl.col("revenue").mean().over("region"),
rank_in_region=pl.col("revenue").rank(descending=True).over("region"),
pct_of_region=pl.col("revenue") / pl.col("revenue").sum().over("region"),
)
print(enriched.select("region", "product", "revenue", "region_avg", "rank_in_region"))
3. Joins
Combine DataFrames on shared keys.
import polars as pl
customers = pl.DataFrame({
"cid": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "Diana"],
})
orders = pl.DataFrame({
"oid": [101, 102, 103, 104],
"cid": [1, 2, 1, 5],
"amount": [100, 200, 150, 300],
})
# Inner join — only matching rows
inner = customers.join(orders, on="cid", how="inner")
print(inner.shape) # (3, 4) — cid 1 (×2), cid 2
# Left join — all left rows, nulls where no match
left = customers.join(orders, on="cid", how="left")
print(left.shape) # (4, 4) — Charlie and Diana have null amount
# Anti join — left rows WITHOUT a match in right
no_orders = customers.join(orders, on="cid", how="anti")
print(no_orders["name"].to_list()) # ['Charlie', 'Diana']
# Join on different column names
customers.join(orders, left_on="cid", right_on="cid", suffix="_order")
# Asof join — match to nearest timestamp (time-series alignment)
quotes = pl.DataFrame({
"time": [1.0, 2.0, 3.0, 4.0],
"price": [100, 101, 102, 103],
}).cast({"time": pl.Float64})
trades = pl.DataFrame({
"time": [1.5, 3.2],
"qty": [50, 75],
}).cast({"time": pl.Float64})
result = trades.join_asof(quotes, on="time", strategy="backward")
print(result)
# time=1.5 matched price=100, time=3.2 matched price=102
4. Reshaping
Pivot, unpivot, explode, and transpose operations.
import polars as pl
# --- Pivot (long → wide) ---
long = pl.DataFrame({
"date": ["Jan", "Jan", "Feb", "Feb"],
"product": ["A", "B", "A", "B"],
"sales": [100, 150, 120, 160],
})
wide = long.pivot(values="sales", index="date", columns="product")
print(wide)
# date | A | B
# Jan | 100 | 150
# Feb | 120 | 160
# --- Unpivot (wide → long) ---
back_to_long = wide.unpivot(
index="date", on=["A", "B"],
variable_name="product", value_name="sales",
)
print(back_to_long.shape) # (4, 3)
# --- Explode list columns ---
nested = pl.DataFrame({
"id": [1, 2],
"tags": [["a", "b", "c"], ["d", "e"]],
})
flat = nested.explode("tags")
print(flat.shape) # (5, 2)
5. Data I/O
Read and write CSV, Parquet, JSON, Excel, databases, and cloud storage.
import polars as pl
# --- CSV ---
df = pl.read_csv("data.csv")
df.write_csv("output.csv")
# --- Parquet (recommended for performance) ---
df = pl.read_parquet("data.parquet")
df.write_parquet("output.parquet", compression="zstd")
# --- JSON / NDJSON ---
df = pl.read_ndjson("data.ndjson")
df.write_ndjson("output.ndjson")
# --- Excel ---
df = pl.read_excel("data.xlsx", sheet_name="Sheet1")
df.write_excel("output.xlsx")
# --- Lazy scan (preferred for large files) ---
lf = pl.scan_csv("large.csv")
result = lf.filter(pl.col("value") > 0).select("id", "value").collect()
print(result.shape)
# --- Database ---
df = pl.read_database_uri(
"SELECT * FROM users WHERE age > 25",
uri="postgresql://user:pass@localhost/db",
)
# --- Cloud storage (S3, GCS, Azure) ---
df = pl.read_parquet("s3://bucket/data.parquet")
df = pl.scan_parquet("gs://bucket/data/*.parquet").collect()
# --- Partitioned Parquet (Hive-style) ---
df.write_parquet("output_dir", partition_by=["year", "month"])
lf = pl.scan_parquet("output_dir/**/*.parquet")
6. Expression API
String, datetime, list, and conditional operations.
import polars as pl
from datetime import date
df = pl.DataFrame({
"text": ["Hello World", "foo bar", "POLARS"],
"dt": [date(2023, 1, 15), date(2023, 6, 30), date(2024, 12, 1)],
"values": [[1, 2, 3], [4, 5], [6]],
})
# String operations
strings = df.select(
lower=pl.col("text").str.to_lowercase(),
length=pl.col("text").str.len_chars(),
contains_o=pl.col("text").str.contains("o"),
split=pl.col("text").str.split(" "),
)
print(strings)
# Datetime operations
dates = df.select(
year=pl.col("dt").dt.year(),
month=pl.col("dt").dt.month(),
weekday=pl.col("dt").dt.weekday(),
quarter=pl.col("dt").dt.quarter(),
)
print(dates)
# List operations
lists = df.select(
list_len=pl.col("values").list.len(),
list_sum=pl.col("values").list.sum(),
first=pl.col("values").list.first(),
)
print(lists)
# Conditional expressions (when/then/otherwise)
df = pl.DataFrame({"score": [45, 72, 88, 95, 60]})
result = df.with_columns(
grade=pl.when(pl.col("score") >= 90).then(pl.lit("A"))
.when(pl.col("score") >= 80).then(pl.lit("B"))
.when(pl.col("score") >= 70).then(pl.lit("C"))
.otherwise(pl.lit("F")),
)
print(result)
# Null handling
df2 = pl.DataFrame({"x": [1, None, 3, None, 5]})
filled = df2.with_columns(
filled=pl.col("x").fill_null(0),
forward=pl.col("x").fill_null(strategy="forward"),
is_null=pl.col("x").is_null(),
)
print(filled)
# Multi-column operations with regex selector
df3 = pl.DataFrame({"val_a": [1, 2], "val_b": [3, 4], "name": ["x", "y"]})
doubled = df3.select(pl.col("^val_.*$") * 2)
print(doubled)
7. Lazy Evaluation
Build optimized query plans before execution.
import polars as pl
# Lazy mode: build plan, optimize, then execute
lf = pl.scan_csv("large_dataset.csv")
result = (
lf
.select("user_id", "category", "amount", "date") # projection pushdown
.filter(pl.col("amount") > 100) # predicate pushdown
.with_columns(pl.col("date").str.to_date())
.group_by("category")
.agg(
pl.col("amount").sum().alias("total"),
pl.col("user_id").n_unique().alias("unique_users"),
)
.sort("total", descending=True)
)
# Inspect the optimized plan
print(result.explain())
# Execute
df = result.collect()
print(df)
# Streaming mode for very large data
lf = pl.scan_parquet("data/*.parquet")
result = (
lf
.filter(pl.col("year") >= 2023)
.group_by("region")
.agg(pl.col("sales").sum())
.collect(streaming=True) # processes in batches
)
print(result)
# Sink directly to file (no full materialization)
lf.filter(pl.col("active")).sink_parquet("filtered_output.parquet")
Key Concepts
Lazy vs Eager Comparison
| Aspect |
Eager (DataFrame) |
Lazy (LazyFrame) |
| Created by |
pl.read_*(), pl.DataFrame() |
pl.scan_*(), df.lazy() |
| Execution |
Immediate |
On .collect() |
| Optimization |
None |
Predicate/projection pushdown, join reordering |
| Streaming |
No |
collect(streaming=True) |
| Best for |
Small data, interactive |
Large data, pipelines |
Polars Data Types
| Type |
Python equivalent |
Notes |
Int8/16/32/64 |
int |
Choose smallest sufficient size |
UInt8/16/32/64 |
int |
Unsigned |
Float32/64 |
float |
Float64 default |
Boolean |
bool |
|
Utf8 |
str |
String type |
Categorical |
— |
Low-cardinality strings (faster groupby) |
Date |
datetime.date |
Date without time |
Datetime |
datetime.datetime |
With microsecond precision |
Duration |
datetime.timedelta |
Time difference |
List |
list |
Variable-length lists |
Struct |
dict |
Named fields |
Null |
None |
All-null column |
Key Differences from Pandas
- No index: Row access by position only; no
.loc/.iloc with labels
- Strict typing: No silent type coercion; explicit
.cast() required
- Expressions, not methods:
pl.col("x").mean() instead of df["x"].mean()
- Parallel by default: All column operations run in parallel
- Lazy evaluation: Available via
LazyFrame for query optimization
Common Workflows
1. ETL Pipeline (CSV → Clean → Parquet)
import polars as pl
# Extract
lf = pl.scan_csv(
"raw_data.csv",
dtypes={"id": pl.Int64, "date": pl.Utf8, "amount": pl.Float64},
)
# Transform
cleaned = (
lf
.with_columns(pl.col("date").str.to_date("%Y-%m-%d"))
.filter(pl.col("amount").is_not_null())
.with_columns(
year=pl.col("date").dt.year(),
month=pl.col("date").dt.month(),
amount_log=pl.col("amount").log(),
)
.drop_nulls()
)
# Load
cleaned.collect().write_parquet("clean_data.parquet", compression="zstd")
print("ETL complete")
2. Multi-Source Join and Aggregation
import polars as pl
# Simulate three data sources
users = pl.DataFrame({
"uid": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "Diana"],
"region": ["East", "West", "East", "West"],
})
orders = pl.DataFrame({
"oid": range(1, 7),
"uid": [1, 1, 2, 3, 3, 3],
"amount": [100, 200, 150, 50, 75, 125],
})
products = pl.DataFrame({
"oid": range(1, 7),
"category": ["Elec", "Books", "Elec", "Books", "Elec", "Elec"],
})
# Join → aggregate
result = (
orders
.join(users, on="uid", how="left")
.join(products, on="oid", how="left")
.group_by("region", "category")
.agg(
pl.col("amount").sum().alias("total"),
pl.col("amount").mean().alias("avg_order"),
pl.len().alias("n_orders"),
)
.sort("total", descending=True)
)
print(result)
3. Time-Series Feature Engineering
Uses: GroupBy, Window functions, Joins, Expression API.
- Load time-series data with
pl.scan_csv() or pl.scan_parquet()
- Parse dates:
.with_columns(pl.col("date").str.to_date())
- Sort by entity and date:
.sort("entity_id", "date")
- Add lag features:
pl.col("value").shift(n).over("entity_id")
- Add rolling statistics:
pl.col("value").rolling_mean(window_size=7).over("entity_id")
- Compute percent change:
(pl.col("value") - pl.col("value").shift(1)) / pl.col("value").shift(1)
- Collect and write:
.collect().write_parquet("features.parquet")
Key Parameters
| Parameter |
Function |
Default |
Range/Options |
Effect |
how |
.join() |
"inner" |
inner, left, outer, cross, semi, anti |
Join type |
strategy |
.join_asof() |
"backward" |
backward, forward, nearest |
Asof match direction |
streaming |
.collect() |
False |
True/False |
Process in batches for large data |
compression |
.write_parquet() |
"zstd" |
snappy, gzip, brotli, lz4, zstd, uncompressed |
Parquet compression |
partition_by |
.write_parquet() |
None |
List of columns |
Hive-style partitioning |
rechunk |
pl.concat() |
False |
True/False |
Rechunk memory after concat |
aggregate_function |
.pivot() |
"first" |
first, sum, mean, max, min, count |
Duplicate handling in pivot |
n_rows |
pl.read_csv() |
None |
Positive int |
Limit rows read (for sampling) |
parallel |
pl.read_csv() |
"auto" |
auto, columns, row_groups, none |
Parallel reading strategy |
dtypes |
pl.read_csv() |
None |
Dict of column→type |
Override type inference |
Best Practices
Use lazy mode for large datasets: pl.scan_csv() not pl.read_csv(). Enables query optimization and streaming.
Stay in the expression API: Avoid .map_elements() (runs Python, no parallelism). Prefer native Polars operations — string, datetime, list namespaces cover most needs.
Select early, filter early: Place .select() and .filter() as early as possible in lazy pipelines. The optimizer can push these down but explicit placement helps.
Use Categorical for low-cardinality strings: df.with_columns(pl.col("region").cast(pl.Categorical)) — dramatically speeds up groupby and joins on repeated string values.
Prefer Parquet over CSV: Parquet preserves types, supports predicate pushdown, and is 5–10x smaller. Use compression="zstd" for best compression/speed balance.
Anti-pattern — Python loops over rows: Never iterate rows with for row in df.iter_rows() for computation. Use expressions instead.
Anti-pattern — chaining .with_columns() calls: Combine multiple column additions into a single .with_columns() call for parallel execution.
Common Recipes
Recipe: Pandas Migration Pattern
import polars as pl
import pandas as pd
# Convert pandas → polars
pd_df = pd.DataFrame({"col": [1, 2, 3], "group": ["a", "b", "a"]})
pl_df = pl.from_pandas(pd_df)
# Key operation mapping:
# pandas: df["col"] → polars: df.select("col")
# pandas: df[df["col"] > 1] → polars: df.filter(pl.col("col") > 1)
# pandas: df.assign(x=...) → polars: df.with_columns(x=...)
# pandas: df.groupby().agg() → polars: df.group_by().agg()
# pandas: df.groupby().transform → polars: pl.col(...).over(...)
# pandas: df.merge() → polars: df.join()
# pandas: df.melt() → polars: df.unpivot()
# Convert back
pd_result = pl_df.to_pandas()
Recipe: Complex Aggregation Report
import polars as pl
df = pl.DataFrame({
"dept": ["Sales", "Eng", "Sales", "Eng", "Sales", "Eng"],
"level": ["Jr", "Sr", "Sr", "Jr", "Jr", "Sr"],
"salary": [50000, 95000, 75000, 70000, 55000, 100000],
})
report = (
df.group_by("dept", "level")
.agg(
pl.col("salary").mean().alias("avg_sal"),
pl.col("salary").median().alias("med_sal"),
pl.col("salary").std().alias("std_sal"),
pl.len().alias("count"),
)
.pivot(values="avg_sal", index="dept", columns="level")
.with_columns(
diff=pl.col("Sr") - pl.col("Jr"),
)
)
print(report)
Recipe: Reading Multiple Files with Schema Alignment
import polars as pl
from pathlib import Path
# Read multiple CSVs with potentially different columns
files = sorted(Path("data/").glob("*.csv"))
dfs = [pl.read_csv(f) for f in files]
# Diagonal concat handles mismatched schemas (fills nulls)
combined = pl.concat(dfs, how="diagonal")
print(f"Combined: {combined.shape}")
print(f"Columns: {combined.columns}")
# Or use lazy scan for Parquet (automatic parallel)
lf = pl.scan_parquet("data/**/*.parquet")
result = lf.filter(pl.col("date") > "2023-01-01").collect()
Troubleshooting
| Problem |
Cause |
Solution |
SchemaError: column not found |
Column name typo or case mismatch |
Check df.columns; Polars is case-sensitive |
ComputeError: cannot cast |
Type mismatch in operation |
Use .cast(pl.Type) explicitly |
OutOfMemoryError on collect |
Data too large for eager mode |
Use lf.collect(streaming=True) or filter first |
Slow .map_elements() |
Python UDF prevents parallelism |
Rewrite using native expressions (str/dt/list namespaces) |
| Join produces more rows than expected |
Duplicate keys in right DataFrame |
Deduplicate first: df.unique(subset=["key"]) |
InvalidOperationError: join on different types |
Key columns have different dtypes |
Cast both to same type: .cast(pl.Int64) |
.over() returns wrong values |
Forgetting to include all group columns |
Include all grouping columns in .over("col1", "col2") |
| Parquet file unreadable |
Written with incompatible compression |
Specify compression="snappy" for maximum compatibility |
| CSV dates read as strings |
No automatic date parsing in CSV reader |
Parse after reading: pl.col("date").str.to_date("%Y-%m-%d") |
concat fails with different schemas |
Columns don't match across DataFrames |
Use how="diagonal" to fill missing columns with null |
Bundled Resources
references/pandas_migration.md — Pandas-to-Polars migration guide with operation mapping tables (selection, filtering, column ops, aggregation, window functions, joins, reshaping, string ops, datetime ops, missing data, I/O), interoperability code, common migration patterns with side-by-side code, migration pitfalls, and migration checklist.
- Covers: all operation mapping content from original pandas_migration.md
- Relocated inline: key pandas differences summary → SKILL.md Key Concepts "Key Differences from Pandas" section; basic conversion recipe → SKILL.md Common Recipes "Pandas Migration Pattern"
- Omitted: anti-pattern code examples for row iteration and sequential pipe — covered in io_best_practices.md
references/advanced_operations.md — Rolling windows (time-based and row-based), cumulative operations (cum_sum/max/min/prod), shift/lag/lead with grouped contexts, struct operations (create/access/unnest), list column manipulation (stats, eval, filter, explode), unique/duplicate detection, advanced sorting (nulls_last, expression-based, top-N per group), column renaming (dict, suffix/prefix/programmatic), sampling (fixed n, fraction, bootstrap), transpose, and advanced reshaping patterns (wide-long-wide, nested JSON to flat, multi-level unpivot, horizontal concat).
- Covers: advanced operations from original operations.md + transformations from original transformations.md not in main SKILL.md
- Relocated inline: basic selection/filtering → SKILL.md Core API section 1; groupby/aggregation → section 2; basic joins/asof → section 3; basic pivot/unpivot/explode/concat → section 4; string/date/list/conditional basics → section 6; basic window functions → section 2
- Omitted: join performance tips (simple; covered in SKILL.md Best Practices); concatenation options (rechunk covered in Key Parameters table)
references/io_best_practices.md — Full I/O format guide (CSV options, Parquet options with partitioning, JSON/NDJSON, Excel multi-sheet, Arrow IPC), database connectivity (PostgreSQL, MySQL, SQLite, BigQuery), cloud storage (S3, Azure, GCS), in-memory format conversions (dict, NumPy, pandas, Arrow), format selection decision guide, schema management and error handling, expression composition and reuse patterns, column selection patterns (by type, regex, exclude), memory management (estimated_size, type optimization, streaming), pipeline functions for composable transforms, testing/debugging (query plans, schema validation, profiling), performance anti-patterns (sequential pipe, many DataFrames, in-place mutation, unspecified types), and version compatibility notes.
- Covers: all I/O content from original io_guide.md + expression/memory/testing/performance content from original best_practices.md + format selection and version notes from original core_concepts.md
- Relocated inline: basic CSV/Parquet/JSON/Excel/database read/write → SKILL.md Core API section 5; lazy vs eager comparison → SKILL.md Key Concepts table; basic expression context/syntax → SKILL.md section 6; parallelization/type system concepts → SKILL.md Key Concepts + Best Practices; null handling → SKILL.md section 6; categorical recommendation → SKILL.md Best Practices item 4
- Omitted: detailed expression fundamentals (what are expressions, expression contexts) — fully covered in SKILL.md Core API; basic conditional logic examples — covered in SKILL.md section 6; basic aggregation patterns — covered in SKILL.md section 2
Per-Reference-File Disposition (Original 6 files)
| Original File |
Lines |
Disposition |
Target |
operations.md |
603 |
Consolidated |
Advanced ops → references/advanced_operations.md; basic selection/filter/groupby/window/string/date → SKILL.md Core API sections 1-2, 6 |
transformations.md |
550 |
Consolidated |
Reshaping/transpose → references/advanced_operations.md; basic joins/pivot/unpivot/explode/concat → SKILL.md Core API sections 3-4 |
io_guide.md |
558 |
Consolidated |
Full I/O detail → references/io_best_practices.md; basic read/write → SKILL.md Core API section 5 |
best_practices.md |
650 |
Consolidated |
Expression reuse, memory, testing, anti-patterns → references/io_best_practices.md; core best practices → SKILL.md Best Practices |
core_concepts.md |
379 |
Consolidated |
Format selection, version notes → references/io_best_practices.md; data types, lazy/eager, parallelism → SKILL.md Key Concepts |
pandas_migration.md |
418 |
Migrated |
→ references/pandas_migration.md (expanded with window/string/datetime/missing data tables) |
Intentional Omissions
- Row iteration examples (operations.md): Not documented as a positive capability; only referenced as anti-pattern in Best Practices
- Expression fundamentals tutorial (core_concepts.md): Expression syntax, contexts, and expansion are fully covered by SKILL.md Core API sections; a separate tutorial would duplicate
- Detailed parallelization internals (core_concepts.md): "What gets parallelized" list omitted — users only need the Best Practices guidance to stay in the expression API
- Copy-on-write comparison (core_concepts.md): Pandas 2.0+ copy-on-write details omitted — migration-focused, not Polars-centric
Related Skills
- zarr-python — Chunked array storage; Polars can read/write Parquet that Zarr processes
- matplotlib-scientific-plotting — Visualization; convert to pandas with
.to_pandas() for plotting
- scikit-learn-machine-learning — ML pipelines; use
.to_numpy() or .to_pandas() for sklearn input
References
1---2name: polars-dataframes3description: Fast in-memory DataFrame with lazy evaluation, parallel execution, Arrow backend. Use for tabular data in RAM (1–100 GB) when pandas is too slow. Expression API: select, filter, group_by, joins, pivots, window. Lazy mode enables predicate/projection pushdown. Reads CSV, Parquet, JSON, Excel, DBs, cloud. Larger-than-RAM: Dask; GPU: cuDF.4license: MIT5---67# Polars DataFrames89## Overview1011Polars is a high-performance DataFrame library for Python built on Apache Arrow with a Rust backend. It provides an expression-based API with lazy evaluation and automatic parallelization for efficient data processing, transformation, and analysis.1213## When to Use1415- Processing tabular datasets from 100 MB to 100 GB that fit in RAM16- ETL pipelines requiring fast read/transform/write cycles17- Replacing pandas when performance matters (10–100x speedup typical)18- Lazy query pipelines with automatic optimization (predicate/projection pushdown)19- Joining, pivoting, and reshaping large tables20- Reading Parquet, CSV, JSON, or cloud-stored data efficiently21- Window functions and complex grouped aggregations22- For larger-than-RAM data, use **Dask** or **Vaex** instead23- For GPU-accelerated DataFrames, use **cuDF** instead2425## Prerequisites2627```bash28pip install polars29# Optional extras:30pip install polars[all] # All I/O backends31pip install polars[pandas] # Pandas interop32pip install polars[numpy] # NumPy interop33pip install connectorx sqlalchemy # Database connectivity34```3536## Quick Start3738```python39import polars as pl4041# Create DataFrame42df = pl.DataFrame({43 "name": ["Alice", "Bob", "Charlie", "Diana"],44 "dept": ["Sales", "Eng", "Sales", "Eng"],45 "salary": [70000, 85000, 72000, 90000],46})4748# Expression-based pipeline49result = (50 df.filter(pl.col("salary") > 71000)51 .with_columns(bonus=pl.col("salary") * 0.1)52 .group_by("dept")53 .agg(54 pl.col("salary").mean().alias("avg_salary"),55 pl.len().alias("count"),56 )57)58print(result)59# shape: (2, 3)60# ┌───────┬────────────┬───────┐61# │ dept ┆ avg_salary ┆ count │62# ├───────┼────────────┼───────┤63# │ Eng ┆ 87500.0 ┆ 2 │64# │ Sales ┆ 72000.0 ┆ 1 │65# └───────┴────────────┴───────┘66```6768## Core API6970### 1. DataFrame Operations7172Select, filter, add/modify columns, sort, and sample rows.7374```python75import polars as pl7677df = pl.DataFrame({78 "id": [1, 2, 3, 4, 5],79 "name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],80 "age": [25, 30, 35, 28, 32],81 "score": [88.5, 92.0, 76.3, 95.1, 84.7],82})8384# Select columns (with computed expressions)85selected = df.select(86 "name",87 pl.col("age"),88 (pl.col("score") / 100).alias("score_pct"),89)90print(selected.shape) # (5, 3)9192# Filter rows (multiple conditions → implicit AND)93filtered = df.filter(94 pl.col("age") > 27,95 pl.col("score") > 80,96)97print(filtered.shape) # (3, 4) — Bob, Diana, Eve9899# Add columns (preserves existing)100enriched = df.with_columns(101 grade=pl.when(pl.col("score") >= 90).then(pl.lit("A"))102 .when(pl.col("score") >= 80).then(pl.lit("B"))103 .otherwise(pl.lit("C")),104 age_months=pl.col("age") * 12,105)106print(enriched.columns)107# ['id', 'name', 'age', 'score', 'grade', 'age_months']108109# Sort110df.sort("score", descending=True).head(3)111```112113### 2. GroupBy & Aggregations114115Group rows and compute summary statistics.116117```python118import polars as pl119120sales = pl.DataFrame({121 "region": ["East", "West", "East", "West", "East", "West"],122 "product": ["A", "A", "B", "B", "A", "B"],123 "revenue": [100, 150, 200, 180, 120, 210],124 "units": [10, 15, 20, 18, 12, 21],125})126127# Basic group_by128summary = sales.group_by("region").agg(129 pl.col("revenue").sum().alias("total_rev"),130 pl.col("revenue").mean().alias("avg_rev"),131 pl.len().alias("n_transactions"),132)133print(summary)134135# Multiple keys + conditional aggregation136by_rp = sales.group_by("region", "product").agg(137 pl.col("revenue").sum(),138 (pl.col("units") > 15).sum().alias("large_orders"),139)140print(by_rp)141```142143```python144# Window functions with over() — add group stats without collapsing rows145enriched = sales.with_columns(146 region_avg=pl.col("revenue").mean().over("region"),147 rank_in_region=pl.col("revenue").rank(descending=True).over("region"),148 pct_of_region=pl.col("revenue") / pl.col("revenue").sum().over("region"),149)150print(enriched.select("region", "product", "revenue", "region_avg", "rank_in_region"))151```152153### 3. Joins154155Combine DataFrames on shared keys.156157```python158import polars as pl159160customers = pl.DataFrame({161 "cid": [1, 2, 3, 4],162 "name": ["Alice", "Bob", "Charlie", "Diana"],163})164orders = pl.DataFrame({165 "oid": [101, 102, 103, 104],166 "cid": [1, 2, 1, 5],167 "amount": [100, 200, 150, 300],168})169170# Inner join — only matching rows171inner = customers.join(orders, on="cid", how="inner")172print(inner.shape) # (3, 4) — cid 1 (×2), cid 2173174# Left join — all left rows, nulls where no match175left = customers.join(orders, on="cid", how="left")176print(left.shape) # (4, 4) — Charlie and Diana have null amount177178# Anti join — left rows WITHOUT a match in right179no_orders = customers.join(orders, on="cid", how="anti")180print(no_orders["name"].to_list()) # ['Charlie', 'Diana']181182# Join on different column names183customers.join(orders, left_on="cid", right_on="cid", suffix="_order")184```185186```python187# Asof join — match to nearest timestamp (time-series alignment)188quotes = pl.DataFrame({189 "time": [1.0, 2.0, 3.0, 4.0],190 "price": [100, 101, 102, 103],191}).cast({"time": pl.Float64})192193trades = pl.DataFrame({194 "time": [1.5, 3.2],195 "qty": [50, 75],196}).cast({"time": pl.Float64})197198result = trades.join_asof(quotes, on="time", strategy="backward")199print(result)200# time=1.5 matched price=100, time=3.2 matched price=102201```202203### 4. Reshaping204205Pivot, unpivot, explode, and transpose operations.206207```python208import polars as pl209210# --- Pivot (long → wide) ---211long = pl.DataFrame({212 "date": ["Jan", "Jan", "Feb", "Feb"],213 "product": ["A", "B", "A", "B"],214 "sales": [100, 150, 120, 160],215})216wide = long.pivot(values="sales", index="date", columns="product")217print(wide)218# date | A | B219# Jan | 100 | 150220# Feb | 120 | 160221222# --- Unpivot (wide → long) ---223back_to_long = wide.unpivot(224 index="date", on=["A", "B"],225 variable_name="product", value_name="sales",226)227print(back_to_long.shape) # (4, 3)228229# --- Explode list columns ---230nested = pl.DataFrame({231 "id": [1, 2],232 "tags": [["a", "b", "c"], ["d", "e"]],233})234flat = nested.explode("tags")235print(flat.shape) # (5, 2)236```237238### 5. Data I/O239240Read and write CSV, Parquet, JSON, Excel, databases, and cloud storage.241242```python243import polars as pl244245# --- CSV ---246df = pl.read_csv("data.csv")247df.write_csv("output.csv")248249# --- Parquet (recommended for performance) ---250df = pl.read_parquet("data.parquet")251df.write_parquet("output.parquet", compression="zstd")252253# --- JSON / NDJSON ---254df = pl.read_ndjson("data.ndjson")255df.write_ndjson("output.ndjson")256257# --- Excel ---258df = pl.read_excel("data.xlsx", sheet_name="Sheet1")259df.write_excel("output.xlsx")260261# --- Lazy scan (preferred for large files) ---262lf = pl.scan_csv("large.csv")263result = lf.filter(pl.col("value") > 0).select("id", "value").collect()264print(result.shape)265```266267```python268# --- Database ---269df = pl.read_database_uri(270 "SELECT * FROM users WHERE age > 25",271 uri="postgresql://user:pass@localhost/db",272)273274# --- Cloud storage (S3, GCS, Azure) ---275df = pl.read_parquet("s3://bucket/data.parquet")276df = pl.scan_parquet("gs://bucket/data/*.parquet").collect()277278# --- Partitioned Parquet (Hive-style) ---279df.write_parquet("output_dir", partition_by=["year", "month"])280lf = pl.scan_parquet("output_dir/**/*.parquet")281```282283### 6. Expression API284285String, datetime, list, and conditional operations.286287```python288import polars as pl289from datetime import date290291df = pl.DataFrame({292 "text": ["Hello World", "foo bar", "POLARS"],293 "dt": [date(2023, 1, 15), date(2023, 6, 30), date(2024, 12, 1)],294 "values": [[1, 2, 3], [4, 5], [6]],295})296297# String operations298strings = df.select(299 lower=pl.col("text").str.to_lowercase(),300 length=pl.col("text").str.len_chars(),301 contains_o=pl.col("text").str.contains("o"),302 split=pl.col("text").str.split(" "),303)304print(strings)305306# Datetime operations307dates = df.select(308 year=pl.col("dt").dt.year(),309 month=pl.col("dt").dt.month(),310 weekday=pl.col("dt").dt.weekday(),311 quarter=pl.col("dt").dt.quarter(),312)313print(dates)314315# List operations316lists = df.select(317 list_len=pl.col("values").list.len(),318 list_sum=pl.col("values").list.sum(),319 first=pl.col("values").list.first(),320)321print(lists)322```323324```python325# Conditional expressions (when/then/otherwise)326df = pl.DataFrame({"score": [45, 72, 88, 95, 60]})327result = df.with_columns(328 grade=pl.when(pl.col("score") >= 90).then(pl.lit("A"))329 .when(pl.col("score") >= 80).then(pl.lit("B"))330 .when(pl.col("score") >= 70).then(pl.lit("C"))331 .otherwise(pl.lit("F")),332)333print(result)334335# Null handling336df2 = pl.DataFrame({"x": [1, None, 3, None, 5]})337filled = df2.with_columns(338 filled=pl.col("x").fill_null(0),339 forward=pl.col("x").fill_null(strategy="forward"),340 is_null=pl.col("x").is_null(),341)342print(filled)343344# Multi-column operations with regex selector345df3 = pl.DataFrame({"val_a": [1, 2], "val_b": [3, 4], "name": ["x", "y"]})346doubled = df3.select(pl.col("^val_.*$") * 2)347print(doubled)348```349350### 7. Lazy Evaluation351352Build optimized query plans before execution.353354```python355import polars as pl356357# Lazy mode: build plan, optimize, then execute358lf = pl.scan_csv("large_dataset.csv")359360result = (361 lf362 .select("user_id", "category", "amount", "date") # projection pushdown363 .filter(pl.col("amount") > 100) # predicate pushdown364 .with_columns(pl.col("date").str.to_date())365 .group_by("category")366 .agg(367 pl.col("amount").sum().alias("total"),368 pl.col("user_id").n_unique().alias("unique_users"),369 )370 .sort("total", descending=True)371)372373# Inspect the optimized plan374print(result.explain())375376# Execute377df = result.collect()378print(df)379```380381```python382# Streaming mode for very large data383lf = pl.scan_parquet("data/*.parquet")384result = (385 lf386 .filter(pl.col("year") >= 2023)387 .group_by("region")388 .agg(pl.col("sales").sum())389 .collect(streaming=True) # processes in batches390)391print(result)392393# Sink directly to file (no full materialization)394lf.filter(pl.col("active")).sink_parquet("filtered_output.parquet")395```396397## Key Concepts398399### Lazy vs Eager Comparison400401| Aspect | Eager (`DataFrame`) | Lazy (`LazyFrame`) |402|--------|--------------------|--------------------|403| Created by | `pl.read_*()`, `pl.DataFrame()` | `pl.scan_*()`, `df.lazy()` |404| Execution | Immediate | On `.collect()` |405| Optimization | None | Predicate/projection pushdown, join reordering |406| Streaming | No | `collect(streaming=True)` |407| Best for | Small data, interactive | Large data, pipelines |408409### Polars Data Types410411| Type | Python equivalent | Notes |412|------|-------------------|-------|413| `Int8/16/32/64` | `int` | Choose smallest sufficient size |414| `UInt8/16/32/64` | `int` | Unsigned |415| `Float32/64` | `float` | Float64 default |416| `Boolean` | `bool` | |417| `Utf8` | `str` | String type |418| `Categorical` | — | Low-cardinality strings (faster groupby) |419| `Date` | `datetime.date` | Date without time |420| `Datetime` | `datetime.datetime` | With microsecond precision |421| `Duration` | `datetime.timedelta` | Time difference |422| `List` | `list` | Variable-length lists |423| `Struct` | `dict` | Named fields |424| `Null` | `None` | All-null column |425426### Key Differences from Pandas427428- **No index**: Row access by position only; no `.loc`/`.iloc` with labels429- **Strict typing**: No silent type coercion; explicit `.cast()` required430- **Expressions, not methods**: `pl.col("x").mean()` instead of `df["x"].mean()`431- **Parallel by default**: All column operations run in parallel432- **Lazy evaluation**: Available via `LazyFrame` for query optimization433434## Common Workflows435436### 1. ETL Pipeline (CSV → Clean → Parquet)437438```python439import polars as pl440441# Extract442lf = pl.scan_csv(443 "raw_data.csv",444 dtypes={"id": pl.Int64, "date": pl.Utf8, "amount": pl.Float64},445)446447# Transform448cleaned = (449 lf450 .with_columns(pl.col("date").str.to_date("%Y-%m-%d"))451 .filter(pl.col("amount").is_not_null())452 .with_columns(453 year=pl.col("date").dt.year(),454 month=pl.col("date").dt.month(),455 amount_log=pl.col("amount").log(),456 )457 .drop_nulls()458)459460# Load461cleaned.collect().write_parquet("clean_data.parquet", compression="zstd")462print("ETL complete")463```464465### 2. Multi-Source Join and Aggregation466467```python468import polars as pl469470# Simulate three data sources471users = pl.DataFrame({472 "uid": [1, 2, 3, 4],473 "name": ["Alice", "Bob", "Charlie", "Diana"],474 "region": ["East", "West", "East", "West"],475})476orders = pl.DataFrame({477 "oid": range(1, 7),478 "uid": [1, 1, 2, 3, 3, 3],479 "amount": [100, 200, 150, 50, 75, 125],480})481products = pl.DataFrame({482 "oid": range(1, 7),483 "category": ["Elec", "Books", "Elec", "Books", "Elec", "Elec"],484})485486# Join → aggregate487result = (488 orders489 .join(users, on="uid", how="left")490 .join(products, on="oid", how="left")491 .group_by("region", "category")492 .agg(493 pl.col("amount").sum().alias("total"),494 pl.col("amount").mean().alias("avg_order"),495 pl.len().alias("n_orders"),496 )497 .sort("total", descending=True)498)499print(result)500```501502### 3. Time-Series Feature Engineering503504Uses: GroupBy, Window functions, Joins, Expression API.5055061. Load time-series data with `pl.scan_csv()` or `pl.scan_parquet()`5072. Parse dates: `.with_columns(pl.col("date").str.to_date())`5083. Sort by entity and date: `.sort("entity_id", "date")`5094. Add lag features: `pl.col("value").shift(n).over("entity_id")`5105. Add rolling statistics: `pl.col("value").rolling_mean(window_size=7).over("entity_id")`5116. Compute percent change: `(pl.col("value") - pl.col("value").shift(1)) / pl.col("value").shift(1)`5127. Collect and write: `.collect().write_parquet("features.parquet")`513514## Key Parameters515516| Parameter | Function | Default | Range/Options | Effect |517|-----------|----------|---------|---------------|--------|518| `how` | `.join()` | `"inner"` | inner, left, outer, cross, semi, anti | Join type |519| `strategy` | `.join_asof()` | `"backward"` | backward, forward, nearest | Asof match direction |520| `streaming` | `.collect()` | `False` | True/False | Process in batches for large data |521| `compression` | `.write_parquet()` | `"zstd"` | snappy, gzip, brotli, lz4, zstd, uncompressed | Parquet compression |522| `partition_by` | `.write_parquet()` | None | List of columns | Hive-style partitioning |523| `rechunk` | `pl.concat()` | `False` | True/False | Rechunk memory after concat |524| `aggregate_function` | `.pivot()` | `"first"` | first, sum, mean, max, min, count | Duplicate handling in pivot |525| `n_rows` | `pl.read_csv()` | None | Positive int | Limit rows read (for sampling) |526| `parallel` | `pl.read_csv()` | `"auto"` | auto, columns, row_groups, none | Parallel reading strategy |527| `dtypes` | `pl.read_csv()` | None | Dict of column→type | Override type inference |528529## Best Practices5305311. **Use lazy mode for large datasets**: `pl.scan_csv()` not `pl.read_csv()`. Enables query optimization and streaming.5325332. **Stay in the expression API**: Avoid `.map_elements()` (runs Python, no parallelism). Prefer native Polars operations — string, datetime, list namespaces cover most needs.5345353. **Select early, filter early**: Place `.select()` and `.filter()` as early as possible in lazy pipelines. The optimizer can push these down but explicit placement helps.5365374. **Use Categorical for low-cardinality strings**: `df.with_columns(pl.col("region").cast(pl.Categorical))` — dramatically speeds up groupby and joins on repeated string values.5385395. **Prefer Parquet over CSV**: Parquet preserves types, supports predicate pushdown, and is 5–10x smaller. Use `compression="zstd"` for best compression/speed balance.5405416. **Anti-pattern — Python loops over rows**: Never iterate rows with `for row in df.iter_rows()` for computation. Use expressions instead.5425437. **Anti-pattern — chaining `.with_columns()` calls**: Combine multiple column additions into a single `.with_columns()` call for parallel execution.544545## Common Recipes546547### Recipe: Pandas Migration Pattern548549```python550import polars as pl551import pandas as pd552553# Convert pandas → polars554pd_df = pd.DataFrame({"col": [1, 2, 3], "group": ["a", "b", "a"]})555pl_df = pl.from_pandas(pd_df)556557# Key operation mapping:558# pandas: df["col"] → polars: df.select("col")559# pandas: df[df["col"] > 1] → polars: df.filter(pl.col("col") > 1)560# pandas: df.assign(x=...) → polars: df.with_columns(x=...)561# pandas: df.groupby().agg() → polars: df.group_by().agg()562# pandas: df.groupby().transform → polars: pl.col(...).over(...)563# pandas: df.merge() → polars: df.join()564# pandas: df.melt() → polars: df.unpivot()565566# Convert back567pd_result = pl_df.to_pandas()568```569570### Recipe: Complex Aggregation Report571572```python573import polars as pl574575df = pl.DataFrame({576 "dept": ["Sales", "Eng", "Sales", "Eng", "Sales", "Eng"],577 "level": ["Jr", "Sr", "Sr", "Jr", "Jr", "Sr"],578 "salary": [50000, 95000, 75000, 70000, 55000, 100000],579})580581report = (582 df.group_by("dept", "level")583 .agg(584 pl.col("salary").mean().alias("avg_sal"),585 pl.col("salary").median().alias("med_sal"),586 pl.col("salary").std().alias("std_sal"),587 pl.len().alias("count"),588 )589 .pivot(values="avg_sal", index="dept", columns="level")590 .with_columns(591 diff=pl.col("Sr") - pl.col("Jr"),592 )593)594print(report)595```596597### Recipe: Reading Multiple Files with Schema Alignment598599```python600import polars as pl601from pathlib import Path602603# Read multiple CSVs with potentially different columns604files = sorted(Path("data/").glob("*.csv"))605dfs = [pl.read_csv(f) for f in files]606607# Diagonal concat handles mismatched schemas (fills nulls)608combined = pl.concat(dfs, how="diagonal")609print(f"Combined: {combined.shape}")610print(f"Columns: {combined.columns}")611612# Or use lazy scan for Parquet (automatic parallel)613lf = pl.scan_parquet("data/**/*.parquet")614result = lf.filter(pl.col("date") > "2023-01-01").collect()615```616617## Troubleshooting618619| Problem | Cause | Solution |620|---------|-------|---------|621| `SchemaError: column not found` | Column name typo or case mismatch | Check `df.columns`; Polars is case-sensitive |622| `ComputeError: cannot cast` | Type mismatch in operation | Use `.cast(pl.Type)` explicitly |623| `OutOfMemoryError` on collect | Data too large for eager mode | Use `lf.collect(streaming=True)` or filter first |624| Slow `.map_elements()` | Python UDF prevents parallelism | Rewrite using native expressions (str/dt/list namespaces) |625| Join produces more rows than expected | Duplicate keys in right DataFrame | Deduplicate first: `df.unique(subset=["key"])` |626| `InvalidOperationError: join on different types` | Key columns have different dtypes | Cast both to same type: `.cast(pl.Int64)` |627| `.over()` returns wrong values | Forgetting to include all group columns | Include all grouping columns in `.over("col1", "col2")` |628| Parquet file unreadable | Written with incompatible compression | Specify `compression="snappy"` for maximum compatibility |629| CSV dates read as strings | No automatic date parsing in CSV reader | Parse after reading: `pl.col("date").str.to_date("%Y-%m-%d")` |630| `concat` fails with different schemas | Columns don't match across DataFrames | Use `how="diagonal"` to fill missing columns with null |631632## Bundled Resources633634- **`references/pandas_migration.md`** — Pandas-to-Polars migration guide with operation mapping tables (selection, filtering, column ops, aggregation, window functions, joins, reshaping, string ops, datetime ops, missing data, I/O), interoperability code, common migration patterns with side-by-side code, migration pitfalls, and migration checklist.635 - Covers: all operation mapping content from original pandas_migration.md636 - Relocated inline: key pandas differences summary → SKILL.md Key Concepts "Key Differences from Pandas" section; basic conversion recipe → SKILL.md Common Recipes "Pandas Migration Pattern"637 - Omitted: anti-pattern code examples for row iteration and sequential pipe — covered in io_best_practices.md638639- **`references/advanced_operations.md`** — Rolling windows (time-based and row-based), cumulative operations (cum_sum/max/min/prod), shift/lag/lead with grouped contexts, struct operations (create/access/unnest), list column manipulation (stats, eval, filter, explode), unique/duplicate detection, advanced sorting (nulls_last, expression-based, top-N per group), column renaming (dict, suffix/prefix/programmatic), sampling (fixed n, fraction, bootstrap), transpose, and advanced reshaping patterns (wide-long-wide, nested JSON to flat, multi-level unpivot, horizontal concat).640 - Covers: advanced operations from original operations.md + transformations from original transformations.md not in main SKILL.md641 - Relocated inline: basic selection/filtering → SKILL.md Core API section 1; groupby/aggregation → section 2; basic joins/asof → section 3; basic pivot/unpivot/explode/concat → section 4; string/date/list/conditional basics → section 6; basic window functions → section 2642 - Omitted: join performance tips (simple; covered in SKILL.md Best Practices); concatenation options (rechunk covered in Key Parameters table)643644- **`references/io_best_practices.md`** — Full I/O format guide (CSV options, Parquet options with partitioning, JSON/NDJSON, Excel multi-sheet, Arrow IPC), database connectivity (PostgreSQL, MySQL, SQLite, BigQuery), cloud storage (S3, Azure, GCS), in-memory format conversions (dict, NumPy, pandas, Arrow), format selection decision guide, schema management and error handling, expression composition and reuse patterns, column selection patterns (by type, regex, exclude), memory management (estimated_size, type optimization, streaming), pipeline functions for composable transforms, testing/debugging (query plans, schema validation, profiling), performance anti-patterns (sequential pipe, many DataFrames, in-place mutation, unspecified types), and version compatibility notes.645 - Covers: all I/O content from original io_guide.md + expression/memory/testing/performance content from original best_practices.md + format selection and version notes from original core_concepts.md646 - Relocated inline: basic CSV/Parquet/JSON/Excel/database read/write → SKILL.md Core API section 5; lazy vs eager comparison → SKILL.md Key Concepts table; basic expression context/syntax → SKILL.md section 6; parallelization/type system concepts → SKILL.md Key Concepts + Best Practices; null handling → SKILL.md section 6; categorical recommendation → SKILL.md Best Practices item 4647 - Omitted: detailed expression fundamentals (what are expressions, expression contexts) — fully covered in SKILL.md Core API; basic conditional logic examples — covered in SKILL.md section 6; basic aggregation patterns — covered in SKILL.md section 2648649### Per-Reference-File Disposition (Original 6 files)650651| Original File | Lines | Disposition | Target |652|---------------|-------|-------------|--------|653| `operations.md` | 603 | Consolidated | Advanced ops → `references/advanced_operations.md`; basic selection/filter/groupby/window/string/date → SKILL.md Core API sections 1-2, 6 |654| `transformations.md` | 550 | Consolidated | Reshaping/transpose → `references/advanced_operations.md`; basic joins/pivot/unpivot/explode/concat → SKILL.md Core API sections 3-4 |655| `io_guide.md` | 558 | Consolidated | Full I/O detail → `references/io_best_practices.md`; basic read/write → SKILL.md Core API section 5 |656| `best_practices.md` | 650 | Consolidated | Expression reuse, memory, testing, anti-patterns → `references/io_best_practices.md`; core best practices → SKILL.md Best Practices |657| `core_concepts.md` | 379 | Consolidated | Format selection, version notes → `references/io_best_practices.md`; data types, lazy/eager, parallelism → SKILL.md Key Concepts |658| `pandas_migration.md` | 418 | Migrated | → `references/pandas_migration.md` (expanded with window/string/datetime/missing data tables) |659660### Intentional Omissions661662- **Row iteration examples** (operations.md): Not documented as a positive capability; only referenced as anti-pattern in Best Practices663- **Expression fundamentals tutorial** (core_concepts.md): Expression syntax, contexts, and expansion are fully covered by SKILL.md Core API sections; a separate tutorial would duplicate664- **Detailed parallelization internals** (core_concepts.md): "What gets parallelized" list omitted — users only need the Best Practices guidance to stay in the expression API665- **Copy-on-write comparison** (core_concepts.md): Pandas 2.0+ copy-on-write details omitted — migration-focused, not Polars-centric666667## Related Skills668669- **zarr-python** — Chunked array storage; Polars can read/write Parquet that Zarr processes670- **matplotlib-scientific-plotting** — Visualization; convert to pandas with `.to_pandas()` for plotting671- **scikit-learn-machine-learning** — ML pipelines; use `.to_numpy()` or `.to_pandas()` for sklearn input672673## References674675- Polars User Guide: https://docs.pola.rs/676- Polars API Reference: https://docs.pola.rs/api/python/stable/reference/677- GitHub: https://github.com/pola-rs/polars678- Polars Cookbook: https://docs.pola.rs/user-guide/