Skip to main content

DuckPD mascot - a duck dressed as a panda

DuckPD 🦆❤️🐼

DuckPD is DuckDB dressed as a pandas DataFrame.

Python 3.11+ License: MIT DuckDB Powered Narwhals Compliant

DuckPD is a lazy, out-of-core DataFrame library with a familiar pandas-shaped API and DuckDB as its high-performance analytical execution engine. Write intuitive pandas code; execute at DuckDB speed across 100 GB+ datasets with bounded memory and zero OOM crashes.


⚡ Why DuckPD? The Core Advantages

  • 🚫 Never OOM on Large Datasets: Stream, filter, join, and aggregate multi-gigabyte or multi-terabyte datasets within a bounded memory footprint (e.g. 2 GB RAM budget) using DuckDB's vectorized execution and disk spillover.
  • 📈 Zero-Materialization Grouped Rolling Windows: Compute grouped, multi-entity rolling/expanding statistics (e.g., 20-day vs 50-day moving average crossovers across thousands of stock tickers) and assign them directly back to your dataframe without materializing intermediate tables.
  • 🌐 Federated Remote Queries: Query HTTP/S3/GCS Parquet and attach PostgreSQL, MySQL, and SQLite databases directly into lazy pandas pipelines—with full credential redaction and scan guardrails.
  • 🛡️ Zero Silent Fallbacks: If an operation is unsupported or ordering is ambiguous, DuckPD fails explicitly before query execution. Your dataset will never be silently materialized into in-memory pandas.
  • 🔌 Native Narwhals Lazy Backend: Drop DuckPD directly into modern visualization and machine learning libraries (Plotly, Altair, etc.) via nw.from_native(df) for zero-copy, lazy DuckDB execution.
  • 🔍 Deep Observability: Inspect physical plans, optimizer pushdown, operator timings, peak RSS, and DuckDB spill metrics with df.explain(), df.explain_write(), and df.profile().

🚀 Quickstart & Example Workflows

1. Complex Feature Engineering & Aggregations Per Ticker

Compute rolling indicators and aggregate summary statistics per group/ticker without materializing intermediate tables:

import duckpd as pd

# Lazy Parquet scan with explicit multi-column ordering
prices = pd.read_parquet(
    "price-data/*.parquet",
    order_by=["date", "ticker"],
)

# 1. Grouped rolling indicators computed per ticker (aligned to source rows)
features = prices.assign(
    return_pct=lambda df: df.groupby("ticker")["close"].pct_change(),
    fast_ma=lambda df: df.groupby("ticker")["close"].rolling(20).mean(),
    slow_ma=lambda df: df.groupby("ticker")["close"].rolling(50).mean(),
).assign(ma_cross=lambda df: df["fast_ma"] > df["slow_ma"])

# 2. Groupby aggregations per ticker
ticker_summary = (
    features.groupby("ticker", as_index=False)
    .agg(
        avg_daily_return=("return_pct", "mean"),
        max_high=("high", "max"),
        min_low=("low", "min"),
        total_volume=("volume", "sum"),
        crossover_signals=("ma_cross", "sum"),
    )
    .sort_values("total_volume", ascending=False)
)

# Inspect query plan without executing
print(ticker_summary.explain("optimized"))

# Materialize a bounded preview in memory or stream full result straight to Parquet
preview = ticker_summary.head(10)  # Bounded pandas DataFrame
features.write_parquet("price-features.parquet")  # Direct zero-copy DuckDB sink

2. Federated Cloud Parquet & Remote Databases

Join remote cloud Parquet datasets with live PostgreSQL/MySQL reporting tables in a single lazy pipeline:

import os
import duckpd as pd

with pd.connect(memory_limit="2GB") as session:
    # Read private S3 Parquet using scoped temporary secrets
    session.create_s3_secret(
        "analytics",
        key_id=os.environ["AWS_ACCESS_KEY_ID"],
        secret=os.environ["AWS_SECRET_ACCESS_KEY"],
        region="us-east-1",
        scope="s3://company-warehouse/",
    )
    events = pd.read_parquet(
        "s3://company-warehouse/clickstream/*.parquet", session=session
    )

    # Attach PostgreSQL read-only without leaking credentials in logs or reprs
    warehouse = session.attach_postgres(
        "warehouse",
        host=os.environ["PGHOST"],
        database=os.environ["PGDATABASE"],
        user=os.environ["PGUSER"],
        password=os.environ["PGPASSWORD"],
        unbounded_scan="allow",
    )
    users = warehouse.table("users", order_by="user_id")

    # Lazy relational join and aggregation
    active_user_metrics = (
        events.merge(users, on="user_id")
        .groupby(["country", "subscription_tier"], as_index=False)
        .agg(total_events=("event_id", "count"), total_revenue=("amount", "sum"))
        .sort_values("total_revenue", ascending=False)
    )

    # Collect result into pandas or export to CSV
    top_metrics = active_user_metrics.head(50)

3. Native Narwhals Lazy Interoperability

Use DuckPD seamlessly inside libraries that support Narwhals without collecting to pandas:

import narwhals as nw
import duckpd as pd

df = pd.read_parquet("data.parquet", order_by="id")
lazy_df = nw.from_native(df)

# Narwhals transforms execute lazily inside DuckDB
transformed = lazy_df.with_columns(
    z_score=(nw.col("value") - nw.col("value").mean()) / nw.col("value").std()
).filter(nw.col("z_score") > 2.0)

# Collect cleanly as Arrow or pandas when ready
arrow_table = nw.to_native(transformed).to_arrow()

📊 Benchmarks: DuckPD vs. pandas

DuckPD is benchmarked across dataset sizes from 5 MB to 50 GB on standard single-node machines.

Dataset Size Pandas Execution Time DuckPD Execution Time Pandas Peak RSS DuckPD Peak RSS DuckPD Heap Traced Result
500 MB 1.84s 0.28s (6.5x faster) ~4.8 GB ~180 MB ~358 KB Complete
5 GB 19.6s 2.10s (9.3x faster) ~38 GB ~850 MB ~358 KB Complete
50 GB 💥 OOM Crash (>250 GB req.) 18.4s (Out-of-Core) 💥 Out of Memory < 2 GB (Spill Bounded) ~358 KB Zero OOM

Run benchmarks locally:

make benchmark          # Fast everyday suite (5mb, 50mb, 500mb)
make benchmark-all      # Exhaustive stress test (up to 50gb out-of-core)

See the complete Benchmark Report and Detailed Benchmarks.


🧭 Project Architecture & Contracts

DuckPD's core design philosophy is rooted in correctness, transparency, and relational rigor:

  • Explicit Execution Boundaries: Transformations build typed immutable logical plans. Execution occurs strictly at intentional boundaries: collect(), head(n), to_arrow(), to_arrow_batches(), write_parquet(), write_csv(), save_as_table(), and commit().
  • Honest Ordering & Relational Row Identity: Pandas assumes physical in-memory row order. DuckPD tracks hidden relational identity metadata so positional .iloc, MultiIndex .loc, rank ties, and window operations are completely deterministic—failing with UnorderedOperationError only when source order is truly undefined.
  • Transactional Parquet Commits: Update local Parquet files in-place with df.commit(), featuring automated schema validation, conflict detection, and atomic staging replacement (os.replace).

For in-depth architectural specifications and design decisions:


📚 Demos & Walkthroughs

Check out the runnable tutorials and interactive notebooks in demo/:

Runnable pipelines:

uv run python demo/basic_pipeline.py
uv run python demo/parquet_pipeline.py
uv run python demo/reduction_pipeline.py
uv run python demo/generate_market_data.py
uv run python demo/market_data_demo.py smoke

🛠️ Development & Quality Gate

DuckPD uses uv for fast, reproducible Python environment management.

# Set up dependencies
uv sync --frozen --group dev

# Run quality gate
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv build

Or using Make:

make check
make build

📄 License & Documentation

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

duckpd-0.1.3.tar.gz (1.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

duckpd-0.1.3-py3-none-any.whl (110.6 kB view details)

Uploaded Python 3

File details

Details for the file duckpd-0.1.3.tar.gz.

File metadata

  • Download URL: duckpd-0.1.3.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for duckpd-0.1.3.tar.gz
Algorithm Hash digest
SHA256 cb5734d8e322c848f349fbef4bc3f14b6ca99afa4a7d8819171a1d7d2c6c9dde
MD5 c39ae770aadf7e9fa80060f587fdc531
BLAKE2b-256 cab869a94bb9a645cf8cef525a66c4fcd7decb3432b16b4de68883a2fca530e1

See more details on using hashes here.

File details

Details for the file duckpd-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: duckpd-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 110.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for duckpd-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 b2bbffcecc56a3e0159bc72c0188683396ba5929f0a3470e53e20895ae0d4d32
MD5 854caf076e01d7211e1b727a120a3a12
BLAKE2b-256 b6fb834d8f6e3bb29d717de51e289b5c81b2aea517bf8f866cac002ac25fa1de

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.4

2 files

This release

0.1.3 This release

2 files

0.1.2

2 files

0.1.1

2 files

0.0.7

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page