1.4.7 · D4Python & Scientific Computing

Exercises — Data loading (CSV, JSON, parquet)

3,077 words14 min readBack to topic

This page builds on the parent topic and assumes you have met NumPy arrays and Pandas DataFrames. Where a number depends on file size, we do the arithmetic from scratch.

Before any exercise, fix the vocabulary you will lean on:

The two pictures below are the mental models the exercises keep returning to — study them once now. Figure 1 shows why reading one column is cheap in a columnar format and expensive in a row-oriented one (the heart of L2.3, L3.2, L5.2). Figure 2 shows the relative on-disk size of the three formats for the very table you compute by hand in L3.1, so you can check your arithmetic against the bars.

Figure — Data loading (CSV, JSON, parquet)
Figure — Data loading (CSV, JSON, parquet)

In Figure 1, notice the row-oriented layout at the top: to reach the magenta age cells you must step across every cell of every row. In the columnar layout below, only the highlighted AGES block is read — the faded name and score blocks are never touched. In Figure 2, the violet JSON bar is exactly 3× the orange CSV bar (because JSON repeats keys on every row), and the magenta Parquet bar is a sliver (columnar compression). Keep both images in mind as you work.


Level 1 — Recognition

Exercise 1.1 (L1)

For each task, name the single best format and give a one-line reason:

  • (a) Emailing a 200-row lookup table to a non-technical colleague who will open it in Excel.
  • (b) Receiving live data from a web API that returns nested {"user": {...}} objects.
  • (c) Storing a 50 GB feature table that an ML training job reads column-by-column thousands of times.
Recall Solution
  • (a) CSV. It is plain text, opens in any spreadsheet with no library, and 200 rows is tiny so speed/size do not matter. Human-readability wins.
  • (b) JSON. APIs speak JSON natively and it is the only listed format that stores nested key/value structure without flattening first.
  • (c) Parquet. Columnar layout means column-by-column reads touch only the needed block, and its compression makes 50 GB far smaller on disk. This is the classic analytics/ML workload.

Exercise 1.2 (L1)

True or false, with a reason:

"A CSV file records that the age column contains integers, so Pandas never has to guess types."

Recall Solution

False. CSV is schema-less — every value is stored as text with no type tag. Pandas must infer types by sampling rows and guessing, or you must pass dtype={...} yourself. Parquet, by contrast, stores the schema inside the file.


Level 2 — Application

Exercise 2.1 (L2)

Write the pd.read_csv call that:

  • treats the strings "", "NA", and "missing" as missing values,
  • skips (does not crash on) rows with the wrong number of columns,
  • forces the zip column to stay a string.
Recall Solution
df = pd.read_csv(
    'data.csv',
    na_values=['', 'NA', 'missing'],  # standardise missing markers to NaN
    on_bad_lines='skip',              # malformed row -> dropped, load survives
    dtype={'zip': str}                # keep leading zeros, no numeric coercion
)

Why each piece: na_values unifies the many ways systems write "no data" into one NaN (see handling missing data); on_bad_lines='skip' prevents a single broken line from aborting a multi-GB load; dtype={'zip': str} stops "01234" from silently becoming the integer 1234.

Exercise 2.2 (L2)

The API below returns nested records. Write code that flattens it into a DataFrame whose columns are name, city, zip (underscore-joined, not dot-joined).

data = [
    {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}},
    {"name": "Bob",   "address": {"city": "LA",  "zip": "90001"}},
]
Recall Solution
df = pd.json_normalize(data, sep='_')
# columns are now: name, address_city, address_zip
df = df.rename(columns={'address_city': 'city', 'address_zip': 'zip'})

json_normalize walks the nested keys address -> city and joins them with sep. Using sep='_' (instead of the default .) means the resulting names work with dot-attribute access like df.city. The final rename just drops the address_ prefix for readability. This is a common first step before data cleaning.

Exercise 2.3 (L2)

A 4 GB file must be summed in a machine with only 1 GB of free RAM. Complete the chunked read that computes the total of column score without ever loading the whole file.

Recall Solution
total = 0.0
for chunk in pd.read_csv('big.csv', chunksize=100_000):
    total += chunk['score'].sum()  # add this chunk, then discard it

chunksize makes read_csv return an iterator of DataFrames, not one giant DataFrame. Each chunk is loaded, its partial sum added, then garbage-collected before the next chunk arrives — so peak memory is bounded by one chunk, not the whole file.


Level 3 — Analysis

Exercise 3.1 (L3)

Estimate the uncompressed on-disk size of a table with 1,000,000 rows × 10 columns, where each value is a small number averaging 3 characters of text (including its comma delimiter), stored as CSV. Then estimate the JSON size if JSON adds an average of 6 extra bytes of keys/punctuation per value. Report both in MB (use bytes) and the JSON/CSV ratio.

Recall Solution

CSV. Bytes : JSON. Each value now costs bytes: Ratio . JSON is 3× larger because it repeats the key text ("age":) on every single row, whereas CSV writes column names only once in the header. Compare these numbers to the orange and violet bars in Figure 2 — they are the same 30 MB and 90 MB. This matches the parent note's "JSON overhead ≈ 2–3× CSV."

Exercise 3.2 (L3)

Using the same 30 MB CSV, Parquet compresses it by a factor of 6. You need to read 1 of the 10 columns. Compute:

  • (a) the Parquet file size in MB,
  • (b) the bytes read when pulling that 1 column from Parquet (assume each column is of the compressed file),
  • (c) the bytes read when pulling that 1 column from CSV,
  • (d) the I/O saving as a percentage, to 1 decimal place.
Recall Solution
  • (a) MB.
  • (b) One of ten columns of the compressed file: MB.
  • (c) CSV must scan the whole file to find the column (see the L2 trap and Figure 1): MB.
  • (d) Saving .

The lesson: the win comes from two multiplied effects — 6× from compression and 10× from touching one column of ten. That is why Parquet dominates wide analytics tables.

Exercise 3.3 (L3)

A Parquet file stores its rows in groups. Each row group records the min and max of every column. You query filters=[('year', '>=', 2020)]. The file has 4 row groups with year ranges , , , . How many row groups must be decompressed and read, and why?

Recall Solution

Only 1 — the last group .

For the first three groups the stored max values are , all , so no row in them can satisfy year >= 2020. The reader compares your predicate against the min/max stats and skips those groups entirely — this is predicate pushdown. CSV/JSON have no such stats, so they must read everything and filter afterward in memory.


Level 4 — Synthesis

Exercise 4.1 (L4)

Design a one-pass pipeline: a 12 GB newline-delimited JSON (JSONL) file arrives from an API. You must (1) read it without exceeding 2 GB RAM, (2) flatten nested address fields, (3) keep only rows where age >= 18, and (4) write the cleaned result as Parquet. Write the code and justify the format choices at each stage.

Recall Solution
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
 
writer = None
reader = pd.read_json('events.jsonl', lines=True, chunksize=200_000)
for chunk in reader:                       # (1) bounded memory: JSONL streams line-by-line
    flat = pd.json_normalize(chunk.to_dict('records'), sep='_')  # (2) flatten address_city, ...
    adults = flat[flat['age'] >= 18]       # (3) filter early -> less data downstream
    table = pa.Table.from_pandas(adults)
    if writer is None:
        writer = pq.ParquetWriter('clean.parquet', table.schema)
    writer.write_table(table)              # (4) append each chunk to one Parquet file
if writer is not None:
    writer.close()

Why these choices:

  • JSONL, not plain JSON, is essential: a single [...] JSON array must be parsed whole (can't stream), but line-delimited JSON lets chunksize read a bounded number of lines at a time — this is what keeps us under 2 GB.
  • Filter inside the loop, so only adults are ever materialised and written; filtering after writing would waste I/O.
  • Write Parquet, because the cleaned output feeds an ML pipeline that will read columns repeatedly — the columnar + compressed format pays off on every future read. This output is exactly what feature engineering and TensorFlow/Keras want to consume.

Exercise 4.2 (L4)

Your teammate loads a Parquet file of user IDs like "007823" and finds they became integers 7823, losing leading zeros. But Parquet stores schema! Explain how this happened and give the fix at the point of failure.

Recall Solution

Parquet preserves whatever type it was given at write time. The corruption happened upstream: the data was first read from a CSV with type inference, which saw "007823", guessed "integer," and stored it as 7823then it was written to Parquet, faithfully preserving the now-wrong int type.

Fix at the source (the CSV read), not the Parquet read:

df = pd.read_csv('users.csv', dtype={'user_id': str})   # keep it text from the start
df.to_parquet('users.parquet')                          # Parquet now stores str, zeros safe

Moral: Parquet guarantees type persistence, not type correctness. Garbage in, faithfully-preserved garbage out.


Level 5 — Mastery

Exercise 5.1 (L5)

You must pick one storage format for a table that is written once per hour by 500 IoT devices appending rows, and read once per day by an analytics job that scans 3 of its 40 columns. Writes and reads have opposite preferences. Reason through the trade-off and give a defensible design (you may use more than one format).

Recall Solution

The conflict: appends favour a row-oriented, append-friendly format (CSV or JSONL — you just tack a line onto the end). Analytics favours columnar Parquet (reads only 3 of 40 columns, ~ of column I/O avoided, plus compression). Parquet is write-slow / append-hostile because appending means rewriting or adding row groups; CSV is read-slow for column scans.

Defensible two-tier design:

  1. Ingest tier — JSONL (append-only). Each device appends a line; cheap, streaming, no rewrite. This is the "hot" landing zone.
  2. Compaction job (once per hour or day) — convert JSONL → Parquet, batching the accumulated lines into row groups. Filter/clean during compaction.
  3. Analytics reads Parquet, getting columnar + predicate-pushdown speed on its 3 columns.

This is the standard append-fast, then compact-to-columnar pattern: use each format where its cost model wins. A single-format answer is wrong because no listed format is simultaneously append-cheap and column-read-cheap.

Exercise 5.2 (L5)

Quantify the mistake of "just use one big CSV" for the table in 5.1. Assume the daily analytics job reads a 20 GB CSV at 170 MB/s, versus an equivalent Parquet where the 3 needed columns of 40 total, after compression, must be read. Assume Parquet columns are equal-sized. Compute both read volumes (GB) and both times (seconds), and the speedup ratio to 1 decimal.

Recall Solution

CSV read volume: entire file GB. Time s.

Parquet read volume: compress GB by 8× GB total; read of it GB. Time s.

Speedup .

Reading 20 GB where you needed 0.19 GB is the tax of the wrong format — over 100× wasted work on a table read every day. The two multiplied effects again: (compression) and 40/3 ≈ 13.3× (column selection), giving roughly .


Recall Self-check: did you earn the level?

One-format-fits-all is always wrong here ::: True — each format wins a different cost (append, size, or column-read); real systems combine them. Why usecols on CSV saves memory but not I/O ::: CSV has no file index, so the parser must scan and split every line to find the column, reading all columns off disk regardless. Why JSON is ~3× larger than CSV ::: JSON repeats the key text on every row; CSV writes column names only once in the header. Where do you fix leading-zero corruption ::: At the CSV read with dtype={'col': str} — upstream of Parquet, which only preserves the type it was handed. The two multiplied effects behind Parquet's column-read speed ::: compression factor × (total columns / columns needed), plus predicate pushdown skipping row groups.