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---6
7# Polars DataFrames
8
9## Overview
10
11Polars 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.
12
13## When to Use
14
15- Processing tabular datasets from 100 MB to 100 GB that fit in RAM
16- ETL pipelines requiring fast read/transform/write cycles
17- 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 tables
20- Reading Parquet, CSV, JSON, or cloud-stored data efficiently
21- Window functions and complex grouped aggregations
22- For larger-than-RAM data, use **Dask** or **Vaex** instead
23- For GPU-accelerated DataFrames, use **cuDF** instead
24
25## Prerequisites
26
27```bash
28pip install polars
29# Optional extras:
30pip install polars[all] # All I/O backends
31pip install polars[pandas] # Pandas interop
32pip install polars[numpy] # NumPy interop
33pip install connectorx sqlalchemy # Database connectivity
34```
35
36## Quick Start
37
38```python
39import polars as pl
40
41# Create DataFrame
42df = pl.DataFrame({
43 "name": ["Alice", "Bob", "Charlie", "Diana"],
44 "dept": ["Sales", "Eng", "Sales", "Eng"],
45 "salary": [70000, 85000, 72000, 90000],
46})
47
48# Expression-based pipeline
49result = (
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```
67
68## Core API
69
70### 1. DataFrame Operations
71
72Select, filter, add/modify columns, sort, and sample rows.
73
74```python
75import polars as pl
76
77df = 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})
83
84# 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)
91
92# 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, Eve
98
99# 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']
108
109# Sort
110df.sort("score", descending=True).head(3)
111```
112
113### 2. GroupBy & Aggregations
114
115Group rows and compute summary statistics.
116
117```python
118import polars as pl
119
120sales = 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})
126
127# Basic group_by
128summary = 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)
134
135# Multiple keys + conditional aggregation
136by_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```
142
143```python
144# Window functions with over() — add group stats without collapsing rows
145enriched = 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```
152
153### 3. Joins
154
155Combine DataFrames on shared keys.
156
157```python
158import polars as pl
159
160customers = 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})
169
170# Inner join — only matching rows
171inner = customers.join(orders, on="cid", how="inner")
172print(inner.shape) # (3, 4) — cid 1 (×2), cid 2
173
174# Left join — all left rows, nulls where no match
175left = customers.join(orders, on="cid", how="left")
176print(left.shape) # (4, 4) — Charlie and Diana have null amount
177
178# Anti join — left rows WITHOUT a match in right
179no_orders = customers.join(orders, on="cid", how="anti")
180print(no_orders["name"].to_list()) # ['Charlie', 'Diana']
181
182# Join on different column names
183customers.join(orders, left_on="cid", right_on="cid", suffix="_order")
184```
185
186```python
187# 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})
192
193trades = pl.DataFrame({
194 "time": [1.5, 3.2],
195 "qty": [50, 75],
196}).cast({"time": pl.Float64})
197
198result = trades.join_asof(quotes, on="time", strategy="backward")
199print(result)
200# time=1.5 matched price=100, time=3.2 matched price=102
201```
202
203### 4. Reshaping
204
205Pivot, unpivot, explode, and transpose operations.
206
207```python
208import polars as pl
209
210# --- 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 | B
219# Jan | 100 | 150
220# Feb | 120 | 160
221
222# --- 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)
228
229# --- 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```
237
238### 5. Data I/O
239
240Read and write CSV, Parquet, JSON, Excel, databases, and cloud storage.
241
242```python
243import polars as pl
244
245# --- CSV ---
246df = pl.read_csv("data.csv")
247df.write_csv("output.csv")
248
249# --- Parquet (recommended for performance) ---
250df = pl.read_parquet("data.parquet")
251df.write_parquet("output.parquet", compression="zstd")
252
253# --- JSON / NDJSON ---
254df = pl.read_ndjson("data.ndjson")
255df.write_ndjson("output.ndjson")
256
257# --- Excel ---
258df = pl.read_excel("data.xlsx", sheet_name="Sheet1")
259df.write_excel("output.xlsx")
260
261# --- 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```
266
267```python
268# --- Database ---
269df = pl.read_database_uri(
270 "SELECT * FROM users WHERE age > 25",
271 uri="postgresql://user:pass@localhost/db",
272)
273
274# --- Cloud storage (S3, GCS, Azure) ---
275df = pl.read_parquet("s3://bucket/data.parquet")
276df = pl.scan_parquet("gs://bucket/data/*.parquet").collect()
277
278# --- Partitioned Parquet (Hive-style) ---
279df.write_parquet("output_dir", partition_by=["year", "month"])
280lf = pl.scan_parquet("output_dir/**/*.parquet")
281```
282
283### 6. Expression API
284
285String, datetime, list, and conditional operations.
286
287```python
288import polars as pl
289from datetime import date
290
291df = 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})
296
297# String operations
298strings = 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)
305
306# Datetime operations
307dates = 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)
314
315# List operations
316lists = 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```
323
324```python
325# 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)
334
335# Null handling
336df2 = 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)
343
344# Multi-column operations with regex selector
345df3 = pl.DataFrame({"val_a": [1, 2], "val_b": [3, 4], "name": ["x", "y"]})
346doubled = df3.select(pl.col("^val_.*$") * 2)
347print(doubled)
348```
349
350### 7. Lazy Evaluation
351
352Build optimized query plans before execution.
353
354```python
355import polars as pl
356
357# Lazy mode: build plan, optimize, then execute
358lf = pl.scan_csv("large_dataset.csv")
359
360result = (
361 lf
362 .select("user_id", "category", "amount", "date") # projection pushdown
363 .filter(pl.col("amount") > 100) # predicate pushdown
364 .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)
372
373# Inspect the optimized plan
374print(result.explain())
375
376# Execute
377df = result.collect()
378print(df)
379```
380
381```python
382# Streaming mode for very large data
383lf = pl.scan_parquet("data/*.parquet")
384result = (
385 lf
386 .filter(pl.col("year") >= 2023)
387 .group_by("region")
388 .agg(pl.col("sales").sum())
389 .collect(streaming=True) # processes in batches
390)
391print(result)
392
393# Sink directly to file (no full materialization)
394lf.filter(pl.col("active")).sink_parquet("filtered_output.parquet")
395```
396
397## Key Concepts
398
399### Lazy vs Eager Comparison
400
401| 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 |
408
409### Polars Data Types
410
411| 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 |
425
426### Key Differences from Pandas
427
428- **No index**: Row access by position only; no `.loc`/`.iloc` with labels
429- **Strict typing**: No silent type coercion; explicit `.cast()` required
430- **Expressions, not methods**: `pl.col("x").mean()` instead of `df["x"].mean()`
431- **Parallel by default**: All column operations run in parallel
432- **Lazy evaluation**: Available via `LazyFrame` for query optimization
433
434## Common Workflows
435
436### 1. ETL Pipeline (CSV → Clean → Parquet)
437
438```python
439import polars as pl
440
441# Extract
442lf = pl.scan_csv(
443 "raw_data.csv",
444 dtypes={"id": pl.Int64, "date": pl.Utf8, "amount": pl.Float64},
445)
446
447# Transform
448cleaned = (
449 lf
450 .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)
459
460# Load
461cleaned.collect().write_parquet("clean_data.parquet", compression="zstd")
462print("ETL complete")
463```
464
465### 2. Multi-Source Join and Aggregation
466
467```python
468import polars as pl
469
470# Simulate three data sources
471users = 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})
485
486# Join → aggregate
487result = (
488 orders
489 .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```
501
502### 3. Time-Series Feature Engineering
503
504Uses: GroupBy, Window functions, Joins, Expression API.
505
5061. 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")`
513
514## Key Parameters
515
516| 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 |
528
529## Best Practices
530
5311. **Use lazy mode for large datasets**: `pl.scan_csv()` not `pl.read_csv()`. Enables query optimization and streaming.
532
5332. **Stay in the expression API**: Avoid `.map_elements()` (runs Python, no parallelism). Prefer native Polars operations — string, datetime, list namespaces cover most needs.
534
5353. **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.
536
5374. **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.
538
5395. **Prefer Parquet over CSV**: Parquet preserves types, supports predicate pushdown, and is 5–10x smaller. Use `compression="zstd"` for best compression/speed balance.
540
5416. **Anti-pattern — Python loops over rows**: Never iterate rows with `for row in df.iter_rows()` for computation. Use expressions instead.
542
5437. **Anti-pattern — chaining `.with_columns()` calls**: Combine multiple column additions into a single `.with_columns()` call for parallel execution.
544
545## Common Recipes
546
547### Recipe: Pandas Migration Pattern
548
549```python
550import polars as pl
551import pandas as pd
552
553# Convert pandas → polars
554pd_df = pd.DataFrame({"col": [1, 2, 3], "group": ["a", "b", "a"]})
555pl_df = pl.from_pandas(pd_df)
556
557# 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()
565
566# Convert back
567pd_result = pl_df.to_pandas()
568```
569
570### Recipe: Complex Aggregation Report
571
572```python
573import polars as pl
574
575df = 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})
580
581report = (
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```
596
597### Recipe: Reading Multiple Files with Schema Alignment
598
599```python
600import polars as pl
601from pathlib import Path
602
603# Read multiple CSVs with potentially different columns
604files = sorted(Path("data/").glob("*.csv"))
605dfs = [pl.read_csv(f) for f in files]
606
607# Diagonal concat handles mismatched schemas (fills nulls)
608combined = pl.concat(dfs, how="diagonal")
609print(f"Combined: {combined.shape}")
610print(f"Columns: {combined.columns}")
611
612# 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```
616
617## Troubleshooting
618
619| 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 |
631
632## Bundled Resources
633
634- **`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.md
636 - 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.md
638
639- **`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.md
641 - 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
642 - Omitted: join performance tips (simple; covered in SKILL.md Best Practices); concatenation options (rechunk covered in Key Parameters table)
643
644- **`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.md
646 - 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
647 - 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
648
649### Per-Reference-File Disposition (Original 6 files)
650
651| 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) |
659
660### Intentional Omissions
661
662- **Row iteration examples** (operations.md): Not documented as a positive capability; only referenced as anti-pattern in Best Practices
663- **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
664- **Detailed parallelization internals** (core_concepts.md): "What gets parallelized" list omitted — users only need the Best Practices guidance to stay in the expression API
665- **Copy-on-write comparison** (core_concepts.md): Pandas 2.0+ copy-on-write details omitted — migration-focused, not Polars-centric
666
667## Related Skills
668
669- **zarr-python** — Chunked array storage; Polars can read/write Parquet that Zarr processes
670- **matplotlib-scientific-plotting** — Visualization; convert to pandas with `.to_pandas()` for plotting
671- **scikit-learn-machine-learning** — ML pipelines; use `.to_numpy()` or `.to_pandas()` for sklearn input
672
673## References
674
675- 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/polars
678- Polars Cookbook: https://docs.pola.rs/user-guide/