Leilei Zhou, Ph.D.
Leilei Zhou, Ph.D.
Back to all articles
#Data Engineering#DuckDB#Spark#Python#High Performance

Scaling Financial Panel Data: DuckDB, Apache Spark, and Out-of-Core Processing

Engineering data pipelines that process 20+ years of panel data across 50M+ facilities without exhausting memory limits.

Leilei Zhou, Ph.D.
Leilei Zhou, Ph.D.
Lead Quantitative Researcher
August 12, 20246 min read
Scaling Financial Panel Data: DuckDB, Apache Spark, and Out-of-Core Processing

Quantitative research pipelines frequently encounter out-of-core memory bottlenecks when analyzing longitudinal panel datasets spanning decades. Storing and calculating rolling statistics across 50+ million credit and facility records in naive in-memory Pandas dataframes will immediately trigger Out-Of-Memory (OOM) failures.


The Modern Quantitative Data Architecture

By pairing DuckDB for vectorized single-node SQL operations with Apache Spark for distributed partition execution, we achieve orders-of-magnitude performance gains:

  1. Columnar Pruning & Parquet Pushdown: Queries only read the exact feature columns required for model inputs, eliminating 80% of disk I/O.
  2. Lazy Evaluation: Transformations are recorded as logical plans and executed only when computing final statistics or writing to disk.
  3. NumPy Vectorization: Custom rolling covariance and matrix factorizations run in compiled C/vector registers rather than Python iteration loops.
import duckdb

def compute_rolling_spread_metrics(parquet_dir: str):
    con = duckdb.connect()
    # Leverage DuckDB vector engine directly on partitioned Parquet
    query = """
    SELECT 
        facility_id,
        as_of_date,
        credit_spread_bps,
        AVG(credit_spread_bps) OVER (
            PARTITION BY facility_id 
            ORDER BY as_of_date 
            ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
        ) as rolling_60d_mean,
        STDDEV(credit_spread_bps) OVER (
            PARTITION BY facility_id 
            ORDER BY as_of_date 
            ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
        ) as rolling_60d_vol
    FROM read_parquet(?)
    WHERE as_of_date >= '2004-01-01'
    """
    return con.execute(query, [f"{parquet_dir}/*.parquet"]).df()

This approach reduced monthly research pipeline runtimes from 8 hours to under 25 minutes while maintaining strict determinism.

Enjoyed this article?

Give it a heart or share your thoughts in the comment section below!

Discussion on this Post

0

Have a question or response to this article? Leave a comment or reply below.

Avatar
Markdown formatting supported0/2000