1.4.7 · D2Python & Scientific Computing

Visual walkthrough — Data loading (CSV, JSON, parquet)

3,112 words14 min readBack to topic

Before we start, one word we will use constantly:

We will build a tiny table and follow it through every format. Our table:

name age score
Alice 25 92.5
Bob 30 87.3

Step 1 — What a table really is before it becomes a file

WHAT. A table lives in our head as a 2‑D grid: rows going down, columns going across. But a file is 1‑D — one long tape. So before anything, we must choose an order in which to unroll the grid onto the tape.

WHY. There are exactly two natural ways to flatten a grid into a line, and this single choice is the entire story of the page. Everything else — speed, size, "columnar win" — is a consequence of this choice.

PICTURE. The grid on the left; the two ways to unroll it (row-by-row vs column-by-column) as two different tapes on the right.

Figure — Data loading (CSV, JSON, parquet)

Step 2 — CSV on the tape: cheap to write, blind to structure

WHAT. We lay our table out as CSV. Each row becomes one line of text; each value is written as characters (the number 25 becomes the two letters '2''5'), separated by commas.

WHY text and row-major? Because CSV's job is human interchange — you want to open it anywhere, in any editor, and read it. Row-major matches how you'd read a spreadsheet aloud ("Alice, 25, 92 point 5..."). The cost of that friendliness appears in the next step.

PICTURE. The CSV tape with each character shown as a box, ages highlighted in pink, and the disk head forced to walk through the names to reach each age.

Figure — Data loading (CSV, JSON, parquet)

Let us count the reading cost of "give me all the ages". Call:

  • — how many rows exist (a million people).
  • — the size of one entire row in bytes (name plus age plus score plus commas).

The head must pass over every byte of every row to find each comma-separated age, because nothing on the tape says "ages start here". The age is 2 bytes but you paid to walk the whole row of, say, 20 bytes to reach it.


Step 3 — JSON on the tape: same walk, now carrying its own dictionary

WHAT. JSON is also row-major, but each row re-writes the column names ("age":) next to every single value.

WHY. JSON's superpower is being self-describing and nestable — a value can be {"address": {"city": "NYC"}}. To allow that, each value must be labelled. The price: the labels repeat on every row.

PICTURE. The JSON tape drawn under the CSV tape at the same scale, so you see the extra "age":, "name":, braces and quotes swelling the tape for the same data.

Figure — Data loading (CSV, JSON, parquet)

The size of one JSON row is:

  • — the actual data (25, Alice …), the only part you care about.
  • — the words name, age, score, paid again on every row.
  • { } " " : scaffolding.

Because and are pure overhead repeated times, JSON is the fattest tape — the parent's rule-of-thumb of CSV. Reading a single column is still the full walk , now over a longer tape. Worst of both worlds for analytics; best for flexible, labelled API data.


Step 4 — The pivot: rewrite the tape column-by-column

WHAT. Take the exact same three values-per-row, but reorder the tape so all names sit together, then all ages, then all scores. This is Parquet's core move — column-major from Step 1.

WHY. If the ages are physically contiguous on the tape, and a small note at the front records where the age block starts and how long it is, the head can jump straight to it and read only those bytes. We are trading away human-readability to buy seekability.

PICTURE. The tape sliced into three coloured blocks (names / ages / scores) with a "footer" box of metadata at the end holding each block's start position and length; an arrow shows the head jumping directly to the age block.

Figure — Data loading (CSV, JSON, parquet)

Now the true cost of "give me all the ages" has two parts, because you can only jump after reading the footer:

  • — the footer/metadata, read first so we learn where the age block lives. Small and fixed, but not zero.
  • — the cost of moving the head to the age block's start position (one disk seek).
  • — the bytes of just the age block. The names and scores are never touched.

For a large table is tiny beside , so — but for a tiny file the footer can dominate, which is exactly the edge case in Step 8.


Step 5 — Quantify the win: the ratio that gives ~40×

WHAT. Put the row cost from Step 2 next to the column cost from Step 4 and take their ratio — this is the parent note's speedup number.

WHY. A speedup claim is meaningless without the formula behind it. Here is where "columnar" turns into an actual multiplier you can predict before running anything.

PICTURE. Two horizontal bars — a long row-format bar (you read all of it) vs a short columnar bar (you read one slice) — with the savings region shaded pale yellow.

Figure — Data loading (CSV, JSON, parquet)

First, the one symbol we have not yet named:

The fraction of work you save by reading one column, ignoring the tiny footer for now:

  • — total bytes a row format forces you to read (Step 2).
  • — bytes columnar actually reads (Step 4).
  • The ratio shrinks as the table gets wider (bigger ), because one column is a smaller slice of the whole.

If every column is roughly the same size, one column is of the data, so , giving:

A 100‑column table () → about 100× fewer bytes read for one column. Add real-world decompression (Step 6) and seek/footer overhead (Step 4) and this settles near the parent's observed ~40×. The width of your table is your speedup.


Step 6 — Why the columnar blocks also compress harder (and what it costs)

WHAT. Within one column, every value has the same type and often repeats (25, 25, 30, 25, 25…). A block of same-kind data squeezes far smaller than a row mixing text, ints and floats.

WHY. Compression works by spotting repetition and predictability. A row Alice,25,92.5 mixes three unpredictable things. A column 25,25,30,25,25 is a predictable stream — perfect for the tricks below. But shrinking on disk is not free: the CPU must un-shrink it on the way back in, so we buy fewer disk-bytes with extra CPU-cycles.

PICTURE. One age column collapsing under Parquet's hybrid encoding, each stage labelled, with a red "CPU cost to decompress" arrow reminding you the squeeze is undone at read time.

Figure — Data loading (CSV, JSON, parquet)

Step 7 — Predicate pushdown: skip blocks you don't even want

WHAT. Ask for age > 25. Parquet groups rows into chunks ("row groups") and the footer records the min and max age of each chunk. If a chunk cannot possibly contain a matching row, the whole chunk is skipped unread.

WHY. Filtering in CSV means: read everything, throw most away. Parquet can decide from the footer alone that a block is irrelevant — it never reads those bytes at all. This is stronger than the column skip of Step 4: now we skip rows too.

PICTURE. Three stacked row-group blocks, each tagged with (min, max) age; the query age > 25 crosses out the block whose max is 24 (no value can exceed 25) before any data is read.

Figure — Data loading (CSV, JSON, parquet)

To be precise about the comparison and avoid an off-by-one, we keep the filter as written and turn it into a keep-rule on the group's max:

  • — the literal filter value written in your query, here from age > 25. It is not shifted to 26.
  • — the largest age in group , straight from the footer.
  • The rule is (strictly, matching the strict > in the query). For age >= 25 the rule would instead be . Match the comparison operator exactly — that is where off-by-one bugs hide.

A group with and : since , no row can satisfy age > 25, so it contributes — skipped unread.


Step 8 — Edge & degenerate cases (never hit a surprise)

WHAT. Four corners where "columnar always wins" bends, plus the classic type trap. The figure lays all five side by side.

WHY. A tool you only know in its best case will burn you in its worst. Each panel below has its own reason it fails, so you can recognise the situation before you pay for it.

PICTURE. Left column: the four "columnar loses" cases stacked, each with its failing quantity called out. Right: the zip-code trap shown as "01234" losing its leading zero the moment CSV guesses it is a number.

Figure — Data loading (CSV, JSON, parquet)

Walking the figure top to bottom:

The right-hand panel of the figure shows the type trap — format-independent but it bites hardest in CSV:


The one-picture summary

The figure below compresses the whole walkthrough onto a single board: the top tape is row-major (CSV/JSON) — the head's arrow runs across every box, cost . The bottom tape is column-major (Parquet) — three contiguous colour blocks plus a yellow footer; the pink arrow dives straight into the AGES block, and the caption reminds you that footer + seek is the small price for that jump. The one-line takeaway under it — width of table ≈ speedup — is the payoff of Steps 4–7.

Figure — Data loading (CSV, JSON, parquet)
Recall Feynman retelling — say it back in plain words

Imagine a library where every book is one person's file: to collect everyone's height, you'd pull every book off every shelf, flip to the height page, and re-shelve — that's CSV, row by row. Now imagine a second library organised so all the heights sit on one shelf, all the names on another, with a directory card at the door saying "heights: aisle 3, and by the way the tallest here is 190". You want heights over 200? The card already says the tallest is 190 — you don't even enter. That directory card is Parquet's footer; the by-column shelving is the columnar layout; and because every book on the height shelf is just numbers, you can shrink-wrap them tight (but you spend a moment un-wrapping each on the way out). The only times the first library wins: when you genuinely want whole books (all columns), when there's only one book (tiny file, the directory card weighs more than the book), or when new books arrive every second (append-heavy writes).

Where does this walkthrough sit in the vault? Files load into arrays and frames from 1.4.05-NumPy-arrays-and-operations and 1.4.06-Pandas-DataFrames-and-operations; you then clean them (1.4.08-Data-cleaning-and-preprocessing, 1.5.01-Handling-missing-data), engineer features (2.3.05-Feature-engineeringtechniques), and feed them to models (3.1.02-TensorFlow-and-Keras-basics). Parquet is the format of choice for that whole ML pipeline precisely because of Steps 4–7.

Recall Quick self-test

Why can't CSV jump straight to the age column? ::: It is row-major text with no index/footer — nothing on the tape records where ages start, so the parser must walk every byte to find each comma. In Step 5, what makes the speedup bigger? ::: A larger (more columns) — one column is a smaller slice of the whole, so shrinks and speedup . In age > 25, what value does Parquet compare each group's max against? ::: The literal filter value (not 26); it keeps a group only if , matching the strict >. Why isn't a Parquet read truly "free" apart from the age block? ::: You must first read the footer () and pay a seek (), and every compressed block costs CPU to decompress. Name one case where columnar loses. ::: Reading all columns, tiny files, or append-heavy writes (Parquet writes are slow).