Home ยท Volume 5 ยท Chapter 03

๐Ÿ“– Chapter 03 โ€” Data Storage and Format

The file format is the foundation. CSV is usually wrong. Parquet is usually right.

v0.1 ยท draft Vol 5 ยท Ch 03
~12 min

Learning Objectives

Introduction

Most people start with CSV. CSV is the lingua franca of data: every tool can read it, every human can open it, every export offers it. The problem: CSV is slow, large, and doesn't preserve types. For analytical work, there are much better choices.

This chapter covers the file formats that make analysis fast: Parquet for columnar storage, Arrow for in-memory, partitioning for query performance. By the end, you'll know when CSV is the right answer (rarely) and when to use something else (almost always).

The problem with CSV

CSV has three problems for analytical work:

For a dataset you'll read once, CSV is fine. For a dataset you'll read many times, the time savings from Parquet are massive.

Parquet: the columnar format

Parquet is an open-source columnar storage format. The design:

For most analytical datasets, Parquet is the right answer. The conversion from CSV to Parquet is one line of code:

import pandas as pd
df = pd.read_csv("data.csv")
df.to_parquet("data.parquet")

Read the CSV once, save as Parquet, and from then on, use the Parquet file. The first read is slow; every subsequent read is fast.

Arrow: the in-memory format

Apache Arrow is an in-memory columnar format. It's the standard for data exchange between tools (Pandas, Polars, DuckDB, Spark, etc.) without copying or serializing.

For most analytical work, you don't interact with Arrow directly. The tools you use (Polars, DuckDB, PyArrow) handle the conversion. But it's good to know: when you load a Parquet file into Polars or DuckDB, it's in Arrow format in memory. When you read a Parquet file with Pandas, it's also in Arrow (since recent Pandas versions).

The benefit: zero-copy between tools. A Polars DataFrame and a PyArrow Table share the same memory; no conversion needed.

Partitioned datasets

For large datasets, partitioning is the right answer. A partitioned dataset is a directory of Parquet files, organized by one or more columns:

data/processed/events/ โ”œโ”€โ”€ year=2024/ โ”‚ โ”œโ”€โ”€ month=01/ โ”‚ โ”‚ โ”œโ”€โ”€ part-0000.parquet โ”‚ โ”‚ โ”œโ”€โ”€ part-0001.parquet โ”‚ โ”‚ โ””โ”€โ”€ ... โ”‚ โ””โ”€โ”€ month=02/ โ”‚ โ””โ”€โ”€ ... โ””โ”€โ”€ year=2025/ โ””โ”€โ”€ ...

The partitioning by year and month means: a query for "all events in March 2024" only needs to read the files under year=2024/month=03/, not the whole dataset. This is the same idea as an index in a database.

For tools like DuckDB and Polars, partitioning is automatic: a query with a WHERE year = 2024 clause reads only the relevant partition.

Choosing partition columns

The right partition columns:

Common choices: year/month/day for time-series data, region or country for geographic data, category for categorical data. Avoid high-cardinality columns (user IDs, product IDs) โ€” too many partitions make the directory listing slow.

The "small files" problem

When you write a partitioned dataset with many small files, you hit the small files problem. Each file has overhead (metadata, headers, etc.). 1000 files of 1 MB each is slower to read than 1 file of 1 GB.

The solution: target file sizes of 100-500 MB. If your writes produce 1 MB files, batch them. If your reads scan 1000 small files, rewrite the dataset with larger files.

For most home data science, the small files problem is a non-issue (datasets are small enough that the overhead doesn't matter). For larger datasets, it's the first thing to look at when queries are slow.

JSON, JSONL, and other formats

For data that's nested or hierarchical (API responses, logs, documents), JSON is the standard. The line-delimited variant (JSONL, one JSON object per line) is the right format for streaming and batch processing.

For analysis, JSON has the same problems as CSV: no types, slow, large. The right approach: load JSON into Arrow (with PyArrow or DuckDB), save as Parquet for analysis.

Database files: SQLite and DuckDB

For some workflows, a single-file database is the right format. SQLite is the standard for embedded relational data. DuckDB is the standard for embedded analytical data. Both are single files that you can put on the NAS.

SQLite is great for transactional data: a contact list, a journal, a collection of small records. Use it when the data is structured and you want to query it with SQL.

DuckDB is great for analytical data: a dataset that you'd load into Pandas, but want to query with SQL without loading everything into memory. Use it when the dataset is bigger than RAM but smaller than disk.

For both, the file is in data/processed/. The analysis tools connect to the file, query, return results.

The "I just want to load a CSV" answer

Sometimes CSV is fine. The cases:

The pattern: load CSV once, save as Parquet, work with Parquet from then on. The first read is slow; every subsequent read is fast.

Tools: the right ones for the right job

For most home data science, Polars + DuckDB is the right stack. Pandas is fine for compatibility with older code; PyArrow is the foundation; both work well with each other.

The "schema matters" point

For Parquet and Arrow, the schema is part of the data. A Parquet file knows: column names, column types, nullability. When you read it, the types are preserved. When you write a DataFrame to Parquet, the types are stored.

The benefit: you don't have to re-infer types on every read. The schema is checked when the data is written; it's trusted when the data is read.

The discipline: write Parquet files with explicit schemas. Don't rely on type inference. The schema is documentation; it tells future-you what the data is.

Engineering Note

The file format is the foundation. A slow format makes everything slow. A small format makes everything small. The 5 minutes of converting a CSV to Parquet saves hours of waiting for the data to load. Do it once, do it right, benefit forever.

Summary

Parquet for columnar storage, Arrow for in-memory, partitioning for query performance, DuckDB for SQL on data that doesn't fit in RAM. CSV is for one-off loads and exports; convert to Parquet for analysis. Polars is the modern default for new projects. Schemas matter: write explicit schemas, preserve types. The file format is the foundation of fast analysis.

Checklist

Looking Ahead

Chapter 04 is pipelines and schedules. Cron, Airflow, Prefect. The chapter that turns "I run this analysis manually once a week" into "this analysis runs every Monday at 9 AM, the result is in my email, and I don't have to think about it."

Ch 03 ยท v0.1 ยท drafted from the original ChatGPT conversation, July 2026