Skip to content

DataFrame Platforms

BenchBox supports native DataFrame benchmarking alongside traditional SQL database benchmarking. DataFrame platforms execute benchmark queries using native DataFrame APIs rather than SQL, enabling direct performance comparison between different DataFrame libraries.

Supported Benchmarks for DataFrame Execution:

BenchmarkDataFrame SupportQuery Count
TPC-HFull22 queries
TPC-DSFull99 queries
Read PrimitivesFull149 queries (both families)
Write PrimitivesFullINSERT, UPDATE, DELETE, MERGE, BULK_LOAD

These counts describe registered query implementations. SQL/DataFrame parity must be checked for the same data, parameters, scale, and engine. See TPC-DS DataFrame parity for binding and coverage requirements.

Why DataFrame Benchmarking Matters

The Challenge

Modern data engineering teams face a choice between two paradigms:

  1. SQL-based platforms (DuckDB, BigQuery, Snowflake) - Query using SQL strings
  2. DataFrame libraries (Pandas, Polars, PySpark) - Query using native APIs

Traditionally, comparing these approaches required:

  • Writing queries twice (SQL + DataFrame)
  • Custom benchmarking infrastructure
  • Non-standardized measurement methodologies

The BenchBox Solution

BenchBox provides unified TPC-H benchmarking across both paradigms. The first command below runs the queries as SQL on DuckDB. The second runs them through the native Polars DataFrame API:

Terminal window
benchbox run --platform duckdb --benchmark tpch --scale 1
benchbox run --platform polars-df --benchmark tpch --scale 1

Same benchmark. Same scale factor. Same metrics. Different execution paradigm.

Key Benefits

BenefitDescription
Apples-to-apples comparisonCompare SQL vs DataFrame on identical workloads
Library evaluationBenchmark Polars vs Pandas vs PySpark on real analytics queries
Migration assessmentMeasure performance impact of paradigm switches
Optimization validationTest DataFrame API optimizations against SQL baselines

Supported Platforms

BenchBox supports 4 production-ready DataFrame platforms with 3 more in development.

Expression Family (Production-Ready)

Libraries using expression objects and declarative, lazy-evaluation style:

PlatformCLI NameStatusBest For
Polarspolars-dfProduction-readyHigh-performance single-node analytics (recommended)
DataFusiondatafusion-dfProduction-readyArrow-native workflows, Rust performance
PySparkpyspark-dfProduction-readyDistributed big data processing

Pandas Family (Production-Ready)

Libraries using string-based column access and imperative style:

PlatformCLI NameStatusBest For
Pandaspandas-dfProduction-readyData science prototyping, ecosystem compatibility

Coming Soon

Infrastructure is in place for these platforms:

PlatformCLI NameFamilyStatusNotes
Daskdask-dfPandasInfrastructure readyParallel computing
cuDFcudf-dfPandasInfrastructure readyNVIDIA GPU acceleration

Quick Start

Installation

Polars DataFrame is recommended and is a core dependency, so the base BenchBox installation already includes it. Install the other DataFrame platforms as extras:

Terminal window
uv add benchbox --extra pandas
uv add benchbox --extra pyspark
uv add benchbox --extra dataframe-all

Running Your First DataFrame Benchmark

The commands below run TPC-H on Polars, Pandas, and PySpark in turn. The PySpark run uses local mode.

Terminal window
benchbox run --platform polars-df --benchmark tpch --scale 0.01
benchbox run --platform pandas-df --benchmark tpch --scale 0.01
benchbox run --platform pyspark-df --benchmark tpch --scale 0.01

Comparing SQL vs DataFrame

The first command uses the Polars SQL interface (SQL mode). The second uses the Polars expression API (DataFrame mode).

Terminal window
benchbox run --platform polars --benchmark tpch --scale 0.1
benchbox run --platform polars-df --benchmark tpch --scale 0.1

Architecture: Family-Based Design

BenchBox uses a family-based architecture that enables 95%+ code reuse across DataFrame libraries.

The Two Families

Python DataFrame libraries cluster into two syntactic families based on API design:

Pandas Family

Libraries using string-based column access and imperative style:

df = df[df['l_shipdate'] <= cutoff]
result = df.groupby(['l_returnflag', 'l_linestatus']).agg({
'l_quantity': 'sum',
'l_extendedprice': 'sum'
})

Members: Pandas, cuDF, Dask, Vaex

Expression Family

Libraries using expression objects and declarative style:

result = (
df.filter(col('l_shipdate') <= lit(cutoff))
.group_by('l_returnflag', 'l_linestatus')
.agg(
col('l_quantity').sum().alias('sum_qty'),
col('l_extendedprice').sum().alias('sum_base_price')
)
)

Members: Polars, PySpark, DataFusion

Why Family-Based Works

Libraries within each family are intentionally API-compatible:

  • cuDF and Dask - designed around pandas-compatible APIs
  • PySpark, DataFusion - share expression-based conceptual model with Polars

Result: Write query once per family, run on multiple platforms.

Platform Configuration

Polars DataFrame (polars-df)

Terminal window
benchbox run --platform polars-df --benchmark tpch --scale 1 \
--execution-engine streaming \
--platform-option rechunk=true
Engine valueMeaning
defaultEach Polars version’s own default
autoPolars chooses the engine for each query
in-memoryMaterialize each query in memory
streamingProcess each query in batches to bound memory

With default, BenchBox passes no engine argument to collect(); the result still records the engine Polars applied. The write_primitives benchmark runs its timed DataFrame operations on DuckDB, so it accepts only default and fails before any work for another engine.

OptionDefaultDescription
rechunktrueRechunk data for better memory layout
n_rows-Limit rows to read (for testing)

Pandas DataFrame (pandas-df)

Terminal window
benchbox run --platform pandas-df --benchmark tpch --scale 1 \
--platform-option dtype_backend=pyarrow
OptionDefaultDescription
dtype_backendnumpy_nullableBackend for nullable dtypes (numpy, numpy_nullable, pyarrow)

PySpark DataFrame (pyspark-df)

Terminal window
benchbox run --platform pyspark-df --benchmark tpch --scale 1 \
--platform-option driver_memory=8g \
--platform-option shuffle_partitions=8
OptionDefaultDescription
masterlocal[*]Spark master URL
driver_memory4gMemory for driver process
shuffle_partitionsCPU countPartitions for shuffle operations
enable_aqetrueEnable Adaptive Query Execution

See PySpark DataFrame Platform for detailed configuration options.

Scale Factor Guidelines

DataFrame platforms have memory constraints. Recommended scale factors:

PlatformMax Recommended SFMemory Required (SF=1)Notes
Pandas10~6 GBEager evaluation, high memory
Polars100~4 GBLazy evaluation, efficient
Dask100+ConfigurableDisk spillover supported
cuDF1-10GPU VRAMLimited by GPU memory
PySpark1000+Cluster memoryDistributed processing

Supported Benchmarks

TPC-H (22 queries)

All 22 TPC-H queries have DataFrame implementations for both expression and pandas families.

Read Primitives (149 queries)

The Read Primitives benchmark provides comprehensive DataFrame support:

  • Expression Family: 149 query implementations (Polars, PySpark, DataFusion)
  • Pandas Family: 149 query implementations (Pandas, Dask, cuDF)
  • Categories: aggregation, filter, groupby, orderby, window, qualify, broadcast, string, and more
Terminal window
benchbox run --platform polars-df --benchmark read-primitives --scale 0.01

See Read Primitives for full documentation.

Write Primitives (DataFrame Write Operations)

Write operations on DataFrame platforms are supported via the DataFrameWriteOperationsManager:

PlatformINSERTUPDATEDELETEMERGEBULK_LOAD
Polars✅✅✅✅✅
Pandas✅❌❌❌✅
PySpark✅✅*✅*✅*✅

*PySpark requires Delta Lake table format for row-level operations.

See Write Primitives for full documentation.

Query Implementation Details

BenchBox implements all 22 TPC-H queries for DataFrame platforms:

Expression Family Example (Q1)

def q1_expression_impl(ctx: DataFrameContext) -> Any:
lineitem = ctx.get_table("lineitem")
col, lit = ctx.col, ctx.lit
cutoff_date = date(1998, 9, 2)
result = (
lineitem.filter(col("l_shipdate") <= lit(cutoff_date))
.group_by("l_returnflag", "l_linestatus")
.agg(
col("l_quantity").sum().alias("sum_qty"),
col("l_extendedprice").sum().alias("sum_base_price"),
(col("l_extendedprice") * (lit(1) - col("l_discount")))
.sum().alias("sum_disc_price"),
)
.sort("l_returnflag", "l_linestatus")
)
return result

Pandas Family Example (Q1)

def q1_pandas_impl(ctx: DataFrameContext) -> Any:
lineitem = ctx.get_table("lineitem")
cutoff = pd.to_datetime("1998-12-01") - pd.Timedelta(days=90)
filtered = lineitem[lineitem["l_shipdate"] <= cutoff]
result = (
filtered
.groupby(["l_returnflag", "l_linestatus"], as_index=False)
.agg({
"l_quantity": ["sum", "mean"],
"l_extendedprice": ["sum", "mean"],
})
.sort_values(["l_returnflag", "l_linestatus"])
)
return result

Execution Differences: SQL vs DataFrame

AspectSQL ModeDataFrame Mode
Query representationSQL stringsNative API calls
OptimizationDatabase query plannerLibrary-specific (lazy if available)
Type checkingRuntime (query execution)Some compile-time (IDE support)
ComposabilityCTEs, subqueriesMethod chaining, intermediate variables
DebuggingEXPLAIN plansStep-through execution

Performance Characteristics

Polars DataFrame

Strengths:

  • Lazy evaluation with query optimization
  • Excellent memory efficiency
  • Parallel execution by default
  • Fast file scanning with predicate pushdown

Best for:

  • Medium to large datasets (up to ~100GB)
  • Complex analytical queries
  • Memory-constrained environments

Pandas DataFrame

Strengths:

  • Familiar API for Python developers
  • Extensive ecosystem
  • Good for prototyping

Best for:

  • Smaller datasets (up to ~10GB)
  • Quick iteration
  • Compatibility with existing codebases

Available Databases

Terminal window
benchbox profile
python -c "from benchbox.platforms.dataframe import format_platform_status_table; print(format_platform_status_table())"

Example output:

DataFrame Platform Status
============================================================
Platform Family Available Version
------------------------------------------------------------
Pandas pandas ✓ 3.0.5
Polars expression ✓ 1.15.0
Dask pandas ✗ N/A
PySpark expression ✗ N/A
DataFusion expression ✓ 43.0.0
------------------------------------------------------------
Available: 3/6 platforms

DataFrame Tuning

BenchBox provides a comprehensive tuning system for DataFrame platforms that allows you to optimize runtime performance based on your system profile and workload characteristics.

Quick Start with Tuning

Terminal window
benchbox run --platform polars-df --benchmark tpch --scale 1 --tuning auto
benchbox run --platform polars-df --benchmark tpch --tuning tuned
benchbox run --platform polars-df --benchmark tpch --tuning ./my_tuning.yaml

--tuning tuned loads examples/tunings/dataframe/<platform>_optimized.yaml for Polars, pandas and cuDF, relative to the current directory, and records the run as mode tuned. Dask and DataFusion ship no such file, so tuned falls back to the system-profile defaults that --tuning auto uses, prints a warning and a line that name what the fallback applies (“engine runtime defaults”), and records mode tuned-fallback.

CLI Commands

Terminal window
benchbox tuning defaults --platform polars
benchbox tuning init --platform polars --output polars_tuning.yaml
benchbox tuning validate polars_tuning.yaml --platform polars
benchbox tuning platforms

Tuning Configuration Options

DataFrame tuning is organized into configuration categories:

Parallelism Settings

SettingTypeDefaultApplicable PlatformsDescription
thread_countintautoPolarsNumber of threads (POLARS_MAX_THREADS)
worker_countintautoDaskNumber of worker processes
threads_per_workerintautoDaskThreads per worker process

Memory Settings

SettingTypeDefaultApplicable PlatformsDescription
memory_limitstrNoneDaskMemory limit per worker (e.g., "4GB", "2GiB")
chunk_sizeintNonePolars, Pandas, DaskSize of chunks for streaming/batched operations
spill_to_diskboolfalseDask, cuDFEnable spilling to disk when memory is exhausted
spill_directorystrNoneDaskDirectory for spill files (None = temp directory)
rechunk_after_filterbooltruePolarsRechunk data after filter operations for better memory layout

Execution Settings

SettingTypeDefaultApplicable PlatformsDescription
streaming_modeboolfalsePolarsEnable streaming execution for memory efficiency
engine_affinitystrNonePolarsPreferred execution engine: "streaming" or "in-memory". To let Polars choose the engine for each query, use --execution-engine auto instead
lazy_evaluationbooltruePolars, DaskEnable lazy evaluation where supported
collect_timeoutintNoneAll lazy platformsMaximum seconds for collect/compute operations

Data Type Settings

SettingTypeDefaultApplicable PlatformsDescription
dtype_backendstr"numpy_nullable"Pandas and DaskBackend for nullable dtypes: "numpy", "numpy_nullable", or "pyarrow"
enable_string_cacheboolfalsePolars, PandasEnable global string caching for categoricals
auto_categorize_stringsboolfalsePandasAuto-convert low-cardinality strings to categoricals
categorical_thresholdfloat0.5PandasUnique ratio threshold for auto-categorization (0.0-1.0)

I/O Settings

SettingTypeDefaultApplicable PlatformsDescription
memory_poolstr"default"AllMemory allocator for Arrow: "default", "jemalloc", "mimalloc", "system"
memory_mapboolfalsePandas and DaskUse memory-mapped files for reading
pre_bufferbooltruePandas, DaskPre-buffer data during file reads
row_group_sizeintNonePolars, Pandas, cuDFRow group size for Parquet writing

Write Settings

SettingTypeDefaultApplicable PlatformsDescription
sort_bylist[object|str][]All DataFrame platformsSort columns before write for better compression and scan locality
partition_bylist[object|str][]Dask, PySparkHive-style partitioned output directories
row_group_sizeintNonePolars, Pandas, cuDFParquet row group sizing for write path
target_file_size_mbintNonePlatform-dependentPreferred output file size target
repartition_countintNoneDask, PySparkNumber of output files/partitions
compressionstr"zstd"All DataFrame platformsParquet compression codec (none, snappy, gzip, zstd, lz4, brotli)
compression_levelintNoneCodec-dependentCompression level for codecs that support levels
dictionary_columnslist[str][]All DataFrame platformsForce dictionary encoding on selected columns
skip_dictionary_columnslist[str][]All DataFrame platformsDisable dictionary encoding for selected columns
data_page_version"1.0"|"2.0"null (PyArrow default 1.0)All Parquet write pathsParquet data page serialization version

GPU Settings (cuDF)

SettingTypeDefaultApplicable PlatformsDescription
enabledboolfalsecuDFEnable GPU acceleration
device_idint0cuDFCUDA device ID to use (0-indexed)
spill_to_hostbooltruecuDFSpill GPU memory to host RAM when exhausted
pool_typestr"default"cuDFRMM memory pool type: "default", "managed", "pool", "cuda"

Example Tuning Configurations

Polars - SF10+ Dataset Optimization

_metadata:
version: "1.0"
platform: polars
description: "Optimized settings for large TPC-H workloads"
parallelism:
thread_count: 8
execution:
streaming_mode: true
engine_affinity: streaming
lazy_evaluation: true
memory:
chunk_size: 100000
rechunk_after_filter: true
io:
memory_pool: jemalloc

Pandas - Memory-Efficient Processing

_metadata:
version: "1.0"
platform: pandas
description: "Memory-efficient Pandas configuration"
data_types:
dtype_backend: pyarrow
auto_categorize_strings: true
categorical_threshold: 0.3
io:
memory_map: true
pre_buffer: false

Dask - Distributed Workload

_metadata:
version: "1.0"
platform: dask
description: "Distributed Dask configuration"
parallelism:
worker_count: 4
threads_per_worker: 2
memory:
memory_limit: "4GB"
spill_to_disk: true
spill_directory: /tmp/dask-spill
execution:
lazy_evaluation: true

cuDF - GPU Acceleration

_metadata:
version: "1.0"
platform: cudf
description: "GPU-accelerated cuDF configuration"
gpu:
enabled: true
device_id: 0
spill_to_host: true
pool_type: managed

Smart Defaults

When you use --tuning auto, BenchBox detects your system profile and applies appropriate settings:

System ProfileMemoryBase Memory Settings (Before Platform Overrides)
Very Low<4GBstreaming_mode=true; chunk_size=50K
Low4GB to <8GBstreaming_mode=true; chunk_size=100K
Medium8GB to <32GBchunk_size=500K
High32GB or morestreaming_mode=false
GPU AvailableN/AGPU enabled with managed pool (cuDF)

System Detection

BenchBox automatically detects:

  • CPU cores: Used to set thread/worker counts
  • Available RAM: Determines memory category and chunk sizes
  • GPU presence: Enables CUDA-based acceleration for cuDF
  • GPU memory: Sets spill thresholds and pool types

Platform-Specific Smart Defaults

Polars:

  • Smart defaults no longer select an execution engine: --tuning auto and --tuning tuned leave the engine at default on every host. Pass --execution-engine streaming to stream explicitly.
  • Low memory: chunk_size=50000; high memory: lazy_evaluation=true

Pandas:

  • Low memory: dtype_backend="numpy_nullable", memory_map=true
  • Medium+ memory: dtype_backend="pyarrow" (2-5x faster for aggregations)

Dask:

  • Workers set to CPU cores / 2
  • Threads per worker: 2
  • Memory limit per worker: available RAM / workers

cuDF:

  • Small GPU (<8GB): pool_type="managed", spill_to_host=true
  • Large GPU (≥8GB): pool_type="pool", spill_to_host=false

Tuning Validation

BenchBox validates your tuning configuration and reports issues at three levels:

LevelDescriptionExample
ERRORInvalid configuration that will failInvalid engine_affinity value
WARNINGSuboptimal or conflicting settingsstreaming_mode=true with engine_affinity="in-memory"
INFOSuggestions for improvementStreaming mode without chunk_size set

Validate your configuration before running. The output lists any issues and suggestions:

Terminal window
benchbox tuning validate my_config.yaml --platform polars
✓ Configuration valid
⚠ WARNING: streaming_mode enabled without chunk_size - consider setting chunk_size
ℹ INFO: Consider enabling lazy_evaluation for improved efficiency

Troubleshooting

Platform Not Available

ValueError: Unknown DataFrame platform: polars-df

Solution: Ensure you’re using a supported platform name with the -df suffix.

Memory Errors with Pandas

MemoryError: Unable to allocate array

Solutions:

  1. Reduce scale factor: --scale 0.1
  2. Switch to Polars: --platform polars-df
  3. Use PyArrow backend: --platform-option dtype_backend=pyarrow

Slow Performance with Large Datasets

For scale factors > 10:

  1. Polars: Run with the streaming engine

    Terminal window
    --execution-engine streaming
  2. Pandas: Consider switching to Polars or Dask

API Reference

DataFrameContext Protocol

The context provides table access and expression helpers. ctx.col builds column references and ctx.lit builds literal values (the expression builders for the expression family):

from benchbox.core.dataframe import DataFrameContext
df = ctx.get_table("lineitem")
col = ctx.col
lit = ctx.lit

DataFrameQuery Class

Query definition with dual-family implementations:

from benchbox.core.dataframe import DataFrameQuery, QueryCategory
query = DataFrameQuery(
query_id="Q1",
query_name="Pricing Summary Report",
category=QueryCategory.TPCH,
expression_impl=q1_expression_impl,
pandas_impl=q1_pandas_impl,
)