Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Analysis with Python (pandas, polars, DuckDB)  Module 14 – Performance and scale

pandas, polars or DuckDB: a measured guide

Choose between pandas, polars and DuckDB with measured numbers: where the data lives, six everyday tasks, mixing engines through Arrow and a team default.

  • Intermediate
  • 40 minutes
  • Examples run with Python 3.14.8, pandas 3.0.6, numpy 2.5.3, pyarrow 25.0.1, polars 1.44.2 and duckdb 1.5.6
  • By MySmartCoPilot
Free core, Pro depth: 13 of 40 minutes free
Free: Engine defaults include indexing and missing-value rules · Measure where the same data is read from · A decision record joins workload, evidence and consumers. With a pass: 3 in-depth parts and 3 interview answers, about 27 minutes.

What you will learn

  • Compare pandas, polars and DuckDB on speed, memory, syntax and ecosystem for typical tasks
  • Read a benchmark critically, checking hardware, versions, data shape, warm-up and what is timed
  • Combine engines in one workflow through Arrow and DuckDB replacement scans
  • Choose a team default and write down the reasons and the exceptions

Before you start

For the original million order lines, choose by where the data lives and what happens to it next. pandas is the default for data that fits comfortably in memory and for work that ends in charts, statistics or models, because most Python data libraries accept its DataFrames. polars is a faster DataFrame engine for pipelines of many steps: it can parallelise supported operators, plans lazy queries and can stream supported larger-than-memory workloads subject to operator and state limits. DuckDB is an SQL database that runs inside your Python process: it queries CSV and Parquet files directly, joins and aggregates data bigger than memory, and suits anyone who thinks in SQL. A data hand-off contract: Requirements for values, keys, types, missingness, metadata and result ownership when data moves between libraries or representations. states what must survive conversion. All three exchange data through Apache Arrow, so one workflow can use each where it is strongest. Measured on the same 1,000,000 order lines, the fastest engine changed with where the data lived, so treat any single ranking with suspicion.

Engine defaults include indexing and missing-value rules

pandas, polars and DuckDB
Criterion pandaspolarsDuckDB
What it is A DataFrame library with a row indexA DataFrame library built on Arrow memoryAn SQL database inside the Python process
How you write it Methods on DataFrames and SeriesExpressions, run at once or as a lazy querySQL, or its Python relational API
Cores it uses Many operations use one threadA thread pool for supported workConfigured parallel workers for supported work
Data bigger than memory By hand, in chunksA streaming engine for lazy queriesSpills to disk within a memory limit
Missing values NaN, or pd.NA in nullable dtypesnull, which is not the same as NaNNULL, which is not the same as NaN
Strongest at Working with plotting, statistics and modelling librariesLong transformation pipelinesFiles, joins and people who write SQL

The table hides two differences that change code, not just speed. pandas aligns rows by their index, so adding two Series matches labels; polars and DuckDB have no index and work by position or by explicit joins. pandas treats NaN as missing, while polars and DuckDB keep NaN as an ordinary floating-point value and use null for missing data, so a file holding both can give each engine a different answer. The Polars guide describes pandas as largely single-threaded and polars as built for many cores on one machine, and DuckDB uses as many threads as the machine has cores unless told otherwise.

Each engine also has a case where it is the wrong first choice. pandas is the wrong start for a file several times larger than memory, because it loads whole tables before it computes anything. polars is a poor fit when every step ends in a library that accepts only pandas, since each hand-over costs a conversion. DuckDB is awkward for logic written row by row or around an index, and a loop of small queries over a DataFrame pays the cost of taking the DataFrame in on every call.

Distinguish index alignment, explicit keys and missing-value conversion before comparing engine outputs.

Interactive · stage by stage Engine defaults include indexing and missing-value rules Distinguish index alignment, explicit keys and missing-value conversion before comparing engine outputs.
Engine defaults include indexing and missing-value rules, step 3 of 3. Showing pandas labels, Explicit keys, Missingness boundary. In focus: Missingness boundary.pandas labelsIndex alignmentExplicit keysPolars and SQLMissingness boundaryNative NaN differs from null

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. pandas operations can align values by row labels. A port needs to make that identity explicit.
  2. The demonstrated polars and SQL operations use columns or row positions rather than a pandas index.
  3. A native NaN can remain a value in polars/SQL, while the default pandas conversion can produce null. Inspect the constructed input and conversion method.

Checkpoint

Check: Engine defaults include indexing and missing-value rules

  1. Question 1 of 2 What should a cross-engine port preserve?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The required key, type and missingness contracts

    A matching row count cannot establish correct values, identities or representation. Counts can match even when key alignment or missing-value meaning differs.

  2. Question 2 of 2 A frame has four explicit order labels 10,11,12,13. How many labels must an identity-preserving round trip account for?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4 labels

    The contract includes all four original order labels.

Measure where the same data is read from

make_lines.py: the seeded order lines of this lesson Python · make_lines.py
# Writes the seeded order lines this lesson's examples use (never downloaded): lines.parquet and lines.csv with
# line_id, order_date, country (40 values), product (4,000 codes), quantity and price, and products.parquet with
# each product's category.
import numpy as np
import pyarrow as pa
import pyarrow.csv as pacsv
import pyarrow.parquet as pq

CATEGORIES = ["grocery", "home", "toys", "books", "garden", "beauty", "sport", "kitchen", "office", "pets", "music", "travel"]


def make_lines(rows=1_000_000, seed=3, csv=False):
    rng = np.random.default_rng(seed)
    products = np.array([f"P{i:04d}" for i in range(4000)])
    lines = pa.table(
        {
            "line_id": np.arange(1, rows + 1),
            "order_date": pa.array((np.datetime64("2025-04-01") + rng.integers(0, 365, rows).astype("timedelta64[D]")).astype("datetime64[us]")),
            "country": pa.array(np.array([f"Country {i:02d}" for i in range(40)])[rng.integers(0, 40, rows)]),
            "product": pa.array(products[rng.integers(0, 4000, rows)]),
            "quantity": rng.integers(1, 20, rows),
            "price": rng.integers(50, 5000, rows) / 100,
        }
    )
    pq.write_table(lines, "lines.parquet")
    if csv:
        pacsv.write_csv(lines, "lines.csv")
    pq.write_table(pa.table({"product": products, "category": np.array(CATEGORIES)[rng.integers(0, 12, 4000)]}), "products.parquet")
Revenue by country from 1,000,000 lines, five ways Python · where_data_lives.py
# Revenue by country from 1,000,000 order lines, five ways: each engine on its own in-memory table, each reading
# the Parquet file, and DuckDB querying the pandas DataFrame. One warm-up run, then the median of 5 runs.
import math
import os
import statistics
import time

import duckdb
import pandas as pd
import polars as pl

from make_lines import make_lines

make_lines()
pandas_df = pd.read_parquet("lines.parquet")
polars_df = pl.read_parquet("lines.parquet")
con = duckdb.connect()
con.execute("SET enable_progress_bar = false")
SQL = "SELECT country, sum(quantity * price) AS revenue FROM {} GROUP BY country"

ways = {
    "pandas, its own DataFrame": lambda: pandas_df.assign(revenue=pandas_df["quantity"] * pandas_df["price"])
    .groupby("country")["revenue"].sum(),
    "polars, its own DataFrame": lambda: polars_df.group_by("country").agg((pl.col("quantity") * pl.col("price")).sum().alias("revenue")),
    "polars, reading Parquet": lambda: pl.scan_parquet("lines.parquet").group_by("country")
    .agg((pl.col("quantity") * pl.col("price")).sum().alias("revenue")).collect(),
    "DuckDB, reading Parquet": lambda: con.execute(SQL.format("'lines.parquet'")).df(),
    "DuckDB, the pandas DataFrame": lambda: con.execute(SQL.format("pandas_df")).df(),
}


def median_ms(func):
    func()  # warm-up, not timed
    times = []
    for _ in range(5):
        start = time.perf_counter()
        func()
        times.append(time.perf_counter() - start)
    return statistics.median(times) * 1000


def as_dict(result):
    if isinstance(result, pd.Series):
        return result.to_dict()
    if isinstance(result, pl.DataFrame):
        return dict(zip(result["country"].to_list(), result["revenue"].to_list()))
    return dict(zip(result["country"], result["revenue"]))


def same(a, b):  # the same countries, and revenues equal up to rounding in the last digits
    return a.keys() == b.keys() and all(math.isclose(a[k], b[k], rel_tol=1e-9) for k in a)


print(f"pandas {pd.__version__}, polars {pl.__version__}, DuckDB {duckdb.__version__}, {os.cpu_count()} CPU cores, {len(pandas_df):,} lines")
answers = []
for label, func in ways.items():
    print(f"{label:<30}{median_ms(func):>8.1f} ms")
    answers.append(as_dict(func()))
print("all five agree on revenue per country:", all(same(a, answers[0]) for a in answers))

Output

pandas 3.0.6, polars 1.44.2, DuckDB 1.5.6, 18 CPU cores, 1,000,000 lines
pandas, its own DataFrame         27.5 ms
polars, its own DataFrame         12.6 ms
polars, reading Parquet           26.8 ms
DuckDB, reading Parquet           16.9 ms
DuckDB, the pandas DataFrame      85.3 ms
all five agree on revenue per country: True

This output changes from run to run: the times change from run to run and from one computer to another

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 where_data_lives.py

All five ways return the same revenue for every country, and the slowest took about seven times as long as the quickest. polars on its own DataFrame was the quickest, using every core for the group-by. pandas on its own DataFrame took about twice as long, still only tens of milliseconds. Reading straight from the Parquet file, DuckDB was quicker than polars. The slowest way was DuckDB querying the pandas DataFrame, the very data pandas had summed in a third of the time: before DuckDB can group the rows, it has to take in the frame’s columns, and pandas 3’s str columns are expensive to take in. The query was the same each time; what changed was where the data lived.

Keep all five original million-row paths and compare their boundary work before using a speed ranking.

Interactive · stage by stage Measure where the same data is read from Keep all five original million-row paths and compare their boundary work before using a speed ranking.
Measure where the same data is read from, step 3 of 3. Showing Original data, Native frame, Replacement scan. In focus: Replacement scan.Original data1,000,000 linesNative frameOwn DataFrameReplacement scanpandas frame into DuckDB

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original million-row fixture is unchanged across all five measured paths.
  2. One path starts from an engine's own frame; another reads Parquet. They include different representation and transfer work.
  3. The original pandas-to-DuckDB path has a larger measured boundary cost on these pins. The ranking is evidence of that workload rather than a universal engine rule.

Checkpoint

Check: Measure where the same data is read from

  1. Question 1 of 2 What can change timing when rows are identical?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The data representation and hand-off path

    The same rows can reach an operator through different scans and conversions. Equivalent results do not imply identical boundary work.

  2. Question 2 of 2 Five original data-location paths are measured. How many pairwise paths exist?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 10 pairs

    Five choose two is 5 times 4 divided by 2, giving ten pairs.

A decision record joins workload, evidence and consumers

An engine choice includes the path into and out of the calculation. The application may need an Arrow table, a plotting frame or a scalar count; those are different timed consumers. State the required keys, types and missingness before choosing a hand-off path, then measure that same path on the relevant data. A convenient API can justify a choice even when a microbenchmark ranks its arithmetic differently.

Choosing an engine: DuckDB for files, SQL and data bigger than memory; polars for fast pipelines; pandas for its ecosystem; Arrow between.Where does the data live,and what happens to it next?In files or a database,larger than memory,or asked about in SQLA Python pipeline ofmany steps on one machine,where speed mattersCharts, statistics, models:libraries that expect pandasDuckDBpolarspandasHand-overs through Apache Arrow:one columnar format all three read

Which engine for which situation

Text description of the diagram

The diagram starts from one question: where does the data live, and what happens to it next? Three answers lead to three engines.

  1. Data in files or a database, larger than memory, or questions asked in SQL: DuckDB.
  2. A Python pipeline of many steps on one machine, where speed matters: polars.
  3. Charts, statistics and models from libraries that expect pandas: pandas.

All three hand data to each other through Apache Arrow, one columnar format that each of them reads.

Four ideas carry the rest of the lesson:

  • Apache Arrow is a columnar memory format that pandas, polars and DuckDB all read, so data can move between them without being rebuilt value by value.
  • Replacement scans let DuckDB query a pandas or polars DataFrame, or an Arrow table, by the name of its Python variable.
  • A benchmark harness runs the same tasks in each engine, discards a warm-up run and reports the median of several runs, next to the versions and the hardware.
  • A team default is the engine new work starts in, with a short written list of the cases that justify another.

Follow the retained diagram from input location to the result consumer and its decision evidence.

Interactive · stage by stage A decision record joins workload, evidence and consumers Follow the retained diagram from input location to the result consumer and its decision evidence.
A decision record joins workload, evidence and consumers, step 3 of 3. Showing Where input lives, Where output goes, Team default. In focus: Team default.Where input livesFiles or framesWhere output goesCharts; models; SQLTeam defaultReasons and exceptions

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The retained decision diagram starts from data location and the operations the application needs.
  2. A small result may be handed to a downstream library. The consumer's required format belongs in the timed task.
  3. A default should record assumptions, measured evidence and exceptions. Arrow enables compatible transfer but not universal zero-copy or unchanged metadata.

Checkpoint

Check: A decision record joins workload, evidence and consumers

  1. Question 1 of 2 What belongs in an engine decision?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Workload, resource and consumer requirements

    The relevant input, operators, output and team constraints define the choice. A single ranking cannot establish the best engine for a different application.

  2. Question 2 of 2 A decision lists three requirements and two exceptions. How many listed items is that?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 5 items

    Three requirements plus two exceptions gives five items.

Six everyday tasks measured, and how to read any benchmark Pro: opens with a Pro, Premium or Ultimate pass

A single number never settles an engine choice. Six everyday tasks are measured here in all three engines on the same data, and three common benchmark traps are reproduced with numbers.

In this part:

  • A harness checks results before interpreting timings
  • Imports and materialisation belong to the timed contract

2 diagrams · 2 runnable examples (Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

One workflow, three engines: what crosses Arrow intact and what changes Pro: opens with a Pro, Premium or Ultimate pass

The recorded Arrow hand-overs cost little here, but supported types, conversions and result size still decide the cost. A three-engine workflow is timed step by step here, and the values and types that change on the way are reproduced.

In this part:

  • A hand-off preserves a declared value and type contract
  • Native NaN, null and index metadata are separate

2 diagrams · 2 runnable examples (Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A team default, decided: eight scenarios and a decision record Pro: opens with a Pro, Premium or Ultimate pass

Most teams do best with one default engine and a short list of exceptions, written down with their reasons. Eight common scenarios are decided here with this lesson’s measurements, followed by a decision record to adapt.

In this part:

  • A scenario recommendation includes its resource assumptions
  • Write the default together with evidence and exceptions
  • Recheck the same fixture after a library change

3 diagrams · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise the preserved performance contracts

Practice that adapts to you

Practise this lesson

7 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 A table has six key-labelled rows. How many identities must a value comparison account for?

    Type a number.

    Show the answer: Question 1 of 7

    Answer: 6 identities

    Each of the six rows contributes its required identity.

  2. Question 2 of 7 Four measured alternatives have how many unordered pairwise comparisons?

    Type a number.

    Show the answer: Question 2 of 7

    Answer: 6 pairs

    Four choose two gives six comparisons.

  3. Question 3 of 7 A decision lists four constraints and three exceptions. How many listed items is that?

    Type a number.

    Show the answer: Question 3 of 7

    Answer: 7 items

    There are seven listed items in total.

  4. Question 4 of 7 30 GB of Parquet files sit on a laptop with 16 GB of memory, and the question needs a join and revenue by month. Which engine fits best?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: DuckDB, querying the files directly

    DuckDB reads the files where they are, needs only the columns the query uses and spills joins and aggregations to disk when memory runs short. read_parquet in pandas or polars loads everything into memory first; Parquet's compression helps on disk, but the decoded columns are larger than 16 GB. A lazy polars scan with the streaming engine would also be a reasonable choice.

  5. Question 5 of 7 In this lesson's run, DuckDB grouped the 1,000,000 lines several times faster from the Parquet file than from the pandas DataFrame that held exactly the same rows. What made the difference?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: Taking in the DataFrame's columns, its text columns above all, before the group-by could start

    The group-by itself was the same query on the same rows, and every way returned the same revenue. What differed was where the data lived: from Parquet, DuckDB reads columns in a format it handles natively, while a pandas DataFrame has to be taken in first, and pandas 3's str columns are expensive to take in.

  6. Question 6 of 7 Which of these statements about polars are true?

    Choose every answer that is right.

    Show the answer: Question 6 of 7

    Answer:

    • It can plan a lazy query before running it
    • It can parallelise supported operations with its configured thread pool
    • Missing values are null, and NaN is an ordinary floating-point value

    polars runs on every core, plans lazy queries before running them, and keeps null (missing) apart from NaN (a floating-point value). It has no row index: rows are identified by their position, and joins match explicit key columns.

  7. Question 7 of 7 A Parquet column holds 1.0, NaN and 3.0. Which engine reports a mean of 2.0 for it, treating NaN as a missing value?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: pandas

    pandas treats NaN as missing, so its mean of the two remaining values is 2.0. polars and DuckDB keep NaN as a floating-point value, so it propagates into the mean, which becomes NaN. Decide at the boundary whether NaN means "missing" in your data, and convert it to null where it does.

Interview questions

  1. Fresher · Warm-up

    What is Apache Arrow, and why does it matter when one workflow uses pandas, polars and DuckDB?

    Apache Arrow is a standard columnar memory format: each column is kept as contiguous buffers of values, with a separate bitmap that marks which values are null, laid out the same way in every program that uses it. polars keeps its data in Arrow memory, DuckDB can read and return Arrow tables, and pandas can store columns in Arrow and convert to and from it. When data moves from one engine to the next as Arrow, columns can often be shared or copied in bulk instead of being rebuilt value by value through Python objects, which is where conversion time usually goes. A shared format is not a shared meaning, though: NaN and null still behave differently in pandas than in polars and DuckDB, so a workflow that mixes engines decides at each boundary what NaN means.

  2. Mid to senior

    A colleague's notebook shows polars running 10 times faster than pandas on "our workload" and proposes moving every report to polars. What do you check before agreeing, and how would you run a fair comparison?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    After DuckDB is added to a pandas 3 pipeline, a query on a 1,000,000-row DataFrame takes about 0.2 seconds per call instead of milliseconds, inside a loop that runs 2,000 times. What is happening, and how do you fix it?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Your analytics team of eight writes pandas notebooks, and data volumes have grown tenfold in a year. Propose a default engine and a policy for exceptions, and say how you would know after six months whether the change worked.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • pandas for data that fits and work that ends in other Python libraries, polars for fast pipelines on one machine, DuckDB for files, SQL and data bigger than memory.
  • The fastest engine depends on where the data lives: the same group-by was quickest in polars on its own frame and slowest when DuckDB had to take in a pandas DataFrame.
  • pandas treats NaN as missing; polars and DuckDB keep NaN as a value and use null for missing data.
  • Arrow is the common format between the three.

Terms in this lesson

data hand-off contract
Requirements for values, keys, types, missingness, metadata and result ownership when data moves between libraries or representations.
engine decision record
A written choice with workload context, evidence, assumptions, exceptions and triggers for reassessment.
result materialisation
Producing the requested result representation from a computation, such as a count, typed table, DataFrame or row tuples.
result row shape
The number and meaning of cells in a result row, checked alongside row counts and values when comparing outputs.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.