Claude Skill

alterlab-polars

Fast in-memory DataFrame analytics with Polars — lazy evaluation, parallel execution, and an Apache Arrow backend for datasets that fit in RAM. Use when pandas is too slow but data still fits in memory, for 1-100GB datasets, ETL pipelines, or a faster pandas replacement. For larg

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_data-science_alterlab-polars-e4836c0.zip · 30 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/data-science/alterlab-polars
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Polars

Overview

Polars is a lightning-fast DataFrame library for Python and Rust built on Apache Arrow. Work with Polars' expression-based API, lazy evaluation framework, and high-performance data manipulation capabilities for efficient data processing, pandas migration, and data pipeline optimization.

When to Use This Skill

  • pandas code is too slow but the data (or the columns a lazy query touches) fits on one machine
  • Building ETL or feature pipelines with lazy scans (scan_csv, scan_parquet) and query optimization
  • Migrating pandas code to the Polars expression API
  • Window functions, joins, and group-by aggregations over large in-memory tables

Does NOT Trigger

Scenario Use Instead
Distributed execution across a cluster, or scaling existing pandas/NumPy code with a task scheduler and dashboard alterlab-dask
Billion-row out-of-core exploration with memory-mapped HDF5/Arrow files and big-data plots alterlab-vaex
First look at an unfamiliar data file: structure, missingness, and quality report alterlab-eda

Quick Start

Installation and Basic Usage

Pin a recent 1.x (examples here use the Polars 1.x API):

uv add 'polars>=1.0,<2'

Polars 2.0 is in release candidate (2.0.0rc2, September 2026). Its upgrade guide lists changes that alter results silently: LazyFrame.collect() defaults to the streaming engine (row order of some joins/unpivots can change), pl.concat(how="horizontal") requires equal heights (how="horizontal_extend" pads), explode() drops empty lists instead of emitting a null row, is_in refuses lossy casts, LazyFrame.profile() is removed, and headerless CSV columns are named from column_0. Check https://docs.pola.rs/releases/upgrade/ before moving to 2.x.

Basic DataFrame creation and operations:

import polars as pl

# Create DataFrame
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "city": ["NY", "LA", "SF"]
})

# Select columns
df.select("name", "age")

# Filter rows
df.filter(pl.col("age") > 25)

# Add computed columns
df.with_columns(
    age_plus_10=pl.col("age") + 10
)

Core Concepts

Expressions

Expressions are the fundamental building blocks of Polars operations. They describe transformations on data and can be composed, reused, and optimized.

Key principles:

  • Use pl.col("column_name") to reference columns
  • Chain methods to build complex transformations
  • Expressions are lazy and only execute within contexts (select, with_columns, filter, group_by)

Example:

# Expression-based computation
df.select(
    pl.col("name"),
    (pl.col("age") * 12).alias("age_in_months")
)

Lazy vs Eager Evaluation

Eager (DataFrame): Operations execute immediately

df = pl.read_csv("file.csv")  # Reads immediately
result = df.filter(pl.col("age") > 25)  # Executes immediately

Lazy (LazyFrame): Operations build a query plan, optimized before execution

lf = pl.scan_csv("file.csv")  # Doesn't read yet
result = lf.filter(pl.col("age") > 25).select("name", "age")
df = result.collect()  # Now executes optimized query

When to use lazy:

  • Working with large datasets
  • Complex query pipelines
  • When only some columns/rows are needed
  • Performance is critical

Benefits of lazy evaluation:

  • Automatic query optimization
  • Predicate pushdown
  • Projection pushdown
  • Parallel execution

For detailed concepts, load references/core_concepts.md.

Common Operations

Select

Select and manipulate columns:

# Select specific columns
df.select("name", "age")

# Select with expressions
df.select(
    pl.col("name"),
    (pl.col("age") * 2).alias("double_age")
)

# Select all columns matching a pattern
df.select(pl.col("^.*_id$"))

Filter

Filter rows by conditions:

# Single condition
df.filter(pl.col("age") > 25)

# Multiple conditions (cleaner than using &)
df.filter(
    pl.col("age") > 25,
    pl.col("city") == "NY"
)

# Complex conditions
df.filter(
    (pl.col("age") > 25) | (pl.col("city") == "LA")
)

With Columns

Add or modify columns while preserving existing ones:

# Add new columns
df.with_columns(
    age_plus_10=pl.col("age") + 10,
    name_upper=pl.col("name").str.to_uppercase()
)

# Parallel computation (all columns computed in parallel)
df.with_columns(
    pl.col("value") * 10,
    pl.col("value") * 100,
)

Group By and Aggregations

Group data and compute aggregations:

# Basic grouping
df.group_by("city").agg(
    pl.col("age").mean().alias("avg_age"),
    pl.len().alias("count")
)

# Multiple group keys
df.group_by("city", "department").agg(
    pl.col("salary").sum()
)

# Conditional aggregations
df.group_by("city").agg(
    (pl.col("age") > 30).sum().alias("over_30")
)

For detailed operation patterns, load references/operations.md.

Aggregations and Window Functions

Aggregation Functions

Common aggregations within group_by context:

  • pl.len() - count rows
  • pl.col("x").sum() - sum values
  • pl.col("x").mean() - average
  • pl.col("x").min() / pl.col("x").max() - extremes
  • pl.first() / pl.last() - first/last values

Window Functions with over()

Apply aggregations while preserving row count:

# Add group statistics to each row
df.with_columns(
    avg_age_by_city=pl.col("age").mean().over("city"),
    rank_in_city=pl.col("salary").rank().over("city")
)

# Multiple grouping columns
df.with_columns(
    group_avg=pl.col("value").mean().over("category", "region")
)

Mapping strategies:

  • group_to_rows (default): Preserves original row order
  • explode: Changes the row count (use in select, not with_columns); output is grouped
  • join: Creates list columns

Data I/O

Supported Formats

Polars supports reading and writing:

  • CSV, Parquet, JSON, Excel
  • Databases (via connectors)
  • Cloud storage (S3, Azure, GCS)
  • Google BigQuery
  • Multiple/partitioned files

Common I/O Operations

CSV:

# Eager
df = pl.read_csv("file.csv")
df.write_csv("output.csv")

# Lazy (preferred for large files)
lf = pl.scan_csv("file.csv")
result = lf.filter(...).select(...).collect()

Parquet (recommended for performance):

df = pl.read_parquet("file.parquet")
df.write_parquet("output.parquet")

JSON:

df = pl.read_json("file.json")
df.write_json("output.json")

For comprehensive I/O documentation, load references/io_guide.md.

Transformations

Joins

Combine DataFrames:

# Inner join
df1.join(df2, on="id", how="inner")

# Left join
df1.join(df2, on="id", how="left")

# Join on different column names
df1.join(df2, left_on="user_id", right_on="id")

Concatenation

Stack DataFrames:

# Vertical (stack rows)
pl.concat([df1, df2], how="vertical")

# Horizontal (add columns)
pl.concat([df1, df2], how="horizontal")

# Diagonal (union with different schemas)
pl.concat([df1, df2], how="diagonal")

Pivot and Unpivot

Reshape data:

# Pivot (wide format): on= is the column whose values become new columns
df.pivot("product", index="date", values="sales")

# Unpivot (long format)
df.unpivot(index="id", on=["col1", "col2"])

For detailed transformation examples, load references/transformations.md.

Pandas Migration

Polars offers significant performance improvements over pandas with a cleaner API. Key differences:

Conceptual Differences

  • No index: Polars uses integer positions only
  • Strict typing: No silent type conversions
  • Lazy evaluation: Available via LazyFrame
  • Parallel by default: Operations parallelized automatically

Common Operation Mappings

Operation Pandas Polars
Select column df["col"] df.select("col")
Filter df[df["col"] > 10] df.filter(pl.col("col") > 10)
Add column df.assign(x=...) df.with_columns(x=...)
Group by df.groupby("col").agg(...) df.group_by("col").agg(...)
Window df.groupby("col").transform(...) df.with_columns(...).over("col")

Key Syntax Patterns

Pandas sequential (slow):

df.assign(
    col_a=lambda df_: df_.value * 10,
    col_b=lambda df_: df_.value * 100
)

Polars parallel (fast):

df.with_columns(
    col_a=pl.col("value") * 10,
    col_b=pl.col("value") * 100,
)

For comprehensive migration guide, load references/pandas_migration.md.

Best Practices

Polars 1.x API notes (renamed since 0.x)

These older names appear in stale tutorials and LLM training data; use the 1.x form:

  • read_csv(dtypes=...) -> read_csv(schema_overrides=...)
  • pl.Utf8 -> pl.String
  • pl.NUMERIC_DTYPES -> import polars.selectors as cs; cs.numeric()
  • join(how="outer") -> join(how="full") (and how="full" no longer coalesces keys; pass coalesce=True for old behavior)
  • pivot(columns=...) -> pivot(on=...) (on is the first positional arg)
  • collect(streaming=True) -> collect(engine="streaming")
  • read_database(connection_uri=...) -> read_database_uri(uri=...)

Performance Optimization

  1. Use lazy evaluation for large datasets:

    lf = pl.scan_csv("large.csv")  # Don't use read_csv
    result = lf.filter(...).select(...).collect()
    
  2. Avoid Python functions in hot paths:

    • Stay within expression API for parallelization
    • Use .map_elements() only when necessary
    • Prefer native Polars operations
  3. Use the streaming engine to lower peak memory:

    lf.collect(engine="streaming")  # `streaming=True` is deprecated
    
  4. Select only needed columns early:

    # Good: Select columns early
    lf.select("col1", "col2").filter(...)
    
    # Bad: Filter on all columns first
    lf.filter(...).select("col1", "col2")
    
  5. Use appropriate data types:

    • Categorical for low-cardinality strings
    • Appropriate integer sizes (i32 vs i64)
    • Date types for temporal data

Expression Patterns

Conditional operations:

pl.when(condition).then(value).otherwise(other_value)

Column operations across multiple columns:

df.select(pl.col("^.*_value$") * 2)  # Regex pattern

Null handling:

pl.col("x").fill_null(0)
pl.col("x").is_null()
pl.col("x").drop_nulls()

For additional best practices and patterns, load references/best_practices.md.

Resources

This skill includes comprehensive reference documentation:

references/

  • core_concepts.md - Detailed explanations of expressions, lazy evaluation, and type system
  • operations.md - Comprehensive guide to all common operations with examples
  • pandas_migration.md - Complete migration guide from pandas to Polars
  • io_guide.md - Data I/O operations for all supported formats
  • transformations.md - Joins, concatenation, pivots, and reshaping operations
  • best_practices.md - Performance optimization tips and common patterns

Load these references as needed when users require detailed information about specific topics.

Files (alterlab-academic-skills)
  • evals
    • evals.json 5.4 KB
      {
        "skill": "alterlab-polars",
        "evals": [
          {
            "id": "lazy-scan-large-csv",
            "prompt": "I have a 40GB CSV of sensor readings that fits in RAM but pandas takes forever to filter and aggregate. I only need three columns and rows where status == 'active', then group by device_id and average the reading. How do I make this fast?",
            "expected_output": "Invokes alterlab-polars and uses lazy evaluation: pl.scan_csv to build a LazyFrame, applies .filter(pl.col('status') == 'active') and .select on the three needed columns early so predicate and projection pushdown kick in, then .group_by('device_id').agg(pl.col('reading').mean()), and finishes with .collect() (optionally engine=\"streaming\" to lower peak memory). Explains that the lazy query plan is optimized before execution, unlike eager pandas.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "scan_csv" },
              { "type": "behavior", "value": "Builds a LazyFrame with scan_csv, pushes filter/select down before collect(), and uses group_by().agg() rather than eager reads." }
            ]
          },
          {
            "id": "pandas-migration-parallel-with-columns",
            "prompt": "I'm porting a slow pandas script to Polars. It does df.assign() to add several derived columns with lambdas and a df.groupby('city').transform('mean') for a per-group average. What's the idiomatic Polars equivalent?",
            "expected_output": "Invokes alterlab-polars and maps the pandas idioms to the expression API: df.assign(...) lambdas become a single df.with_columns(...) call (all columns computed in parallel using pl.col), and groupby().transform('mean') becomes a window function pl.col('value').mean().over('city') inside with_columns. Notes Polars has no index and parallelizes with_columns expressions, so chaining many sequential assigns is unnecessary.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "with_columns" },
              { "type": "behavior", "value": "Translates assign->with_columns and groupby.transform->.over() window expression, emphasizing parallel expression evaluation." }
            ]
          },
          {
            "id": "groupby-window-rank",
            "prompt": "Using Polars, for each employee row I want to add their department's average salary and their salary rank within the department, without collapsing the rows. How do I do this in one pass?",
            "expected_output": "Invokes alterlab-polars and uses window functions with over() inside with_columns to preserve row count: pl.col('salary').mean().over('department') for the group average and pl.col('salary').rank().over('department') for the within-group rank. Distinguishes this from group_by().agg(), which would collapse rows, and may mention mapping strategies (group_to_rows preserves order).",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "over" },
              { "type": "behavior", "value": "Uses over() window expressions inside with_columns to add per-group statistics while preserving the original row count." }
            ]
          },
          {
            "id": "parquet-join-pipeline",
            "prompt": "I have two Parquet files, an orders table and a customers table, and I want to left-join them on customer_id, filter to orders over $100, and write the result back to Parquet efficiently.",
            "expected_output": "Invokes alterlab-polars to build the pipeline: read/scan the Parquet files (pl.scan_parquet for laziness), df1.join(df2, on='customer_id', how='left'), .filter(pl.col('amount') > 100), then write_parquet. Recommends Parquet and lazy scanning for performance and selecting only needed columns early.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "join" },
              { "type": "behavior", "value": "Joins the two frames on customer_id with how='left', filters, and reads/writes Parquet, favoring lazy scanning for performance." }
            ]
          },
          {
            "id": "near-miss-vaex",
            "prompt": "My dataset is 600GB of taxi trips and does not fit in my 48GB of RAM. I need to compute group-by aggregations over the whole thing without loading it into memory. What library should I use?",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-vaex (or alterlab-dask). The data is far larger than RAM, and Polars is for datasets that fit in memory; the polars description explicitly says to prefer vaex/dask for larger-than-RAM data. The response should point to alterlab-vaex for out-of-core, memory-mapped processing.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-vaex" }
            ]
          },
          {
            "id": "near-miss-dask",
            "prompt": "I need to distribute a heavy DataFrame ETL job across all the cores of a multi-node cluster with a task graph and a dashboard to monitor workers. What's the right tool?",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-dask. The user wants distributed, multi-node parallelism with a task graph and worker dashboard, which is Dask's territory. Polars parallelizes on a single machine over in-memory data and does not orchestrate a cluster, so the response should point to alterlab-dask.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-dask" }
            ]
          }
        ]
      }
      
  • references
    • best_practices.md 14.1 KB
      # Polars Best Practices and Performance Guide
      
      Comprehensive guide to writing efficient Polars code and avoiding common pitfalls.
      
      ## Performance Optimization
      
      ### 1. Use Lazy Evaluation
      
      **Always prefer lazy mode for large datasets:**
      
      ```python
      # Bad: Eager mode loads everything immediately
      df = pl.read_csv("large_file.csv")
      result = df.filter(pl.col("age") > 25).select("name", "age")
      
      # Good: Lazy mode optimizes before execution
      lf = pl.scan_csv("large_file.csv")
      result = lf.filter(pl.col("age") > 25).select("name", "age").collect()
      ```
      
      **Benefits of lazy evaluation:**
      - Predicate pushdown (filter at source)
      - Projection pushdown (read only needed columns)
      - Query optimization
      - Parallel execution planning
      
      ### 2. Filter and Select Early
      
      Push filters and column selection as early as possible in the pipeline:
      
      ```python
      # Bad: Process all data, then filter and select
      result = (
          lf.group_by("category")
          .agg(pl.col("value").mean())
          .join(other, on="category")
          .filter(pl.col("value") > 100)
          .select("category", "value")
      )
      
      # Good: Filter and select early
      result = (
          lf.select("category", "value")  # Only needed columns
          .filter(pl.col("value") > 100)  # Filter early
          .group_by("category")
          .agg(pl.col("value").mean())
          .join(other.select("category", "other_col"), on="category")
      )
      ```
      
      ### 3. Avoid Python Functions
      
      Stay within the expression API to maintain parallelization:
      
      ```python
      # Bad: Python function disables parallelization
      df = df.with_columns(
          result=pl.col("value").map_elements(lambda x: x * 2, return_dtype=pl.Float64)
      )
      
      # Good: Use native expressions (parallelized)
      df = df.with_columns(result=pl.col("value") * 2)
      ```
      
      **When you must use custom functions:**
      ```python
      # If truly needed, be explicit
      df = df.with_columns(
          result=pl.col("value").map_elements(
              custom_function,
              return_dtype=pl.Float64,
              skip_nulls=True  # Optimize null handling
          )
      )
      ```
      
      ### 4. Use the Streaming Engine for Large Data
      
      Enable the streaming engine to lower peak memory on large datasets:
      
      ```python
      # Streaming engine processes data in chunks
      lf = pl.scan_parquet("very_large.parquet")
      result = lf.filter(pl.col("value") > 100).collect(engine="streaming")
      # Note: `collect(streaming=True)` is deprecated in favor of engine="streaming".
      
      # Or use sink_* for direct streaming writes (always streams, no collect)
      lf.filter(pl.col("value") > 100).sink_parquet("output.parquet")
      ```
      
      ### 5. Optimize Data Types
      
      Choose appropriate data types to reduce memory and improve performance:
      
      ```python
      # Bad: Default types may be wasteful
      df = pl.read_csv("data.csv")
      
      # Good: Specify optimal types
      df = pl.read_csv(
          "data.csv",
          schema_overrides={  # `dtypes=` was renamed to `schema_overrides=`
              "id": pl.UInt32,  # Instead of Int64 if values fit
              "category": pl.Categorical,  # For low-cardinality strings
              "date": pl.Date,  # Instead of String
              "small_int": pl.Int16,  # Instead of Int64
          }
      )
      ```
      
      **Type optimization guidelines:**
      - Use smallest integer type that fits your data
      - Use `Categorical` for strings with low cardinality (<50% unique)
      - Use `Date` instead of `Datetime` when time isn't needed
      - Use `Boolean` instead of integers for binary flags
      
      ### 6. Parallel Operations
      
      Structure code to maximize parallelization:
      
      ```python
      # Bad: Sequential pipe operations disable parallelization
      df = (
          df.pipe(operation1)
          .pipe(operation2)
          .pipe(operation3)
      )
      
      # Good: Combined operations enable parallelization
      df = df.with_columns(
          result1=operation1_expr(),
          result2=operation2_expr(),
          result3=operation3_expr()
      )
      ```
      
      ### 7. Rechunk After Concatenation
      
      ```python
      # Concatenation can fragment data
      combined = pl.concat([df1, df2, df3])
      
      # Rechunk for better performance in subsequent operations
      combined = pl.concat([df1, df2, df3], rechunk=True)
      ```
      
      ## Expression Patterns
      
      ### Conditional Logic
      
      **Simple conditions:**
      ```python
      df.with_columns(
          status=pl.when(pl.col("age") >= 18)
              .then("adult")
              .otherwise("minor")
      )
      ```
      
      **Multiple conditions:**
      ```python
      df.with_columns(
          grade=pl.when(pl.col("score") >= 90)
              .then("A")
              .when(pl.col("score") >= 80)
              .then("B")
              .when(pl.col("score") >= 70)
              .then("C")
              .when(pl.col("score") >= 60)
              .then("D")
              .otherwise("F")
      )
      ```
      
      **Complex conditions:**
      ```python
      df.with_columns(
          category=pl.when(
              (pl.col("revenue") > 1000000) & (pl.col("customers") > 100)
          )
          .then("enterprise")
          .when(
              (pl.col("revenue") > 100000) | (pl.col("customers") > 50)
          )
          .then("business")
          .otherwise("starter")
      )
      ```
      
      ### Null Handling
      
      **Check for nulls:**
      ```python
      df.filter(pl.col("value").is_null())
      df.filter(pl.col("value").is_not_null())
      ```
      
      **Fill nulls:**
      ```python
      # Constant value
      df.with_columns(pl.col("value").fill_null(0))
      
      # Forward fill
      df.with_columns(pl.col("value").fill_null(strategy="forward"))
      
      # Backward fill
      df.with_columns(pl.col("value").fill_null(strategy="backward"))
      
      # Mean
      df.with_columns(pl.col("value").fill_null(strategy="mean"))
      
      # Per-group fill
      df.with_columns(
          pl.col("value").fill_null(pl.col("value").mean()).over("group")
      )
      ```
      
      **Coalesce (first non-null):**
      ```python
      df.with_columns(
          combined=pl.coalesce(["col1", "col2", "col3"])
      )
      ```
      
      ### Column Selection Patterns
      
      **By name:**
      ```python
      df.select("col1", "col2", "col3")
      ```
      
      **By pattern:**
      ```python
      # Regex
      df.select(pl.col("^sales_.*$"))
      
      # Starts with
      df.select(pl.col("^sales"))
      
      # Ends with
      df.select(pl.col("_total$"))
      
      # Contains
      df.select(pl.col(".*revenue.*"))
      ```
      
      **By type (use the selectors module):**
      ```python
      import polars.selectors as cs
      
      # All numeric columns
      df.select(cs.numeric())
      
      # All string columns
      df.select(cs.string())
      
      # Multiple types (selectors compose with set operators)
      df.select(cs.numeric() | cs.boolean())
      ```
      
      **Exclude columns:**
      ```python
      df.select(pl.all().exclude("id", "timestamp"))
      ```
      
      **Transform multiple columns:**
      ```python
      # Apply same operation to multiple columns
      df.select(
          pl.col("^sales_.*$") * 1.1  # 10% increase to all sales columns
      )
      ```
      
      ### Aggregation Patterns
      
      **Multiple aggregations:**
      ```python
      df.group_by("category").agg(
          pl.col("value").sum().alias("total"),
          pl.col("value").mean().alias("average"),
          pl.col("value").std().alias("std_dev"),
          pl.col("id").count().alias("count"),
          pl.col("id").n_unique().alias("unique_count"),
          pl.col("value").min().alias("minimum"),
          pl.col("value").max().alias("maximum"),
          pl.col("value").quantile(0.5).alias("median"),
          pl.col("value").quantile(0.95).alias("p95")
      )
      ```
      
      **Conditional aggregations:**
      ```python
      df.group_by("category").agg(
          # Count high values
          (pl.col("value") > 100).sum().alias("high_count"),
      
          # Average of filtered values
          pl.col("value").filter(pl.col("active")).mean().alias("active_avg"),
      
          # Conditional sum
          pl.when(pl.col("status") == "completed")
              .then(pl.col("amount"))
              .otherwise(0)
              .sum()
              .alias("completed_total")
      )
      ```
      
      **Grouped transformations:**
      ```python
      df.with_columns(
          # Group statistics
          group_mean=pl.col("value").mean().over("category"),
          group_std=pl.col("value").std().over("category"),
      
          # Rank within groups
          rank=pl.col("value").rank().over("category"),
      
          # Percentage of group total
          pct_of_group=(pl.col("value") / pl.col("value").sum().over("category")) * 100
      )
      ```
      
      ## Common Pitfalls and Anti-Patterns
      
      ### Pitfall 1: Row Iteration
      
      ```python
      # Bad: Never iterate rows
      for row in df.iter_rows():
          # Process row
          result = row[0] * 2
      
      # Good: Use vectorized operations
      df = df.with_columns(result=pl.col("value") * 2)
      ```
      
      ### Pitfall 2: Modifying in Place
      
      ```python
      # Bad: Polars is immutable, this doesn't work as expected
      df["new_col"] = df["old_col"] * 2  # May work but not recommended
      
      # Good: Functional style
      df = df.with_columns(new_col=pl.col("old_col") * 2)
      ```
      
      ### Pitfall 3: Not Using Expressions
      
      ```python
      # Bad: String-based operations
      df.select("value * 2")  # Won't work
      
      # Good: Expression-based
      df.select(pl.col("value") * 2)
      ```
      
      ### Pitfall 4: Inefficient Joins
      
      ```python
      # Bad: Join large tables without filtering
      result = large_df1.join(large_df2, on="id")
      
      # Good: Filter before joining
      result = (
          large_df1.filter(pl.col("active"))
          .join(
              large_df2.filter(pl.col("status") == "valid"),
              on="id"
          )
      )
      ```
      
      ### Pitfall 5: Not Specifying Types
      
      ```python
      # Bad: Let Polars infer everything
      df = pl.read_csv("data.csv")
      
      # Good: Specify types for correctness and performance
      df = pl.read_csv(
          "data.csv",
          schema_overrides={"id": pl.Int64, "date": pl.Date, "category": pl.Categorical}
      )
      ```
      
      ### Pitfall 6: Creating Many Small DataFrames
      
      ```python
      # Bad: Many operations creating intermediate DataFrames
      df1 = df.filter(pl.col("age") > 25)
      df2 = df1.select("name", "age")
      df3 = df2.sort("age")
      result = df3.head(10)
      
      # Good: Chain operations
      result = (
          df.filter(pl.col("age") > 25)
          .select("name", "age")
          .sort("age")
          .head(10)
      )
      
      # Better: Use lazy mode
      result = (
          df.lazy()
          .filter(pl.col("age") > 25)
          .select("name", "age")
          .sort("age")
          .head(10)
          .collect()
      )
      ```
      
      ## Memory Management
      
      ### Monitor Memory Usage
      
      ```python
      # Check DataFrame size
      print(f"Estimated size: {df.estimated_size('mb'):.2f} MB")
      
      # Profile memory during operations
      lf = pl.scan_csv("large.csv")
      print(lf.explain())  # See query plan
      ```
      
      ### Reduce Memory Footprint
      
      ```python
      # 1. Use lazy mode
      lf = pl.scan_parquet("data.parquet")
      
      # 2. Stream results
      result = lf.collect(engine="streaming")
      
      # 3. Select only needed columns
      lf = lf.select("col1", "col2")
      
      # 4. Optimize data types
      df = df.with_columns(
          pl.col("int_col").cast(pl.Int32),  # Downcast if possible
          pl.col("category").cast(pl.Categorical)  # For low cardinality
      )
      
      # 5. Drop columns not needed
      df = df.drop("large_text_col", "unused_col")
      ```
      
      ## Testing and Debugging
      
      ### Inspect Query Plans
      
      ```python
      lf = pl.scan_csv("data.csv")
      query = lf.filter(pl.col("age") > 25).select("name", "age")
      
      # View the optimized query plan
      print(query.explain())
      
      # View detailed query plan
      print(query.explain(optimized=True))
      ```
      
      ### Sample Data for Development
      
      ```python
      # Use n_rows for testing
      df = pl.read_csv("large.csv", n_rows=1000)
      
      # Or sample after reading
      df_sample = df.sample(n=1000, seed=42)
      ```
      
      ### Validate Schemas
      
      ```python
      # Check schema
      print(df.schema)
      
      # Ensure schema matches expectation
      expected_schema = {
          "id": pl.Int64,
          "name": pl.String,
          "date": pl.Date
      }
      
      assert df.schema == expected_schema
      ```
      
      ### Profile Performance
      
      ```python
      import time
      
      # Time operations
      start = time.time()
      result = lf.collect()
      print(f"Execution time: {time.time() - start:.2f}s")
      
      # Compare eager vs lazy
      start = time.time()
      df_eager = pl.read_csv("data.csv").filter(pl.col("age") > 25)
      eager_time = time.time() - start
      
      start = time.time()
      df_lazy = pl.scan_csv("data.csv").filter(pl.col("age") > 25).collect()
      lazy_time = time.time() - start
      
      print(f"Eager: {eager_time:.2f}s, Lazy: {lazy_time:.2f}s")
      ```
      
      ## File Format Best Practices
      
      ### Choose the Right Format
      
      **Parquet:**
      - Best for: Large datasets, archival, data lakes
      - Pros: Excellent compression, columnar, fast reads
      - Cons: Not human-readable
      
      **CSV:**
      - Best for: Small datasets, human inspection, legacy systems
      - Pros: Universal, human-readable
      - Cons: Slow, large file size, no type preservation
      
      **Arrow IPC:**
      - Best for: Inter-process communication, temporary storage
      - Pros: Fastest, zero-copy, preserves all types
      - Cons: Less compression than Parquet
      
      ### File Reading Best Practices
      
      ```python
      # 1. Use lazy reading
      lf = pl.scan_parquet("data.parquet")  # Not read_parquet
      
      # 2. Read multiple files efficiently
      lf = pl.scan_parquet("data/*.parquet")  # Parallel reading
      
      # 3. Specify schema when known
      lf = pl.scan_csv(
          "data.csv",
          schema_overrides={"id": pl.Int64, "date": pl.Date}
      )
      
      # 4. Use predicate pushdown
      result = lf.filter(pl.col("date") >= "2023-01-01").collect()
      ```
      
      ### File Writing Best Practices
      
      ```python
      # 1. Use Parquet for large data
      df.write_parquet("output.parquet", compression="zstd")
      
      # 2. Partition large datasets
      df.write_parquet("output", partition_by=["year", "month"])
      
      # 3. Use streaming for very large writes
      lf.sink_parquet("output.parquet")  # Streaming write
      
      # 4. Optimize compression
      df.write_parquet(
          "output.parquet",
          compression="snappy",  # Fast compression
          statistics=True  # Enable predicate pushdown on read
      )
      ```
      
      ## Code Organization
      
      ### Reusable Expressions
      
      ```python
      # Define reusable expressions
      age_group = (
          pl.when(pl.col("age") < 18)
          .then("minor")
          .when(pl.col("age") < 65)
          .then("adult")
          .otherwise("senior")
      )
      
      revenue_per_customer = pl.col("revenue") / pl.col("customer_count")
      
      # Use in multiple contexts
      df = df.with_columns(
          age_group=age_group,
          rpc=revenue_per_customer
      )
      
      # Reuse in filtering
      df = df.filter(revenue_per_customer > 100)
      ```
      
      ### Pipeline Functions
      
      ```python
      def clean_data(lf: pl.LazyFrame) -> pl.LazyFrame:
          """Clean and standardize data."""
          return lf.with_columns(
              pl.col("name").str.to_uppercase(),
              pl.col("date").str.strptime(pl.Date, "%Y-%m-%d"),
              pl.col("amount").fill_null(0)
          )
      
      def add_features(lf: pl.LazyFrame) -> pl.LazyFrame:
          """Add computed features."""
          return lf.with_columns(
              month=pl.col("date").dt.month(),
              year=pl.col("date").dt.year(),
              amount_log=pl.col("amount").log()
          )
      
      # Compose pipeline
      result = (
          pl.scan_csv("data.csv")
          .pipe(clean_data)
          .pipe(add_features)
          .filter(pl.col("year") == 2023)
          .collect()
      )
      ```
      
      ## Documentation
      
      Always document complex expressions and transformations:
      
      ```python
      # Good: Document intent
      df = df.with_columns(
          # Calculate customer lifetime value as sum of purchases
          # divided by months since first purchase
          clv=(
              pl.col("total_purchases") /
              ((pl.col("last_purchase_date") - pl.col("first_purchase_date"))
               .dt.total_days() / 30)
          )
      )
      ```
      
      ## Version Compatibility
      
      ```python
      # Check Polars version
      import polars as pl
      print(pl.__version__)
      
      # Feature availability varies by version
      # Document version requirements for production code
      ```
      
    • core_concepts.md 8.4 KB
      # Polars Core Concepts
      
      ## Expressions
      
      Expressions are the foundation of Polars' API. They are composable units that describe data transformations without executing them immediately.
      
      ### What are Expressions?
      
      An expression describes a transformation on data. It only materializes (executes) within specific contexts:
      - `select()` - Select and transform columns
      - `with_columns()` - Add or modify columns
      - `filter()` - Filter rows
      - `group_by().agg()` - Aggregate data
      
      ### Expression Syntax
      
      **Basic column reference:**
      ```python
      pl.col("column_name")
      ```
      
      **Computed expressions:**
      ```python
      # Arithmetic
      pl.col("height") * 2
      pl.col("price") + pl.col("tax")
      
      # With alias
      (pl.col("weight") / (pl.col("height") ** 2)).alias("bmi")
      
      # Method chaining
      pl.col("name").str.to_uppercase().str.slice(0, 3)
      ```
      
      ### Expression Contexts
      
      **Select context:**
      ```python
      df.select(
          "name",  # Simple column name
          pl.col("age"),  # Expression
          (pl.col("age") * 12).alias("age_in_months")  # Computed expression
      )
      ```
      
      **With_columns context:**
      ```python
      df.with_columns(
          age_doubled=pl.col("age") * 2,
          name_upper=pl.col("name").str.to_uppercase()
      )
      ```
      
      **Filter context:**
      ```python
      df.filter(
          pl.col("age") > 25,
          pl.col("city").is_in(["NY", "LA", "SF"])
      )
      ```
      
      **Group_by context:**
      ```python
      df.group_by("department").agg(
          pl.col("salary").mean(),
          pl.col("employee_id").count()
      )
      ```
      
      ### Expression Expansion
      
      Apply operations to multiple columns at once:
      
      **All columns:**
      ```python
      df.select(pl.all() * 2)
      ```
      
      **Pattern matching:**
      ```python
      # All columns ending with "_value"
      df.select(pl.col("^.*_value$") * 100)
      
      # All numeric columns (use the selectors module)
      import polars.selectors as cs
      df.select(cs.numeric() + 1)
      ```
      
      **Exclude patterns:**
      ```python
      df.select(pl.all().exclude("id", "name"))
      ```
      
      ### Expression Composition
      
      Expressions can be stored and reused:
      
      ```python
      # Define reusable expressions
      age_expression = pl.col("age") * 12
      name_expression = pl.col("name").str.to_uppercase()
      
      # Use in multiple contexts
      df.select(age_expression, name_expression)
      df.with_columns(age_months=age_expression)
      ```
      
      ## Data Types
      
      Polars has a strict type system based on Apache Arrow.
      
      ### Core Data Types
      
      **Numeric:**
      - `Int8`, `Int16`, `Int32`, `Int64` - Signed integers
      - `UInt8`, `UInt16`, `UInt32`, `UInt64` - Unsigned integers
      - `Float32`, `Float64` - Floating point numbers
      
      **Text:**
      - `String` - UTF-8 encoded strings (`Utf8` is a deprecated alias)
      - `Categorical` - Categorized strings (low cardinality)
      - `Enum` - Fixed set of string values
      
      **Temporal:**
      - `Date` - Calendar date (no time)
      - `Datetime` - Date and time with optional timezone
      - `Time` - Time of day
      - `Duration` - Time duration/difference
      
      **Boolean:**
      - `Boolean` - True/False values
      
      **Nested:**
      - `List` - Variable-length lists
      - `Array` - Fixed-length arrays
      - `Struct` - Nested record structures
      
      **Other:**
      - `Binary` - Binary data
      - `Object` - Python objects (avoid in production)
      - `Null` - Null type
      
      ### Type Casting
      
      Convert between types explicitly:
      
      ```python
      # Cast to different type
      df.select(
          pl.col("age").cast(pl.Float64),
          pl.col("date_string").str.strptime(pl.Date, "%Y-%m-%d"),
          pl.col("id").cast(pl.String)
      )
      ```
      
      ### Null Handling
      
      Polars uses consistent null handling across all types:
      
      **Check for nulls:**
      ```python
      df.filter(pl.col("value").is_null())
      df.filter(pl.col("value").is_not_null())
      ```
      
      **Fill nulls:**
      ```python
      pl.col("value").fill_null(0)
      pl.col("value").fill_null(strategy="forward")
      pl.col("value").fill_null(strategy="backward")
      pl.col("value").fill_null(strategy="mean")
      ```
      
      **Drop nulls:**
      ```python
      df.drop_nulls()  # Drop any row with nulls
      df.drop_nulls(subset=["col1", "col2"])  # Drop rows with nulls in specific columns
      ```
      
      ### Categorical Data
      
      Use categorical types for string columns with low cardinality (repeated values):
      
      ```python
      # Cast to categorical
      df.with_columns(
          pl.col("category").cast(pl.Categorical)
      )
      
      # Benefits:
      # - Reduced memory usage
      # - Faster grouping and joining
      # - Maintains order information
      ```
      
      ## Lazy vs Eager Evaluation
      
      Polars supports two execution modes: eager (DataFrame) and lazy (LazyFrame).
      
      ### Eager Evaluation (DataFrame)
      
      Operations execute immediately:
      
      ```python
      import polars as pl
      
      # DataFrame operations execute right away
      df = pl.read_csv("data.csv")  # Reads file immediately
      result = df.filter(pl.col("age") > 25)  # Filters immediately
      final = result.select("name", "age")  # Selects immediately
      ```
      
      **When to use eager:**
      - Small datasets that fit in memory
      - Interactive exploration in notebooks
      - Simple one-off operations
      - Immediate feedback needed
      
      ### Lazy Evaluation (LazyFrame)
      
      Operations build a query plan, optimized before execution:
      
      ```python
      import polars as pl
      
      # LazyFrame operations build a query plan
      lf = pl.scan_csv("data.csv")  # Doesn't read yet
      lf2 = lf.filter(pl.col("age") > 25)  # Adds to plan
      lf3 = lf2.select("name", "age")  # Adds to plan
      df = lf3.collect()  # NOW executes optimized plan
      ```
      
      **When to use lazy:**
      - Large datasets
      - Complex query pipelines
      - Only need subset of data
      - Performance is critical
      - Streaming required
      
      ### Query Optimization
      
      Polars automatically optimizes lazy queries:
      
      **Predicate Pushdown:**
      Filter operations pushed to data source when possible:
      ```python
      # Only reads rows where age > 25 from CSV
      lf = pl.scan_csv("data.csv")
      result = lf.filter(pl.col("age") > 25).collect()
      ```
      
      **Projection Pushdown:**
      Only read needed columns from data source:
      ```python
      # Only reads "name" and "age" columns from CSV
      lf = pl.scan_csv("data.csv")
      result = lf.select("name", "age").collect()
      ```
      
      **Query Plan Inspection:**
      ```python
      # View the optimized query plan
      lf = pl.scan_csv("data.csv")
      result = lf.filter(pl.col("age") > 25).select("name", "age")
      print(result.explain())  # Shows optimized plan
      ```
      
      ### Streaming Engine
      
      The streaming engine processes the query in chunks to lower peak memory:
      
      ```python
      # Enable the streaming engine for large datasets
      lf = pl.scan_csv("very_large.csv")
      result = lf.filter(pl.col("age") > 25).collect(engine="streaming")
      # Note: the old `collect(streaming=True)` keyword is deprecated.
      ```
      
      **Streaming benefits:**
      - Lower peak memory usage
      - Chunk-based processing
      
      **Streaming limitations:**
      - Not all operations support streaming (unsupported ops fall back to in-memory)
      - May be slower for small data
      - Some operations require materializing the entire dataset
      
      For datasets that genuinely exceed RAM, prefer an out-of-core engine (dask/vaex)
      rather than relying on Polars streaming.
      
      ### Converting Between Eager and Lazy
      
      **Eager to Lazy:**
      ```python
      df = pl.read_csv("data.csv")
      lf = df.lazy()  # Convert to LazyFrame
      ```
      
      **Lazy to Eager:**
      ```python
      lf = pl.scan_csv("data.csv")
      df = lf.collect()  # Execute and return DataFrame
      ```
      
      ## Memory Format
      
      Polars uses Apache Arrow columnar memory format:
      
      **Benefits:**
      - Zero-copy data sharing with other Arrow libraries
      - Efficient columnar operations
      - SIMD vectorization
      - Reduced memory overhead
      - Fast serialization
      
      **Implications:**
      - Data stored column-wise, not row-wise
      - Column operations very fast
      - Random row access slower than pandas
      - Best for analytical workloads
      
      ## Parallelization
      
      Polars parallelizes operations automatically using Rust's concurrency:
      
      **What gets parallelized:**
      - Aggregations within groups
      - Window functions
      - Most expression evaluations
      - File reading (multiple files)
      - Join operations
      
      **What to avoid for parallelization:**
      - Python user-defined functions (UDFs)
      - Lambda functions in `.map_elements()`
      - Sequential `.pipe()` chains
      
      **Best practice:**
      ```python
      # Good: Stays in expression API (parallelized)
      df.with_columns(
          pl.col("value") * 10,
          pl.col("value").log(),
          pl.col("value").sqrt()
      )
      
      # Bad: Uses Python function (sequential)
      df.with_columns(
          pl.col("value").map_elements(lambda x: x * 10)
      )
      ```
      
      ## Strict Type System
      
      Polars enforces strict typing:
      
      **No silent conversions:**
      ```python
      # This will error - can't mix types
      # df.with_columns(pl.col("int_col") + "string")
      
      # Must cast explicitly
      df.with_columns(
          pl.col("int_col").cast(pl.String) + "_suffix"
      )
      ```
      
      **Benefits:**
      - Prevents silent bugs
      - Predictable behavior
      - Better performance
      - Clearer code intent
      
      **Integer nulls:**
      Unlike pandas, integer columns can have nulls without converting to float:
      ```python
      # In pandas: Int column with null becomes Float
      # In polars: Int column with null stays Int (with null values)
      df = pl.DataFrame({"int_col": [1, 2, None, 4]})
      # dtype: Int64 (not Float64)
      ```
      
    • io_guide.md 11.9 KB
      # Polars Data I/O Guide
      
      Comprehensive guide to reading and writing data in various formats with Polars.
      
      ## CSV Files
      
      ### Reading CSV
      
      **Eager mode (loads into memory):**
      ```python
      import polars as pl
      
      # Basic read
      df = pl.read_csv("data.csv")
      
      # With options
      df = pl.read_csv(
          "data.csv",
          separator=",",
          has_header=True,
          columns=["col1", "col2"],  # Select specific columns
          n_rows=1000,  # Read only first 1000 rows
          skip_rows=10,  # Skip first 10 rows
          schema_overrides={"col1": pl.Int64, "col2": pl.String},  # `dtypes=` was renamed
          null_values=["NA", "null", ""],  # Define null values
          encoding="utf-8",
          ignore_errors=False
      )
      ```
      
      **Lazy mode (scans without loading - recommended for large files):**
      ```python
      # Scan CSV (builds query plan)
      lf = pl.scan_csv("data.csv")
      
      # Apply operations
      result = lf.filter(pl.col("age") > 25).select("name", "age")
      
      # Execute and load
      df = result.collect()
      ```
      
      ### Writing CSV
      
      ```python
      # Basic write
      df.write_csv("output.csv")
      
      # With options
      df.write_csv(
          "output.csv",
          separator=",",
          include_header=True,
          null_value="",  # How to represent nulls
          quote_char='"',
          line_terminator="\n"
      )
      ```
      
      ### Multiple CSV Files
      
      **Read multiple files:**
      ```python
      # Read all CSVs in directory
      lf = pl.scan_csv("data/*.csv")
      
      # Read specific files
      lf = pl.scan_csv(["file1.csv", "file2.csv", "file3.csv"])
      ```
      
      ## Parquet Files
      
      Parquet is the recommended format for performance and compression.
      
      ### Reading Parquet
      
      **Eager:**
      ```python
      df = pl.read_parquet("data.parquet")
      
      # With options
      df = pl.read_parquet(
          "data.parquet",
          columns=["col1", "col2"],  # Select specific columns
          n_rows=1000,  # Read first N rows
          parallel="auto"  # Control parallelization
      )
      ```
      
      **Lazy (recommended):**
      ```python
      lf = pl.scan_parquet("data.parquet")
      
      # Automatic predicate and projection pushdown
      result = lf.filter(pl.col("age") > 25).select("name", "age").collect()
      ```
      
      ### Writing Parquet
      
      ```python
      # Basic write
      df.write_parquet("output.parquet")
      
      # With compression
      df.write_parquet(
          "output.parquet",
          compression="snappy",  # Options: "snappy", "gzip", "brotli", "lz4", "zstd"
          statistics=True,  # Write statistics (enables predicate pushdown)
          use_pyarrow=False  # Use Rust writer (faster)
      )
      ```
      
      ### Partitioned Parquet (Hive-style)
      
      **Write partitioned:**
      ```python
      # Write with partitioning
      df.write_parquet(
          "output_dir",
          partition_by=["year", "month"]  # Creates directory structure
      )
      # Creates: output_dir/year=2023/month=01/data.parquet
      ```
      
      **Read partitioned:**
      ```python
      lf = pl.scan_parquet("output_dir/**/*.parquet")
      
      # Hive partitioning columns are automatically added
      result = lf.filter(pl.col("year") == 2023).collect()
      ```
      
      ## JSON Files
      
      ### Reading JSON
      
      **NDJSON (newline-delimited JSON) - recommended:**
      ```python
      df = pl.read_ndjson("data.ndjson")
      
      # Lazy
      lf = pl.scan_ndjson("data.ndjson")
      ```
      
      **Standard JSON:**
      ```python
      df = pl.read_json("data.json")
      
      # From JSON string
      df = pl.read_json('{"col1": [1, 2], "col2": ["a", "b"]}')
      ```
      
      ### Writing JSON
      
      ```python
      # Write NDJSON
      df.write_ndjson("output.ndjson")
      
      # Write standard JSON
      df.write_json("output.json")
      
      # Pretty printed
      df.write_json("output.json", pretty=True, row_oriented=False)
      ```
      
      ## Excel Files
      
      ### Reading Excel
      
      ```python
      # Read first sheet
      df = pl.read_excel("data.xlsx")
      
      # Specific sheet by name
      df = pl.read_excel("data.xlsx", sheet_name="Sheet1")
      # Or by 1-based index (sheet_id=0 reads ALL sheets into a dict)
      df = pl.read_excel("data.xlsx", sheet_id=1)
      
      # With options
      df = pl.read_excel(
          "data.xlsx",
          sheet_name="Sheet1",
          columns=["A", "B", "C"],  # Excel columns
          n_rows=100,
          skip_rows=5,
          has_header=True
      )
      ```
      
      ### Writing Excel
      
      ```python
      # Write to Excel (requires the xlsxwriter package)
      df.write_excel("output.xlsx")
      
      # Multiple sheets: pass a shared xlsxwriter.Workbook via `workbook=`
      import xlsxwriter
      with xlsxwriter.Workbook("output.xlsx") as wb:
          df1.write_excel(workbook=wb, worksheet="Sheet1")
          df2.write_excel(workbook=wb, worksheet="Sheet2")
      ```
      
      ## Database Connectivity
      
      ### Read from Database
      
      ```python
      import polars as pl
      
      # From a URI (uses ConnectorX/ADBC under the hood) - fast for bulk reads
      df = pl.read_database_uri(
          "SELECT * FROM users WHERE age > 25",
          uri="postgresql://user:pass@localhost/db"
      )
      
      # From an existing connection object (e.g. SQLAlchemy / DBAPI / ADBC cursor)
      from sqlalchemy import create_engine
      engine = create_engine("postgresql://user:pass@localhost/db")
      with engine.connect() as conn:
          df = pl.read_database("SELECT * FROM users", connection=conn)
      ```
      
      Note: `pl.read_database` takes a live `connection`, while `pl.read_database_uri`
      takes a `uri` string. The old `read_database(..., connection_uri=...)` signature
      was split into these two functions.
      
      ### Write to Database
      
      ```python
      # Using SQLAlchemy
      from sqlalchemy import create_engine
      
      engine = create_engine("postgresql://user:pass@localhost/db")
      df.write_database("table_name", connection=engine)
      
      # With options
      df.write_database(
          "table_name",
          connection=engine,
          if_exists="replace",  # or "append", "fail"
      )
      ```
      
      ### Common Database Connectors
      
      **PostgreSQL:**
      ```python
      uri = "postgresql://username:password@localhost:5432/database"
      df = pl.read_database_uri("SELECT * FROM table", uri=uri)
      ```
      
      **MySQL:**
      ```python
      uri = "mysql://username:password@localhost:3306/database"
      df = pl.read_database_uri("SELECT * FROM table", uri=uri)
      ```
      
      **SQLite:**
      ```python
      uri = "sqlite:///path/to/database.db"
      df = pl.read_database_uri("SELECT * FROM table", uri=uri)
      ```
      
      ## Cloud Storage
      
      ### AWS S3
      
      ```python
      # Read from S3
      df = pl.read_parquet("s3://bucket/path/to/file.parquet")
      lf = pl.scan_parquet("s3://bucket/path/*.parquet")
      
      # Write to S3
      df.write_parquet("s3://bucket/path/output.parquet")
      
      # With credentials
      import os
      os.environ["AWS_ACCESS_KEY_ID"] = "your_key"
      os.environ["AWS_SECRET_ACCESS_KEY"] = "your_secret"
      os.environ["AWS_REGION"] = "us-west-2"
      
      df = pl.read_parquet("s3://bucket/file.parquet")
      ```
      
      ### Azure Blob Storage
      
      ```python
      # Read from Azure
      df = pl.read_parquet("az://container/path/file.parquet")
      
      # Write to Azure
      df.write_parquet("az://container/path/output.parquet")
      
      # With credentials
      os.environ["AZURE_STORAGE_ACCOUNT_NAME"] = "account"
      os.environ["AZURE_STORAGE_ACCOUNT_KEY"] = "key"
      ```
      
      ### Google Cloud Storage (GCS)
      
      ```python
      # Read from GCS
      df = pl.read_parquet("gs://bucket/path/file.parquet")
      
      # Write to GCS
      df.write_parquet("gs://bucket/path/output.parquet")
      
      # With credentials
      os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/credentials.json"
      ```
      
      ## Google BigQuery
      
      ```python
      # Via the Google Cloud BigQuery client, then hand the Arrow result to Polars
      from google.cloud import bigquery
      client = bigquery.Client()
      
      query = "SELECT * FROM project.dataset.table WHERE date > '2023-01-01'"
      df = pl.from_arrow(client.query(query).to_arrow())  # zero-copy from Arrow
      ```
      
      ## Apache Arrow
      
      ### IPC/Feather Format
      
      **Read:**
      ```python
      df = pl.read_ipc("data.arrow")
      lf = pl.scan_ipc("data.arrow")
      ```
      
      **Write:**
      ```python
      df.write_ipc("output.arrow")
      
      # Compressed
      df.write_ipc("output.arrow", compression="zstd")
      ```
      
      ### Arrow Streaming
      
      ```python
      # Write streaming format
      df.write_ipc("output.arrows", compression="zstd")
      
      # Read streaming
      df = pl.read_ipc("output.arrows")
      ```
      
      ### From/To Arrow
      
      ```python
      import pyarrow as pa
      
      # From Arrow Table
      arrow_table = pa.table({"col": [1, 2, 3]})
      df = pl.from_arrow(arrow_table)
      
      # To Arrow Table
      arrow_table = df.to_arrow()
      ```
      
      ## In-Memory Formats
      
      ### Python Dictionaries
      
      ```python
      # From dict
      df = pl.DataFrame({
          "col1": [1, 2, 3],
          "col2": ["a", "b", "c"]
      })
      
      # To dict
      data_dict = df.to_dict()  # Column-oriented
      data_dict = df.to_dict(as_series=False)  # Lists instead of Series
      ```
      
      ### NumPy Arrays
      
      ```python
      import numpy as np
      
      # From NumPy
      arr = np.array([[1, 2], [3, 4], [5, 6]])
      df = pl.DataFrame(arr, schema=["col1", "col2"])
      
      # To NumPy
      arr = df.to_numpy()
      ```
      
      ### Pandas DataFrames
      
      ```python
      import pandas as pd
      
      # From Pandas
      pd_df = pd.DataFrame({"col": [1, 2, 3]})
      pl_df = pl.from_pandas(pd_df)
      
      # To Pandas
      pd_df = pl_df.to_pandas()
      
      # Go through Arrow for zero-copy when possible (from_arrow takes an Arrow table)
      import pyarrow as pa
      pl_df = pl.from_arrow(pa.Table.from_pandas(pd_df))
      ```
      
      ### Lists of Rows
      
      ```python
      # From list of dicts
      data = [
          {"name": "Alice", "age": 25},
          {"name": "Bob", "age": 30}
      ]
      df = pl.DataFrame(data)
      
      # To list of dicts
      rows = df.to_dicts()
      
      # From list of tuples
      data = [("Alice", 25), ("Bob", 30)]
      df = pl.DataFrame(data, schema=["name", "age"])
      ```
      
      ## Streaming Large Files
      
      For large datasets, use lazy mode with the streaming engine to lower peak memory:
      
      ```python
      # Streaming engine
      lf = pl.scan_csv("very_large.csv")
      result = lf.filter(pl.col("value") > 100).collect(engine="streaming")
      
      # Streaming with multiple files
      lf = pl.scan_parquet("data/*.parquet")
      result = lf.group_by("category").agg(pl.col("value").sum()).collect(engine="streaming")
      ```
      
      `collect(streaming=True)` is deprecated; use `engine="streaming"`. For data that
      genuinely exceeds RAM, prefer an out-of-core engine (dask/vaex).
      
      ## Best Practices
      
      ### Format Selection
      
      **Use Parquet when:**
      - Need compression (up to 10x smaller than CSV)
      - Want fast reads/writes
      - Need to preserve data types
      - Working with large datasets
      - Need predicate pushdown
      
      **Use CSV when:**
      - Need human-readable format
      - Interfacing with legacy systems
      - Data is small
      - Need universal compatibility
      
      **Use JSON when:**
      - Working with nested/hierarchical data
      - Need web API compatibility
      - Data has flexible schema
      
      **Use Arrow IPC when:**
      - Need zero-copy data sharing
      - Fastest serialization required
      - Working between Arrow-compatible systems
      
      ### Reading Large Files
      
      ```python
      # 1. Always use lazy mode
      lf = pl.scan_csv("large.csv")  # NOT read_csv
      
      # 2. Filter and select early (pushdown optimization)
      result = (
          lf
          .select("col1", "col2", "col3")  # Only needed columns
          .filter(pl.col("date") > "2023-01-01")  # Filter early
          .collect()
      )
      
      # 3. Use the streaming engine for large data
      result = lf.filter(...).select(...).collect(engine="streaming")
      
      # 4. Read only needed rows during development
      df = pl.read_csv("large.csv", n_rows=10000)  # Sample for testing
      ```
      
      ### Writing Large Files
      
      ```python
      # 1. Use Parquet with compression
      df.write_parquet("output.parquet", compression="zstd")
      
      # 2. Use partitioning for very large datasets
      df.write_parquet("output", partition_by=["year", "month"])
      
      # 3. Write streaming
      lf = pl.scan_csv("input.csv")
      lf.sink_parquet("output.parquet")  # Streaming write
      ```
      
      ### Performance Tips
      
      ```python
      # 1. Specify types when reading CSV
      df = pl.read_csv(
          "data.csv",
          schema_overrides={"id": pl.Int64, "name": pl.String}  # Avoids inference
      )
      
      # 2. Use appropriate compression
      df.write_parquet("output.parquet", compression="snappy")  # Fast
      df.write_parquet("output.parquet", compression="zstd")    # Better compression
      
      # 3. Parallel reading
      df = pl.read_csv("data.csv", parallel="auto")
      
      # 4. Read multiple files in parallel
      lf = pl.scan_parquet("data/*.parquet")  # Automatic parallel read
      ```
      
      ## Error Handling
      
      ```python
      try:
          df = pl.read_csv("data.csv")
      except pl.exceptions.ComputeError as e:
          print(f"Error reading CSV: {e}")
      
      # Ignore errors during parsing
      df = pl.read_csv("messy.csv", ignore_errors=True)
      
      # Handle missing files
      from pathlib import Path
      if Path("data.csv").exists():
          df = pl.read_csv("data.csv")
      else:
          print("File not found")
      ```
      
      ## Schema Management
      
      ```python
      # Infer schema from a sample
      schema = pl.read_csv("data.csv", n_rows=1000).schema
      
      # Define schema explicitly
      schema = {
          "id": pl.Int64,
          "name": pl.String,
          "date": pl.Date,
          "value": pl.Float64,
      }
      
      # `schema=` declares the full schema (all columns);
      # `schema_overrides=` overrides only the columns you name and infers the rest.
      df = pl.read_csv("data.csv", schema=schema)
      df = pl.read_csv("data.csv", schema_overrides={"id": pl.Int64})
      ```
      
    • operations.md 12.1 KB
      # Polars Operations Reference
      
      This reference covers all common Polars operations with comprehensive examples.
      
      ## Selection Operations
      
      ### Select Columns
      
      **Basic selection:**
      ```python
      # Select specific columns
      df.select("name", "age", "city")
      
      # Using expressions
      df.select(pl.col("name"), pl.col("age"))
      ```
      
      **Pattern-based selection:**
      ```python
      # All columns starting with "sales_"
      df.select(pl.col("^sales_.*$"))
      
      # All numeric columns (use the selectors module)
      import polars.selectors as cs
      df.select(cs.numeric())
      
      # All columns except specific ones
      df.select(pl.all().exclude("id", "timestamp"))
      ```
      
      **Computed columns:**
      ```python
      df.select(
          "name",
          (pl.col("age") * 12).alias("age_in_months"),
          (pl.col("salary") * 1.1).alias("salary_after_raise")
      )
      ```
      
      ### With Columns (Add/Modify)
      
      Add new columns or modify existing ones while preserving all other columns:
      
      ```python
      # Add new columns
      df.with_columns(
          age_doubled=pl.col("age") * 2,
          full_name=pl.col("first_name") + " " + pl.col("last_name")
      )
      
      # Modify existing columns
      df.with_columns(
          pl.col("name").str.to_uppercase().alias("name"),
          pl.col("salary").cast(pl.Float64).alias("salary")
      )
      
      # Multiple operations in parallel
      df.with_columns(
          pl.col("value") * 10,
          pl.col("value") * 100,
          pl.col("value") * 1000,
      )
      ```
      
      ## Filtering Operations
      
      ### Basic Filtering
      
      ```python
      # Single condition
      df.filter(pl.col("age") > 25)
      
      # Multiple conditions (AND)
      df.filter(
          pl.col("age") > 25,
          pl.col("city") == "NY"
      )
      
      # OR conditions
      df.filter(
          (pl.col("age") > 30) | (pl.col("salary") > 100000)
      )
      
      # NOT condition
      df.filter(~pl.col("active"))
      df.filter(pl.col("city") != "NY")
      ```
      
      ### Advanced Filtering
      
      **String operations:**
      ```python
      # Contains substring
      df.filter(pl.col("name").str.contains("John"))
      
      # Starts with
      df.filter(pl.col("email").str.starts_with("admin"))
      
      # Regex match
      df.filter(pl.col("phone").str.contains(r"^\d{3}-\d{3}-\d{4}$"))
      ```
      
      **Membership checks:**
      ```python
      # In list
      df.filter(pl.col("city").is_in(["NY", "LA", "SF"]))
      
      # Not in list
      df.filter(~pl.col("status").is_in(["inactive", "deleted"]))
      ```
      
      **Range filters:**
      ```python
      # Between values
      df.filter(pl.col("age").is_between(25, 35))
      
      # Date range
      df.filter(
          pl.col("date") >= pl.date(2023, 1, 1),
          pl.col("date") <= pl.date(2023, 12, 31)
      )
      ```
      
      **Null filtering:**
      ```python
      # Filter out nulls
      df.filter(pl.col("value").is_not_null())
      
      # Keep only nulls
      df.filter(pl.col("value").is_null())
      ```
      
      ## Grouping and Aggregation
      
      ### Basic Group By
      
      ```python
      # Group by single column
      df.group_by("department").agg(
          pl.col("salary").mean().alias("avg_salary"),
          pl.len().alias("employee_count")
      )
      
      # Group by multiple columns
      df.group_by("department", "location").agg(
          pl.col("salary").sum()
      )
      
      # Maintain order
      df.group_by("category", maintain_order=True).agg(
          pl.col("value").sum()
      )
      ```
      
      ### Aggregation Functions
      
      **Count and length:**
      ```python
      df.group_by("category").agg(
          pl.len().alias("count"),
          pl.col("id").count().alias("non_null_count"),
          pl.col("id").n_unique().alias("unique_count")
      )
      ```
      
      **Statistical aggregations:**
      ```python
      df.group_by("group").agg(
          pl.col("value").sum().alias("total"),
          pl.col("value").mean().alias("average"),
          pl.col("value").median().alias("median"),
          pl.col("value").std().alias("std_dev"),
          pl.col("value").var().alias("variance"),
          pl.col("value").min().alias("minimum"),
          pl.col("value").max().alias("maximum"),
          pl.col("value").quantile(0.95).alias("p95")
      )
      ```
      
      **First and last:**
      ```python
      df.group_by("user_id").agg(
          pl.col("timestamp").first().alias("first_seen"),
          pl.col("timestamp").last().alias("last_seen"),
          pl.col("event").first().alias("first_event")
      )
      ```
      
      **List aggregation:**
      ```python
      # Collect values into lists
      df.group_by("category").agg(
          pl.col("item").alias("all_items")  # Creates list column
      )
      ```
      
      ### Conditional Aggregations
      
      Filter within aggregations:
      
      ```python
      df.group_by("department").agg(
          # Count high earners
          (pl.col("salary") > 100000).sum().alias("high_earners"),
      
          # Average of filtered values
          pl.col("salary").filter(pl.col("bonus") > 0).mean().alias("avg_with_bonus"),
      
          # Conditional sum
          pl.when(pl.col("active"))
            .then(pl.col("sales"))
            .otherwise(0)
            .sum()
            .alias("active_sales")
      )
      ```
      
      ### Multiple Aggregations
      
      Combine multiple aggregations efficiently:
      
      ```python
      df.group_by("store_id").agg(
          pl.col("transaction_id").count().alias("num_transactions"),
          pl.col("amount").sum().alias("total_sales"),
          pl.col("amount").mean().alias("avg_transaction"),
          pl.col("customer_id").n_unique().alias("unique_customers"),
          pl.col("amount").max().alias("largest_transaction"),
          pl.col("timestamp").min().alias("first_transaction_date"),
          pl.col("timestamp").max().alias("last_transaction_date")
      )
      ```
      
      ## Window Functions
      
      Window functions apply aggregations while preserving the original row count.
      
      ### Basic Window Operations
      
      **Group statistics:**
      ```python
      # Add group mean to each row
      df.with_columns(
          avg_age_by_dept=pl.col("age").mean().over("department")
      )
      
      # Multiple group columns
      df.with_columns(
          group_avg=pl.col("value").mean().over("category", "region")
      )
      ```
      
      **Ranking:**
      ```python
      df.with_columns(
          # Rank within groups
          rank=pl.col("score").rank().over("team"),
      
          # Dense rank (no gaps)
          dense_rank=pl.col("score").rank(method="dense").over("team"),
      
          # Row number
          row_num=pl.col("timestamp").sort().rank(method="ordinal").over("user_id")
      )
      ```
      
      ### Window Mapping Strategies
      
      **group_to_rows (default):**
      Preserves original row order:
      ```python
      df.with_columns(
          group_mean=pl.col("value").mean().over("category", mapping_strategy="group_to_rows")
      )
      ```
      
      **explode:**
      Changes the row count (like `group_by().agg()` followed by `explode()`), so it only works
      in `select`, not `with_columns`; results come out grouped. Useful for per-group top-k:
      ```python
      df.select(
          pl.col("category").head(2).over("category", mapping_strategy="explode"),
          pl.col("value").sort(descending=True).head(2).over("category", mapping_strategy="explode"),
      )
      ```
      
      **join:**
      Creates list columns:
      ```python
      df.with_columns(
          group_values=pl.col("value").over("category", mapping_strategy="join")
      )
      ```
      
      ### Rolling Windows
      
      **Time-based rolling:**
      ```python
      df.with_columns(
          rolling_avg=pl.col("value").rolling_mean(
              window_size="7d",
              by="date"
          )
      )
      ```
      
      **Row-based rolling:**
      ```python
      df.with_columns(
          rolling_sum=pl.col("value").rolling_sum(window_size=3),
          rolling_max=pl.col("value").rolling_max(window_size=5)
      )
      ```
      
      ### Cumulative Operations
      
      ```python
      df.with_columns(
          cumsum=pl.col("value").cum_sum().over("group"),
          cummax=pl.col("value").cum_max().over("group"),
          cummin=pl.col("value").cum_min().over("group"),
          cumprod=pl.col("value").cum_prod().over("group")
      )
      ```
      
      ### Shift and Lag/Lead
      
      ```python
      df.with_columns(
          # Previous value (lag)
          prev_value=pl.col("value").shift(1).over("user_id"),
      
          # Next value (lead)
          next_value=pl.col("value").shift(-1).over("user_id"),
      
          # Calculate difference from previous
          diff=pl.col("value") - pl.col("value").shift(1).over("user_id")
      )
      ```
      
      ## Sorting
      
      ### Basic Sorting
      
      ```python
      # Sort by single column
      df.sort("age")
      
      # Sort descending
      df.sort("age", descending=True)
      
      # Sort by multiple columns
      df.sort("department", "age")
      
      # Mixed sorting order
      df.sort(["department", "salary"], descending=[False, True])
      ```
      
      ### Advanced Sorting
      
      **Null handling:**
      ```python
      # Nulls first
      df.sort("value", nulls_last=False)
      
      # Nulls last
      df.sort("value", nulls_last=True)
      ```
      
      **Sort by expression:**
      ```python
      # Sort by computed value
      df.sort(pl.col("first_name").str.len())
      
      # Sort by multiple expressions
      df.sort(
          pl.col("last_name").str.to_lowercase(),
          pl.col("age").abs()
      )
      ```
      
      ## Conditional Operations
      
      ### When/Then/Otherwise
      
      ```python
      # Basic conditional
      df.with_columns(
          status=pl.when(pl.col("age") >= 18)
              .then("adult")
              .otherwise("minor")
      )
      
      # Multiple conditions
      df.with_columns(
          category=pl.when(pl.col("score") >= 90)
              .then("A")
              .when(pl.col("score") >= 80)
              .then("B")
              .when(pl.col("score") >= 70)
              .then("C")
              .otherwise("F")
      )
      
      # Conditional computation
      df.with_columns(
          adjusted_price=pl.when(pl.col("is_member"))
              .then(pl.col("price") * 0.9)
              .otherwise(pl.col("price"))
      )
      ```
      
      ## String Operations
      
      ### Common String Methods
      
      ```python
      df.with_columns(
          # Case conversion
          upper=pl.col("name").str.to_uppercase(),
          lower=pl.col("name").str.to_lowercase(),
          title=pl.col("name").str.to_titlecase(),
      
          # Trimming
          trimmed=pl.col("text").str.strip_chars(),
      
          # Substring
          first_3=pl.col("name").str.slice(0, 3),
      
          # Replace
          cleaned=pl.col("text").str.replace("old", "new"),
          cleaned_all=pl.col("text").str.replace_all("old", "new"),
      
          # Split
          parts=pl.col("full_name").str.split(" "),
      
          # Length
          name_length=pl.col("name").str.len_chars()
      )
      ```
      
      ### String Filtering
      
      ```python
      # Contains
      df.filter(pl.col("email").str.contains("@gmail.com"))
      
      # Starts/ends with
      df.filter(pl.col("name").str.starts_with("A"))
      df.filter(pl.col("file").str.ends_with(".csv"))
      
      # Regex matching
      df.filter(pl.col("phone").str.contains(r"^\d{3}-\d{4}$"))
      ```
      
      ## Date and Time Operations
      
      ### Date Parsing
      
      ```python
      # Parse strings to dates
      df.with_columns(
          date=pl.col("date_str").str.strptime(pl.Date, "%Y-%m-%d"),
          datetime=pl.col("dt_str").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S")
      )
      ```
      
      ### Date Components
      
      ```python
      df.with_columns(
          year=pl.col("date").dt.year(),
          month=pl.col("date").dt.month(),
          day=pl.col("date").dt.day(),
          weekday=pl.col("date").dt.weekday(),
          hour=pl.col("datetime").dt.hour(),
          minute=pl.col("datetime").dt.minute()
      )
      ```
      
      ### Date Arithmetic
      
      ```python
      # Add duration
      df.with_columns(
          next_week=pl.col("date") + pl.duration(weeks=1),
          next_month=pl.col("date") + pl.duration(months=1)
      )
      
      # Difference between dates
      df.with_columns(
          days_diff=(pl.col("end_date") - pl.col("start_date")).dt.total_days()
      )
      ```
      
      ### Date Filtering
      
      ```python
      # Filter by date range
      df.filter(
          pl.col("date").is_between(pl.date(2023, 1, 1), pl.date(2023, 12, 31))
      )
      
      # Filter by year
      df.filter(pl.col("date").dt.year() == 2023)
      
      # Filter by month
      df.filter(pl.col("date").dt.month().is_in([6, 7, 8]))  # Summer months
      ```
      
      ## List Operations
      
      ### Working with List Columns
      
      ```python
      # Combine several columns into one list column
      df.with_columns(
          items_list=pl.concat_list("item1", "item2", "item3")
      )
      
      # List operations
      df.with_columns(
          list_len=pl.col("items").list.len(),
          first_item=pl.col("items").list.first(),
          last_item=pl.col("items").list.last(),
          unique_items=pl.col("items").list.unique(),
          sorted_items=pl.col("items").list.sort()
      )
      
      # Explode lists to rows
      df.explode("items")
      
      # Filter list elements
      df.with_columns(
          filtered=pl.col("items").list.eval(pl.element() > 10)
      )
      ```
      
      ## Struct Operations
      
      ### Working with Nested Structures
      
      ```python
      # Create struct column
      df.with_columns(
          address=pl.struct(["street", "city", "zip"])
      )
      
      # Access struct fields
      df.with_columns(
          city=pl.col("address").struct.field("city")
      )
      
      # Unnest struct to columns
      df.unnest("address")
      ```
      
      ## Unique and Duplicate Operations
      
      ```python
      # Get unique rows
      df.unique()
      
      # Unique on specific columns
      df.unique(subset=["name", "email"])
      
      # Keep first/last duplicate
      df.unique(subset=["id"], keep="first")
      df.unique(subset=["id"], keep="last")
      
      # Identify duplicates
      df.with_columns(
          is_duplicate=pl.col("id").is_duplicated()
      )
      
      # Count duplicates
      df.group_by("email").agg(
          pl.len().alias("count")
      ).filter(pl.col("count") > 1)
      ```
      
      ## Sampling
      
      ```python
      # Random sample
      df.sample(n=100)
      
      # Sample fraction
      df.sample(fraction=0.1)
      
      # Sample with seed for reproducibility
      df.sample(n=100, seed=42)
      ```
      
      ## Column Renaming
      
      ```python
      # Rename specific columns
      df.rename({"old_name": "new_name", "age": "years"})
      
      # Rename with expression
      df.select(pl.col("*").name.suffix("_renamed"))
      df.select(pl.col("*").name.prefix("data_"))
      df.select(pl.col("*").name.to_uppercase())
      ```
      
    • pandas_migration.md 12.1 KB
      # Pandas to Polars Migration Guide
      
      This guide helps you migrate from pandas to Polars with comprehensive operation mappings and key differences.
      
      ## Core Conceptual Differences
      
      ### 1. No Index System
      
      **Pandas:** Uses row-based indexing system
      ```python
      df.loc[0, "column"]
      df.iloc[0:5]
      df.set_index("id")
      ```
      
      **Polars:** Uses integer positions only
      ```python
      df[0, "column"]  # Row position, column name
      df[0:5]  # Row slice
      # No set_index equivalent - use group_by instead
      ```
      
      ### 2. Memory Format
      
      **Pandas:** Row-oriented NumPy arrays
      **Polars:** Columnar Apache Arrow format
      
      **Implications:**
      - Polars is faster for column operations
      - Polars uses less memory
      - Polars has better data sharing capabilities
      
      ### 3. Parallelization
      
      **Pandas:** Primarily single-threaded (requires Dask for parallelism)
      **Polars:** Parallel by default using Rust's concurrency
      
      ### 4. Lazy Evaluation
      
      **Pandas:** Only eager evaluation
      **Polars:** Both eager (DataFrame) and lazy (LazyFrame) with query optimization
      
      ### 5. Type Strictness
      
      **Pandas:** Allows silent type conversions
      **Polars:** Strict typing, explicit casts required
      
      **Example:**
      ```python
      # Pandas: Silently converts to float
      pd_df["int_col"] = [1, 2, None, 4]  # dtype: float64
      
      # Polars: Keeps as integer with null
      pl_df = pl.DataFrame({"int_col": [1, 2, None, 4]})  # dtype: Int64
      ```
      
      ## Operation Mappings
      
      ### Data Selection
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Select column | `df["col"]` or `df.col` | `df.select("col")` or `df["col"]` |
      | Select multiple | `df[["a", "b"]]` | `df.select("a", "b")` |
      | Select by position | `df.iloc[:, 0:3]` | `df.select(pl.col(df.columns[0:3]))` |
      | Select by condition | `df[df["age"] > 25]` | `df.filter(pl.col("age") > 25)` |
      
      ### Data Filtering
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Single condition | `df[df["age"] > 25]` | `df.filter(pl.col("age") > 25)` |
      | Multiple conditions | `df[(df["age"] > 25) & (df["city"] == "NY")]` | `df.filter(pl.col("age") > 25, pl.col("city") == "NY")` |
      | Query method | `df.query("age > 25")` | `df.filter(pl.col("age") > 25)` |
      | isin | `df[df["city"].isin(["NY", "LA"])]` | `df.filter(pl.col("city").is_in(["NY", "LA"]))` |
      | isna | `df[df["value"].isna()]` | `df.filter(pl.col("value").is_null())` |
      | notna | `df[df["value"].notna()]` | `df.filter(pl.col("value").is_not_null())` |
      
      ### Adding/Modifying Columns
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Add column | `df["new"] = df["old"] * 2` | `df.with_columns(new=pl.col("old") * 2)` |
      | Multiple columns | `df.assign(a=..., b=...)` | `df.with_columns(a=..., b=...)` |
      | Conditional column | `np.where(condition, a, b)` | `pl.when(condition).then(a).otherwise(b)` |
      
      **Important difference - Parallel execution:**
      
      ```python
      # Pandas: Sequential (lambda sees previous results)
      df.assign(
          a=lambda df_: df_.value * 10,
          b=lambda df_: df_.value * 100
      )
      
      # Polars: Parallel (all computed together)
      df.with_columns(
          a=pl.col("value") * 10,
          b=pl.col("value") * 100
      )
      ```
      
      ### Grouping and Aggregation
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Group by | `df.groupby("col")` | `df.group_by("col")` |
      | Agg single | `df.groupby("col")["val"].mean()` | `df.group_by("col").agg(pl.col("val").mean())` |
      | Agg multiple | `df.groupby("col").agg({"val": ["mean", "sum"]})` | `df.group_by("col").agg(pl.col("val").mean(), pl.col("val").sum())` |
      | Size | `df.groupby("col").size()` | `df.group_by("col").agg(pl.len())` |
      | Count | `df.groupby("col").count()` | `df.group_by("col").agg(pl.col("*").count())` |
      
      ### Window Functions
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Transform | `df.groupby("col").transform("mean")` | `df.with_columns(pl.col("val").mean().over("col"))` |
      | Rank | `df.groupby("col")["val"].rank()` | `df.with_columns(pl.col("val").rank().over("col"))` |
      | Shift | `df.groupby("col")["val"].shift(1)` | `df.with_columns(pl.col("val").shift(1).over("col"))` |
      | Cumsum | `df.groupby("col")["val"].cumsum()` | `df.with_columns(pl.col("val").cum_sum().over("col"))` |
      
      ### Joins
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Inner join | `df1.merge(df2, on="id")` | `df1.join(df2, on="id", how="inner")` |
      | Left join | `df1.merge(df2, on="id", how="left")` | `df1.join(df2, on="id", how="left")` |
      | Different keys | `df1.merge(df2, left_on="a", right_on="b")` | `df1.join(df2, left_on="a", right_on="b")` |
      
      ### Concatenation
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Vertical | `pd.concat([df1, df2], axis=0)` | `pl.concat([df1, df2], how="vertical")` |
      | Horizontal | `pd.concat([df1, df2], axis=1)` | `pl.concat([df1, df2], how="horizontal")` |
      
      ### Sorting
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Sort by column | `df.sort_values("col")` | `df.sort("col")` |
      | Descending | `df.sort_values("col", ascending=False)` | `df.sort("col", descending=True)` |
      | Multiple columns | `df.sort_values(["a", "b"])` | `df.sort("a", "b")` |
      
      ### Reshaping
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Pivot | `df.pivot(index="a", columns="b", values="c")` | `df.pivot("b", index="a", values="c")` |
      | Melt | `df.melt(id_vars="id")` | `df.unpivot(index="id")` |
      
      ### I/O Operations
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Read CSV | `pd.read_csv("file.csv")` | `pl.read_csv("file.csv")` or `pl.scan_csv()` |
      | Write CSV | `df.to_csv("file.csv")` | `df.write_csv("file.csv")` |
      | Read Parquet | `pd.read_parquet("file.parquet")` | `pl.read_parquet("file.parquet")` |
      | Write Parquet | `df.to_parquet("file.parquet")` | `df.write_parquet("file.parquet")` |
      | Read Excel | `pd.read_excel("file.xlsx")` | `pl.read_excel("file.xlsx")` |
      
      ### String Operations
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Upper | `df["col"].str.upper()` | `df.select(pl.col("col").str.to_uppercase())` |
      | Lower | `df["col"].str.lower()` | `df.select(pl.col("col").str.to_lowercase())` |
      | Contains | `df["col"].str.contains("pattern")` | `df.filter(pl.col("col").str.contains("pattern"))` |
      | Replace | `df["col"].str.replace("old", "new")` | `df.select(pl.col("col").str.replace("old", "new"))` |
      | Split | `df["col"].str.split(" ")` | `df.select(pl.col("col").str.split(" "))` |
      
      ### Datetime Operations
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Parse dates | `pd.to_datetime(df["col"])` | `df.select(pl.col("col").str.strptime(pl.Date, "%Y-%m-%d"))` |
      | Year | `df["date"].dt.year` | `df.select(pl.col("date").dt.year())` |
      | Month | `df["date"].dt.month` | `df.select(pl.col("date").dt.month())` |
      | Day | `df["date"].dt.day` | `df.select(pl.col("date").dt.day())` |
      
      ### Missing Data
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Drop nulls | `df.dropna()` | `df.drop_nulls()` |
      | Fill nulls | `df.fillna(0)` | `df.fill_null(0)` |
      | Check null | `df["col"].isna()` | `df.select(pl.col("col").is_null())` |
      | Forward fill | `df.fillna(method="ffill")` | `df.select(pl.col("col").fill_null(strategy="forward"))` |
      
      ### Other Operations
      
      | Operation | Pandas | Polars |
      |-----------|--------|--------|
      | Unique values | `df["col"].unique()` | `df["col"].unique()` |
      | Value counts | `df["col"].value_counts()` | `df["col"].value_counts()` |
      | Describe | `df.describe()` | `df.describe()` |
      | Sample | `df.sample(n=100)` | `df.sample(n=100)` |
      | Head | `df.head()` | `df.head()` |
      | Tail | `df.tail()` | `df.tail()` |
      
      ## Common Migration Patterns
      
      ### Pattern 1: Chained Operations
      
      **Pandas:**
      ```python
      result = (df
          .assign(new_col=lambda x: x["old_col"] * 2)
          .query("new_col > 10")
          .groupby("category")
          .agg({"value": "sum"})
          .reset_index()
      )
      ```
      
      **Polars:**
      ```python
      result = (df
          .with_columns(new_col=pl.col("old_col") * 2)
          .filter(pl.col("new_col") > 10)
          .group_by("category")
          .agg(pl.col("value").sum())
      )
      # No reset_index needed - Polars doesn't have index
      ```
      
      ### Pattern 2: Apply Functions
      
      **Pandas:**
      ```python
      # Avoid in Polars - breaks parallelization
      df["result"] = df["value"].apply(lambda x: x * 2)
      ```
      
      **Polars:**
      ```python
      # Use expressions instead
      df = df.with_columns(result=pl.col("value") * 2)
      
      # If custom function needed
      df = df.with_columns(
          result=pl.col("value").map_elements(lambda x: x * 2, return_dtype=pl.Float64)
      )
      ```
      
      ### Pattern 3: Conditional Column Creation
      
      **Pandas:**
      ```python
      df["category"] = np.where(
          df["value"] > 100,
          "high",
          np.where(df["value"] > 50, "medium", "low")
      )
      ```
      
      **Polars:**
      ```python
      df = df.with_columns(
          category=pl.when(pl.col("value") > 100)
              .then("high")
              .when(pl.col("value") > 50)
              .then("medium")
              .otherwise("low")
      )
      ```
      
      ### Pattern 4: Group Transform
      
      **Pandas:**
      ```python
      df["group_mean"] = df.groupby("category")["value"].transform("mean")
      ```
      
      **Polars:**
      ```python
      df = df.with_columns(
          group_mean=pl.col("value").mean().over("category")
      )
      ```
      
      ### Pattern 5: Multiple Aggregations
      
      **Pandas:**
      ```python
      result = df.groupby("category").agg({
          "value": ["mean", "sum", "count"],
          "price": ["min", "max"]
      })
      ```
      
      **Polars:**
      ```python
      result = df.group_by("category").agg(
          pl.col("value").mean().alias("value_mean"),
          pl.col("value").sum().alias("value_sum"),
          pl.col("value").count().alias("value_count"),
          pl.col("price").min().alias("price_min"),
          pl.col("price").max().alias("price_max")
      )
      ```
      
      ## Performance Anti-Patterns to Avoid
      
      ### Anti-Pattern 1: Sequential Pipe Operations
      
      **Bad (disables parallelization):**
      ```python
      df = df.pipe(function1).pipe(function2).pipe(function3)
      ```
      
      **Good (enables parallelization):**
      ```python
      df = df.with_columns(
          function1_result(),
          function2_result(),
          function3_result()
      )
      ```
      
      ### Anti-Pattern 2: Python Functions in Hot Paths
      
      **Bad:**
      ```python
      df = df.with_columns(
          result=pl.col("value").map_elements(lambda x: x * 2)
      )
      ```
      
      **Good:**
      ```python
      df = df.with_columns(result=pl.col("value") * 2)
      ```
      
      ### Anti-Pattern 3: Using Eager Reading for Large Files
      
      **Bad:**
      ```python
      df = pl.read_csv("large_file.csv")
      result = df.filter(pl.col("age") > 25).select("name", "age")
      ```
      
      **Good:**
      ```python
      lf = pl.scan_csv("large_file.csv")
      result = lf.filter(pl.col("age") > 25).select("name", "age").collect()
      ```
      
      ### Anti-Pattern 4: Row Iteration
      
      **Bad:**
      ```python
      for row in df.iter_rows():
          # Process row
          pass
      ```
      
      **Good:**
      ```python
      # Use vectorized operations
      df = df.with_columns(
          # Vectorized computation
      )
      ```
      
      ## Migration Checklist
      
      When migrating from pandas to Polars:
      
      1. **Remove index operations** - Use integer positions or group_by
      2. **Replace apply/map with expressions** - Use Polars native operations
      3. **Update column assignment** - Use `with_columns()` instead of direct assignment
      4. **Change groupby.transform to .over()** - Window functions work differently
      5. **Update string operations** - Use `.str.to_uppercase()` instead of `.str.upper()`
      6. **Add explicit type casts** - Polars won't silently convert types
      7. **Consider lazy evaluation** - Use `scan_*` instead of `read_*` for large data
      8. **Update aggregation syntax** - More explicit in Polars
      9. **Remove reset_index calls** - Not needed in Polars
      10. **Update conditional logic** - Use `when().then().otherwise()` pattern
      
      ## Compatibility Layer
      
      For gradual migration, you can use both libraries:
      
      ```python
      import pandas as pd
      import polars as pl
      
      # Convert pandas to Polars
      pl_df = pl.from_pandas(pd_df)
      
      # Convert Polars to pandas
      pd_df = pl_df.to_pandas()
      
      # Use Arrow for zero-copy (when possible)
      pl_df = pl.from_arrow(pd_df)
      pd_df = pl_df.to_arrow().to_pandas()
      ```
      
      ## When to Stick with Pandas
      
      Consider staying with pandas when:
      - Working with time series requiring complex index operations
      - Need extensive ecosystem support (some libraries only support pandas)
      - Team lacks Rust/Polars expertise
      - Data is small and performance isn't critical
      - Using advanced pandas features without Polars equivalents
      
      ## When to Switch to Polars
      
      Switch to Polars when:
      - Performance is critical
      - Working with large datasets (>1GB)
      - Need lazy evaluation and query optimization
      - Want better type safety
      - Need parallel execution by default
      - Starting a new project
      
    • transformations.md 11.1 KB
      # Polars Data Transformations
      
      Comprehensive guide to joins, concatenation, and reshaping operations in Polars.
      
      ## Joins
      
      Joins combine data from multiple DataFrames based on common columns.
      
      ### Basic Join Types
      
      **Inner Join (intersection):**
      ```python
      # Keep only matching rows from both DataFrames
      result = df1.join(df2, on="id", how="inner")
      ```
      
      **Left Join (all left + matches from right):**
      ```python
      # Keep all rows from left, add matching rows from right
      result = df1.join(df2, on="id", how="left")
      ```
      
      **Full (outer) Join (union):**
      ```python
      # Keep all rows from both DataFrames (how="outer" was renamed to "full")
      result = df1.join(df2, on="id", how="full")
      
      # how="full" keeps both key columns (id, id_right). Coalesce them into one:
      result = df1.join(df2, on="id", how="full", coalesce=True)
      ```
      
      **Cross Join (Cartesian product):**
      ```python
      # Every row from left with every row from right
      result = df1.join(df2, how="cross")
      ```
      
      **Semi Join (filtered left):**
      ```python
      # Keep only left rows that have a match in right
      result = df1.join(df2, on="id", how="semi")
      ```
      
      **Anti Join (non-matching left):**
      ```python
      # Keep only left rows that DON'T have a match in right
      result = df1.join(df2, on="id", how="anti")
      ```
      
      ### Join Syntax Variations
      
      **Single column join:**
      ```python
      df1.join(df2, on="id")
      ```
      
      **Multiple columns join:**
      ```python
      df1.join(df2, on=["id", "date"])
      ```
      
      **Different column names:**
      ```python
      df1.join(df2, left_on="user_id", right_on="id")
      ```
      
      **Multiple different columns:**
      ```python
      df1.join(
          df2,
          left_on=["user_id", "date"],
          right_on=["id", "timestamp"]
      )
      ```
      
      ### Suffix Handling
      
      When both DataFrames have columns with the same name (other than join keys):
      
      ```python
      # Add suffixes to distinguish columns
      result = df1.join(df2, on="id", suffix="_right")
      
      # Results in: value, value_right (if both had "value" column)
      ```
      
      ### Join Examples
      
      **Example 1: Customer Orders**
      ```python
      customers = pl.DataFrame({
          "customer_id": [1, 2, 3, 4],
          "name": ["Alice", "Bob", "Charlie", "David"]
      })
      
      orders = pl.DataFrame({
          "order_id": [101, 102, 103],
          "customer_id": [1, 2, 1],
          "amount": [100, 200, 150]
      })
      
      # Inner join - only customers with orders
      result = customers.join(orders, on="customer_id", how="inner")
      
      # Left join - all customers, even without orders
      result = customers.join(orders, on="customer_id", how="left")
      ```
      
      **Example 2: Time-series data**
      ```python
      prices = pl.DataFrame({
          "date": ["2023-01-01", "2023-01-02", "2023-01-03"],
          "stock": ["AAPL", "AAPL", "AAPL"],
          "price": [150, 152, 151]
      })
      
      volumes = pl.DataFrame({
          "date": ["2023-01-01", "2023-01-02"],
          "stock": ["AAPL", "AAPL"],
          "volume": [1000000, 1100000]
      })
      
      result = prices.join(
          volumes,
          on=["date", "stock"],
          how="left"
      )
      ```
      
      ### Asof Joins (Nearest Match)
      
      For time-series data, join to nearest timestamp:
      
      ```python
      # Join to nearest earlier timestamp
      quotes = pl.DataFrame({
          "timestamp": [1, 2, 3, 4, 5],
          "stock": ["A", "A", "A", "A", "A"],
          "quote": [100, 101, 102, 103, 104]
      })
      
      trades = pl.DataFrame({
          "timestamp": [1.5, 3.5, 4.2],
          "stock": ["A", "A", "A"],
          "trade": [50, 75, 100]
      })
      
      result = trades.join_asof(
          quotes,
          on="timestamp",
          by="stock",
          strategy="backward"  # or "forward", "nearest"
      )
      ```
      
      ## Concatenation
      
      Concatenation stacks DataFrames together.
      
      ### Vertical Concatenation (Stack Rows)
      
      ```python
      df1 = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
      df2 = pl.DataFrame({"a": [5, 6], "b": [7, 8]})
      
      # Stack rows
      result = pl.concat([df1, df2], how="vertical")
      # Result: 4 rows, same columns
      ```
      
      **Handling mismatched schemas:**
      ```python
      df1 = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
      df2 = pl.DataFrame({"a": [5, 6], "c": [7, 8]})
      
      # Diagonal concat - fills missing columns with nulls
      result = pl.concat([df1, df2], how="diagonal")
      # Result: columns a, b, c (with nulls where not present)
      ```
      
      ### Horizontal Concatenation (Stack Columns)
      
      ```python
      df1 = pl.DataFrame({"a": [1, 2, 3]})
      df2 = pl.DataFrame({"b": [4, 5, 6]})
      
      # Stack columns
      result = pl.concat([df1, df2], how="horizontal")
      # Result: 3 rows, columns a and b
      ```
      
      **Note:** Horizontal concat requires same number of rows.
      
      ### Concatenation Options
      
      ```python
      # Rechunk after concatenation (better performance for subsequent operations)
      result = pl.concat([df1, df2], rechunk=True)
      
      # Parallel execution
      result = pl.concat([df1, df2], parallel=True)
      ```
      
      ### Use Cases
      
      **Combining data from multiple sources:**
      ```python
      # Read multiple files and concatenate
      files = ["data_2023.csv", "data_2024.csv", "data_2025.csv"]
      dfs = [pl.read_csv(f) for f in files]
      combined = pl.concat(dfs, how="vertical")
      ```
      
      **Adding computed columns:**
      ```python
      base = pl.DataFrame({"value": [1, 2, 3]})
      computed = pl.DataFrame({"doubled": [2, 4, 6]})
      result = pl.concat([base, computed], how="horizontal")
      ```
      
      ## Pivoting (Wide Format)
      
      Convert unique values from one column into multiple columns.
      
      ### Basic Pivot
      
      ```python
      df = pl.DataFrame({
          "date": ["2023-01", "2023-01", "2023-02", "2023-02"],
          "product": ["A", "B", "A", "B"],
          "sales": [100, 150, 120, 160]
      })
      
      # Pivot: products become columns (`columns=` was renamed to `on=`)
      pivoted = df.pivot(
          "product",      # on: column whose values become new columns
          index="date",
          values="sales",
      )
      # Result:
      # date     | A   | B
      # 2023-01  | 100 | 150
      # 2023-02  | 120 | 160
      ```
      
      ### Pivot with Aggregation
      
      When there are duplicate combinations, aggregate:
      
      ```python
      df = pl.DataFrame({
          "date": ["2023-01", "2023-01", "2023-01"],
          "product": ["A", "A", "B"],
          "sales": [100, 110, 150]
      })
      
      # Aggregate duplicates
      pivoted = df.pivot(
          "product",
          index="date",
          values="sales",
          aggregate_function="sum"  # or "mean", "max", "min", etc.
      )
      ```
      
      ### Multiple Index Columns
      
      ```python
      df = pl.DataFrame({
          "region": ["North", "North", "South", "South"],
          "date": ["2023-01", "2023-01", "2023-01", "2023-01"],
          "product": ["A", "B", "A", "B"],
          "sales": [100, 150, 120, 160]
      })
      
      pivoted = df.pivot(
          "product",
          index=["region", "date"],
          values="sales",
      )
      ```
      
      ## Unpivoting/Melting (Long Format)
      
      Convert multiple columns into rows (opposite of pivot).
      
      ### Basic Unpivot
      
      ```python
      df = pl.DataFrame({
          "date": ["2023-01", "2023-02"],
          "product_A": [100, 120],
          "product_B": [150, 160]
      })
      
      # Unpivot: convert columns to rows
      unpivoted = df.unpivot(
          index="date",
          on=["product_A", "product_B"]
      )
      # Result:
      # date     | variable   | value
      # 2023-01  | product_A  | 100
      # 2023-01  | product_B  | 150
      # 2023-02  | product_A  | 120
      # 2023-02  | product_B  | 160
      ```
      
      ### Custom Column Names
      
      ```python
      unpivoted = df.unpivot(
          index="date",
          on=["product_A", "product_B"],
          variable_name="product",
          value_name="sales"
      )
      ```
      
      ### Unpivot by Pattern
      
      ```python
      # Unpivot all columns matching pattern
      df = pl.DataFrame({
          "id": [1, 2],
          "sales_Q1": [100, 200],
          "sales_Q2": [150, 250],
          "sales_Q3": [120, 220],
          "revenue_Q1": [1000, 2000]
      })
      
      # Unpivot all sales columns
      unpivoted = df.unpivot(
          index="id",
          on=pl.col("^sales_.*$")
      )
      ```
      
      ## Exploding (Unnesting Lists)
      
      Convert list columns into multiple rows.
      
      ### Basic Explode
      
      ```python
      df = pl.DataFrame({
          "id": [1, 2],
          "values": [[1, 2, 3], [4, 5]]
      })
      
      # Explode list into rows
      exploded = df.explode("values")
      # Result:
      # id | values
      # 1  | 1
      # 1  | 2
      # 1  | 3
      # 2  | 4
      # 2  | 5
      ```
      
      ### Multiple Column Explode
      
      ```python
      df = pl.DataFrame({
          "id": [1, 2],
          "letters": [["a", "b"], ["c", "d"]],
          "numbers": [[1, 2], [3, 4]]
      })
      
      # Explode multiple columns (must be same length)
      exploded = df.explode("letters", "numbers")
      ```
      
      ## Transposing
      
      Swap rows and columns:
      
      ```python
      df = pl.DataFrame({
          "metric": ["sales", "costs", "profit"],
          "Q1": [100, 60, 40],
          "Q2": [150, 80, 70]
      })
      
      # Transpose
      transposed = df.transpose(
          include_header=True,
          header_name="quarter",
          column_names="metric"
      )
      # Result: quarters as rows, metrics as columns
      ```
      
      ## Reshaping Patterns
      
      ### Pattern 1: Wide to Long to Wide
      
      ```python
      # Start wide
      wide = pl.DataFrame({
          "id": [1, 2],
          "A": [10, 20],
          "B": [30, 40]
      })
      
      # To long
      long = wide.unpivot(index="id", on=["A", "B"])
      
      # Back to wide (maybe with transformations)
      wide_again = long.pivot("variable", index="id", values="value")
      ```
      
      ### Pattern 2: Nested to Flat
      
      ```python
      # Nested data
      df = pl.DataFrame({
          "user": [1, 2],
          "purchases": [
              [{"item": "A", "qty": 2}, {"item": "B", "qty": 1}],
              [{"item": "C", "qty": 3}]
          ]
      })
      
      # Explode and unnest
      flat = (
          df.explode("purchases")
          .unnest("purchases")
      )
      ```
      
      ### Pattern 3: Aggregation to Pivot
      
      ```python
      # Raw data
      sales = pl.DataFrame({
          "date": ["2023-01", "2023-01", "2023-02"],
          "product": ["A", "B", "A"],
          "sales": [100, 150, 120]
      })
      
      # Aggregate then pivot
      result = (
          sales
          .group_by("date", "product")
          .agg(pl.col("sales").sum())
          .pivot("product", index="date", values="sales")
      )
      ```
      
      ## Advanced Transformations
      
      ### Conditional Reshaping
      
      ```python
      # Pivot only certain values
      df.filter(pl.col("year") >= 2020).pivot(...)
      
      # Unpivot with filtering
      df.unpivot(index="id", on=pl.col("^sales.*$"))
      ```
      
      ### Multi-level Transformations
      
      ```python
      # Complex reshaping pipeline
      result = (
          df
          .unpivot(index="id", on=pl.col("^Q[0-9]_.*$"))
          .with_columns(
              quarter=pl.col("variable").str.extract(r"Q([0-9])", 1),
              metric=pl.col("variable").str.extract(r"Q[0-9]_(.*)", 1)
          )
          .drop("variable")
          .pivot("metric", index=["id", "quarter"], values="value")
      )
      ```
      
      ## Performance Considerations
      
      ### Join Performance
      
      ```python
      # 1. Join on indexed/sorted columns when possible
      df1_sorted = df1.sort("id")
      df2_sorted = df2.sort("id")
      result = df1_sorted.join(df2_sorted, on="id")
      
      # 2. Use appropriate join type
      # semi/anti are faster than inner+filter
      matches = df1.join(df2, on="id", how="semi")  # Better than filtering after inner join
      
      # 3. Filter before joining
      df1_filtered = df1.filter(pl.col("active"))
      result = df1_filtered.join(df2, on="id")  # Smaller join
      ```
      
      ### Concatenation Performance
      
      ```python
      # 1. Rechunk after concatenation
      result = pl.concat(dfs, rechunk=True)
      
      # 2. Use lazy mode for large concatenations
      lf1 = pl.scan_parquet("file1.parquet")
      lf2 = pl.scan_parquet("file2.parquet")
      result = pl.concat([lf1, lf2]).collect()
      ```
      
      ### Pivot Performance
      
      ```python
      # 1. Filter before pivoting
      pivoted = df.filter(pl.col("year") == 2023).pivot(...)
      
      # 2. Specify aggregate function explicitly
      pivoted = df.pivot(..., aggregate_function="first")  # Faster than "sum" if only one value
      ```
      
      ## Common Use Cases
      
      ### Time Series Alignment
      
      ```python
      # Align two time series with different timestamps
      ts1.join_asof(ts2, on="timestamp", strategy="backward")
      ```
      
      ### Feature Engineering
      
      ```python
      # Create lag features
      df.with_columns(
          pl.col("value").shift(1).over("user_id").alias("prev_value"),
          pl.col("value").shift(2).over("user_id").alias("prev_prev_value")
      )
      ```
      
      ### Data Denormalization
      
      ```python
      # Combine normalized tables
      orders.join(customers, on="customer_id").join(products, on="product_id")
      ```
      
      ### Report Generation
      
      ```python
      # Pivot for reporting
      sales.pivot("product", index="month", values="amount")
      ```
      
  • SKILL.md 11.6 KB
    ---
    name: alterlab-polars
    description: Fast in-memory DataFrame analytics with Polars — lazy evaluation, parallel execution, and an Apache Arrow backend for datasets that fit in RAM. Use when pandas is too slow but data still fits in memory, for 1-100GB datasets, ETL pipelines, or a faster pandas replacement. For larger-than-RAM data prefer dask or vaex. Part of the AlterLab Academic Skills suite.
    license: MIT
    allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*)
    compatibility: No API key required. Runs locally via `uv run python`; requires polars >= 1.0 (current 1.44 as of 2026-09; 2.0 is in release candidate).
    metadata:
        skill-author: AlterLab
        version: "1.0.1"
        last_updated: "2026-09-23"
    ---
    
    # Polars
    
    ## Overview
    
    Polars is a lightning-fast DataFrame library for Python and Rust built on Apache Arrow. Work with Polars' expression-based API, lazy evaluation framework, and high-performance data manipulation capabilities for efficient data processing, pandas migration, and data pipeline optimization.
    
    ## When to Use This Skill
    
    - pandas code is too slow but the data (or the columns a lazy query touches) fits on one machine
    - Building ETL or feature pipelines with lazy scans (`scan_csv`, `scan_parquet`) and query optimization
    - Migrating pandas code to the Polars expression API
    - Window functions, joins, and group-by aggregations over large in-memory tables
    
    ### Does NOT Trigger
    
    | Scenario | Use Instead |
    |----------|-------------|
    | Distributed execution across a cluster, or scaling existing pandas/NumPy code with a task scheduler and dashboard | `alterlab-dask` |
    | Billion-row out-of-core exploration with memory-mapped HDF5/Arrow files and big-data plots | `alterlab-vaex` |
    | First look at an unfamiliar data file: structure, missingness, and quality report | `alterlab-eda` |
    
    ## Quick Start
    
    ### Installation and Basic Usage
    
    Pin a recent 1.x (examples here use the Polars 1.x API):
    ```bash
    uv add 'polars>=1.0,<2'
    ```
    
    Polars 2.0 is in release candidate (2.0.0rc2, September 2026). Its upgrade guide lists changes
    that alter results silently: `LazyFrame.collect()` defaults to the streaming engine (row order
    of some joins/unpivots can change), `pl.concat(how="horizontal")` requires equal heights
    (`how="horizontal_extend"` pads), `explode()` drops empty lists instead of emitting a null row,
    `is_in` refuses lossy casts, `LazyFrame.profile()` is removed, and headerless CSV columns are
    named from `column_0`. Check https://docs.pola.rs/releases/upgrade/ before moving to 2.x.
    
    Basic DataFrame creation and operations:
    ```python
    import polars as pl
    
    # Create DataFrame
    df = pl.DataFrame({
        "name": ["Alice", "Bob", "Charlie"],
        "age": [25, 30, 35],
        "city": ["NY", "LA", "SF"]
    })
    
    # Select columns
    df.select("name", "age")
    
    # Filter rows
    df.filter(pl.col("age") > 25)
    
    # Add computed columns
    df.with_columns(
        age_plus_10=pl.col("age") + 10
    )
    ```
    
    ## Core Concepts
    
    ### Expressions
    
    Expressions are the fundamental building blocks of Polars operations. They describe transformations on data and can be composed, reused, and optimized.
    
    **Key principles:**
    - Use `pl.col("column_name")` to reference columns
    - Chain methods to build complex transformations
    - Expressions are lazy and only execute within contexts (select, with_columns, filter, group_by)
    
    **Example:**
    ```python
    # Expression-based computation
    df.select(
        pl.col("name"),
        (pl.col("age") * 12).alias("age_in_months")
    )
    ```
    
    ### Lazy vs Eager Evaluation
    
    **Eager (DataFrame):** Operations execute immediately
    ```python
    df = pl.read_csv("file.csv")  # Reads immediately
    result = df.filter(pl.col("age") > 25)  # Executes immediately
    ```
    
    **Lazy (LazyFrame):** Operations build a query plan, optimized before execution
    ```python
    lf = pl.scan_csv("file.csv")  # Doesn't read yet
    result = lf.filter(pl.col("age") > 25).select("name", "age")
    df = result.collect()  # Now executes optimized query
    ```
    
    **When to use lazy:**
    - Working with large datasets
    - Complex query pipelines
    - When only some columns/rows are needed
    - Performance is critical
    
    **Benefits of lazy evaluation:**
    - Automatic query optimization
    - Predicate pushdown
    - Projection pushdown
    - Parallel execution
    
    For detailed concepts, load `references/core_concepts.md`.
    
    ## Common Operations
    
    ### Select
    Select and manipulate columns:
    ```python
    # Select specific columns
    df.select("name", "age")
    
    # Select with expressions
    df.select(
        pl.col("name"),
        (pl.col("age") * 2).alias("double_age")
    )
    
    # Select all columns matching a pattern
    df.select(pl.col("^.*_id$"))
    ```
    
    ### Filter
    Filter rows by conditions:
    ```python
    # Single condition
    df.filter(pl.col("age") > 25)
    
    # Multiple conditions (cleaner than using &)
    df.filter(
        pl.col("age") > 25,
        pl.col("city") == "NY"
    )
    
    # Complex conditions
    df.filter(
        (pl.col("age") > 25) | (pl.col("city") == "LA")
    )
    ```
    
    ### With Columns
    Add or modify columns while preserving existing ones:
    ```python
    # Add new columns
    df.with_columns(
        age_plus_10=pl.col("age") + 10,
        name_upper=pl.col("name").str.to_uppercase()
    )
    
    # Parallel computation (all columns computed in parallel)
    df.with_columns(
        pl.col("value") * 10,
        pl.col("value") * 100,
    )
    ```
    
    ### Group By and Aggregations
    Group data and compute aggregations:
    ```python
    # Basic grouping
    df.group_by("city").agg(
        pl.col("age").mean().alias("avg_age"),
        pl.len().alias("count")
    )
    
    # Multiple group keys
    df.group_by("city", "department").agg(
        pl.col("salary").sum()
    )
    
    # Conditional aggregations
    df.group_by("city").agg(
        (pl.col("age") > 30).sum().alias("over_30")
    )
    ```
    
    For detailed operation patterns, load `references/operations.md`.
    
    ## Aggregations and Window Functions
    
    ### Aggregation Functions
    Common aggregations within `group_by` context:
    - `pl.len()` - count rows
    - `pl.col("x").sum()` - sum values
    - `pl.col("x").mean()` - average
    - `pl.col("x").min()` / `pl.col("x").max()` - extremes
    - `pl.first()` / `pl.last()` - first/last values
    
    ### Window Functions with `over()`
    Apply aggregations while preserving row count:
    ```python
    # Add group statistics to each row
    df.with_columns(
        avg_age_by_city=pl.col("age").mean().over("city"),
        rank_in_city=pl.col("salary").rank().over("city")
    )
    
    # Multiple grouping columns
    df.with_columns(
        group_avg=pl.col("value").mean().over("category", "region")
    )
    ```
    
    **Mapping strategies:**
    - `group_to_rows` (default): Preserves original row order
    - `explode`: Changes the row count (use in `select`, not `with_columns`); output is grouped
    - `join`: Creates list columns
    
    ## Data I/O
    
    ### Supported Formats
    Polars supports reading and writing:
    - CSV, Parquet, JSON, Excel
    - Databases (via connectors)
    - Cloud storage (S3, Azure, GCS)
    - Google BigQuery
    - Multiple/partitioned files
    
    ### Common I/O Operations
    
    **CSV:**
    ```python
    # Eager
    df = pl.read_csv("file.csv")
    df.write_csv("output.csv")
    
    # Lazy (preferred for large files)
    lf = pl.scan_csv("file.csv")
    result = lf.filter(...).select(...).collect()
    ```
    
    **Parquet (recommended for performance):**
    ```python
    df = pl.read_parquet("file.parquet")
    df.write_parquet("output.parquet")
    ```
    
    **JSON:**
    ```python
    df = pl.read_json("file.json")
    df.write_json("output.json")
    ```
    
    For comprehensive I/O documentation, load `references/io_guide.md`.
    
    ## Transformations
    
    ### Joins
    Combine DataFrames:
    ```python
    # Inner join
    df1.join(df2, on="id", how="inner")
    
    # Left join
    df1.join(df2, on="id", how="left")
    
    # Join on different column names
    df1.join(df2, left_on="user_id", right_on="id")
    ```
    
    ### Concatenation
    Stack DataFrames:
    ```python
    # Vertical (stack rows)
    pl.concat([df1, df2], how="vertical")
    
    # Horizontal (add columns)
    pl.concat([df1, df2], how="horizontal")
    
    # Diagonal (union with different schemas)
    pl.concat([df1, df2], how="diagonal")
    ```
    
    ### Pivot and Unpivot
    Reshape data:
    ```python
    # Pivot (wide format): on= is the column whose values become new columns
    df.pivot("product", index="date", values="sales")
    
    # Unpivot (long format)
    df.unpivot(index="id", on=["col1", "col2"])
    ```
    
    For detailed transformation examples, load `references/transformations.md`.
    
    ## Pandas Migration
    
    Polars offers significant performance improvements over pandas with a cleaner API. Key differences:
    
    ### Conceptual Differences
    - **No index**: Polars uses integer positions only
    - **Strict typing**: No silent type conversions
    - **Lazy evaluation**: Available via LazyFrame
    - **Parallel by default**: Operations parallelized automatically
    
    ### Common Operation Mappings
    
    | Operation | Pandas | Polars |
    |-----------|--------|--------|
    | Select column | `df["col"]` | `df.select("col")` |
    | Filter | `df[df["col"] > 10]` | `df.filter(pl.col("col") > 10)` |
    | Add column | `df.assign(x=...)` | `df.with_columns(x=...)` |
    | Group by | `df.groupby("col").agg(...)` | `df.group_by("col").agg(...)` |
    | Window | `df.groupby("col").transform(...)` | `df.with_columns(...).over("col")` |
    
    ### Key Syntax Patterns
    
    **Pandas sequential (slow):**
    ```python
    df.assign(
        col_a=lambda df_: df_.value * 10,
        col_b=lambda df_: df_.value * 100
    )
    ```
    
    **Polars parallel (fast):**
    ```python
    df.with_columns(
        col_a=pl.col("value") * 10,
        col_b=pl.col("value") * 100,
    )
    ```
    
    For comprehensive migration guide, load `references/pandas_migration.md`.
    
    ## Best Practices
    
    ### Polars 1.x API notes (renamed since 0.x)
    These older names appear in stale tutorials and LLM training data; use the 1.x form:
    - `read_csv(dtypes=...)` -> `read_csv(schema_overrides=...)`
    - `pl.Utf8` -> `pl.String`
    - `pl.NUMERIC_DTYPES` -> `import polars.selectors as cs; cs.numeric()`
    - `join(how="outer")` -> `join(how="full")` (and `how="full"` no longer coalesces keys; pass `coalesce=True` for old behavior)
    - `pivot(columns=...)` -> `pivot(on=...)` (`on` is the first positional arg)
    - `collect(streaming=True)` -> `collect(engine="streaming")`
    - `read_database(connection_uri=...)` -> `read_database_uri(uri=...)`
    
    ### Performance Optimization
    
    1. **Use lazy evaluation for large datasets:**
       ```python
       lf = pl.scan_csv("large.csv")  # Don't use read_csv
       result = lf.filter(...).select(...).collect()
       ```
    
    2. **Avoid Python functions in hot paths:**
       - Stay within expression API for parallelization
       - Use `.map_elements()` only when necessary
       - Prefer native Polars operations
    
    3. **Use the streaming engine to lower peak memory:**
       ```python
       lf.collect(engine="streaming")  # `streaming=True` is deprecated
       ```
    
    4. **Select only needed columns early:**
       ```python
       # Good: Select columns early
       lf.select("col1", "col2").filter(...)
    
       # Bad: Filter on all columns first
       lf.filter(...).select("col1", "col2")
       ```
    
    5. **Use appropriate data types:**
       - Categorical for low-cardinality strings
       - Appropriate integer sizes (i32 vs i64)
       - Date types for temporal data
    
    ### Expression Patterns
    
    **Conditional operations:**
    ```python
    pl.when(condition).then(value).otherwise(other_value)
    ```
    
    **Column operations across multiple columns:**
    ```python
    df.select(pl.col("^.*_value$") * 2)  # Regex pattern
    ```
    
    **Null handling:**
    ```python
    pl.col("x").fill_null(0)
    pl.col("x").is_null()
    pl.col("x").drop_nulls()
    ```
    
    For additional best practices and patterns, load `references/best_practices.md`.
    
    ## Resources
    
    This skill includes comprehensive reference documentation:
    
    ### references/
    - `core_concepts.md` - Detailed explanations of expressions, lazy evaluation, and type system
    - `operations.md` - Comprehensive guide to all common operations with examples
    - `pandas_migration.md` - Complete migration guide from pandas to Polars
    - `io_guide.md` - Data I/O operations for all supported formats
    - `transformations.md` - Joins, concatenation, pivots, and reshaping operations
    - `best_practices.md` - Performance optimization tips and common patterns
    
    Load these references as needed when users require detailed information about specific topics.
    
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related