σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 10 · High-performance Python engineering›Topic 10.12·~50 minAdvanced

Parquet and Arrow

Skill positionModule 10 of 11 · Topic 12 of 13
10.12 Parquet and Arrow
—Lab score— / 100Total XP—Streak—

Rows or columns?

A CSV file or a database row store keeps each record together: all the fields of trade 1, then trade 2, and so on. Analysis rarely wants that. Computing the average price of ten million trades needs only the price column, but a row layout makes the reader step over every other field. A columnar layout keeps each column contiguous, so a query reads only the columns it names, similar values sit side by side and compress very well, and arithmetic over a column runs over one tight block of memory. This is why quant data lakes use Parquet on disk and Arrow in memory.

# The same table as rows and as columns
rows = [{"symbol": "AAPL", "price": 184.25, "qty": 100},
        {"symbol": "MSFT", "price": 410.5, "qty": 50},
        {"symbol": "AAPL", "price": 184.5, "qty": 25}]

columns = {name: [row[name] for row in rows] for name in rows[0]}
print(columns)                                       # {'symbol': ['AAPL', 'MSFT', 'AAPL'], 'price': [184.25, 410.5, 184.5], 'qty': [100, 50, 25]}
print(sum(columns["price"]) / len(rows))             # 259.75   the average only touched one column

back = [{name: columns[name][i] for name in columns} for i in range(len(rows))]
print(back == rows)                                  # True  nothing is lost either way

Parquet: row groups, statistics and encodings

A Parquet file splits the table into row groups (say a million rows each); inside a group every column is stored as its own chunk, and the file records the minimum and maximum of each chunk. A query for price between 180 and 190 reads the statistics first and skips every row group whose range cannot contain such a price (predicate pushdown), and it reads only the requested columns (projection pushdown). Columns are also encoded: a column with few distinct values, such as the symbol, is stored as a small dictionary plus one tiny integer code per row, and the result is then compressed. A file of repetitive market data is often ten times smaller than the CSV.

# Dictionary encoding turns a few distinct values into a small dictionary plus integer codes
def dictionary_encode(values):
    positions, dictionary, codes = {}, [], []
    for value in values:
        if value not in positions:
            positions[value] = len(dictionary)
            dictionary.append(value)
        codes.append(positions[value])
    return dictionary, codes

symbols = ["AAPL", "MSFT", "AAPL", "AAPL", "MSFT", "NVDA"] * 1000
dictionary, codes = dictionary_encode(symbols)
print(dictionary, codes[:6])                         # ['AAPL', 'MSFT', 'NVDA'] [0, 1, 0, 0, 1, 2]
print([dictionary[c] for c in codes] == symbols)     # True  decoding restores the column
print(len(dictionary), "distinct values for", len(symbols), "rows")      # 3 distinct values for 6000 rows
# Statistics let a reader skip whole row groups without opening them
def groups_to_read(stats, low, high):
    return [i for i, (minimum, maximum) in enumerate(stats) if maximum >= low and minimum <= high]

stats = [(180.0, 183.5), (183.0, 186.0), (185.5, 190.0), (189.0, 195.0)]        # (min, max) of price per row group
print(groups_to_read(stats, 184.0, 185.0))           # [1]        only the second group can hold a price in this range
print(groups_to_read(stats, 189.5, 192.0))           # [2, 3]
print(groups_to_read(stats, 200.0, 210.0))           # []         no group needs to be read at all

Arrow, pandas and reading less

Apache Arrow defines the same columnar layout for memory, so programs and languages can hand a table to each other without converting it: pandas, Polars, DuckDB and Spark can all view one Arrow table with no copy. In practice you use pandas.read_parquet(path, columns=["symbol", "price"], filters=[("price", ">", 180)]), which pushes the column choice and the filter into the reader, or pyarrow.parquet.read_table. Write data partitioned by a column such as date (df.to_parquet(path, partition_cols=["date"])) so a query for one day opens one directory. Keep row groups large enough to compress well but small enough that the statistics can skip many of them, and sort the data by the column you filter on so each group covers a narrow range.

# Sorting by the filter column makes the row groups cover narrow, non-overlapping ranges
import random

rng = random.Random(7)
prices = [round(rng.uniform(100, 200), 2) for _ in range(10_000)]

def group_stats(values, size):
    groups = [values[i : i + size] for i in range(0, len(values), size)]
    return [(min(g), max(g)) for g in groups]

def groups_to_read(stats, low, high):
    return [i for i, (a, b) in enumerate(stats) if b >= low and a <= high]

unsorted_groups = len(groups_to_read(group_stats(prices, 1000), 150.0, 151.0))
sorted_groups = len(groups_to_read(group_stats(sorted(prices), 1000), 150.0, 151.0))
print(unsorted_groups, sorted_groups)                # 10 1   without sorting every group's range overlaps the query
A columnar file is for analysis, not for editsParquet files are written once and read many times. Changing one row means rewriting a file, so they suit history (ticks, bars, backtest results) and not a live order table, which belongs in a database. Append new data as new files or partitions instead of editing old ones. Keep the schema stable and the timestamps in UTC, and remember that float columns keep the float rounding you met earlier.
  • Columnar storage keeps each column together: queries read only the columns they need, similar values compress well, and column arithmetic is fast.
  • Parquet splits data into row groups with min/max statistics and dictionary-encodes repeated values; readers skip groups and columns they do not need.
  • Arrow is the in-memory twin shared by pandas, Polars and DuckDB; partition by date, sort by the filter column, append instead of editing.

Next up: time-series databases, stores built for timestamped data such as ticks and metrics.

Lab · what's graded

  • Turn rows into columns with to_columns25 pts
  • Rebuild the rows with to_rows25 pts
  • Shrink repeats with dictionary_encode25 pts
  • Skip what you can with groups_to_read25 pts
Explorer · lab-128-1
▾ lab-128-1
…
bash · in-browser
learner@quantcraft:~/lab-128-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-128-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub