Methodology

The judgement calls behind the numbers.

Every published answer on this site is the product of explicit choices: which source to trust, which comparison is fair enough to publish, which model assumptions are acceptable, and where the data stops supporting the story. This page is the paper trail.

If you only read one part of this page, the core rules are simple:

Sources

The Ergast → Jolpica migration turned out to be mostly a plumbing job: Jolpica preserves Ergast's schema deliberately, so the ingestion rewrite was mechanical rather than conceptual.

Pipeline shape

raw Parquet → staging views → ephemeral intermediates → materialised marts
             (rename + cast)    (gaps-and-islands,       (dim + fact)
                                  DNF classification,
                                  teammate pairings)

Staging lives in DuckDB as views — cheap, 1:1 with raw. Intermediate models are ephemeral: dbt inlines them as CTEs inside downstream marts rather than materialising them as separate objects. Marts are tables because the presentation layer hits them directly and wants fast reads.

Ingestion and the Bradley–Terry fit are both Python because each job picks its best tool: httpx + pyarrow for polite paginated API fetches with atomic Parquet writes, scipy.optimize for a ridge-penalised maximum-likelihood fit of the latent-pace model. Forcing either into pure SQL would hide more than it would reveal.

Grain decisions

Every fact table documents its grain in its YAML description. Headlines:

Model Grain Notes
fct_race_results one row per driver per race Joined to dim_drivers via the SCD-2 window so the team assignment is date-correct.
fct_teammate_pairings one row per constructor per race per driver_a < driver_b Canonical ordering so each head-to-head is counted once.

fct_teammate_pairings is the input to the Bradley–Terry fit.

DNF classification

finish_status in Jolpica is free text. Modern seasons use strings like "Engine", "Gearbox", "Collision", "+1 Lap", "Lapped", "Did not start", and a long tail of component failures. Raw text is useful for exposition (“Verstappen retired — gearbox”) but hostile to aggregation (“what share of DNFs was mechanical in the hybrid era?”).

A dnf_category macro collapses the long tail into a ten-element enum:

finished, lapped, collision, mechanical, disqualified, dns, dnq, medical, retired, unknown.

Two calls worth flagging. First, both "+N Lap(s)" and bare "Lapped" get bucketed as lapped — missing the second form misclassifies roughly 30% of modern-era results as mechanical DNFs (caught during smoke testing, before it reached the ratings model). Second, mechanical is a catch-all for any named component failure — Engine, Gearbox, ERS, Hydraulics, Clutch, Brakes, Suspension, Tyre, and the rest. The original finish_status is preserved alongside the category so a downstream split (power-unit vs. chassis) is a single CTE away when we need it.

Slowly changing driver → constructor relationships

A driver can race for several constructors over their career and occasionally switches mid-season. Any fact table that links to dim_drivers needs the version of the driver row that was valid on the race date, not the latest row.

dim_drivers is therefore SCD-2: one row per (driver_id, span_index), with valid_from / valid_to / is_current. The spans are derived with a gaps- and-islands walk over each driver's chronological race appearances — a new span opens whenever the constructor changes, and a driver who returns to a previous constructor gets a fresh span rather than extending the old one. Historical team loyalty is preserved faithfully.

This is a computed SCD-2 rather than a dbt snapshot on purpose: snapshots depend on when you ran them, which breaks backfill reproducibility. A computed SCD-2 produces the same spans from the same raw data forever.

The Bradley–Terry fit

The live GOAT page is the short-form write-up; this section is the technical version.

Generated model documentation

Everything above describes methodology at a narrative level. The machine- readable version — every model, every column, every test, every line of lineage — is published as a static site at /projects/f1/docs/. It's the output of dbt docs generate plus the descriptions and tests declared alongside the models, rebuilt on every deploy.

A few spots worth looking at if you're curious:

Other analyses on this site

Every page is backed by a mart_* parquet written by dbt-duckdb's external materialisation, so the presentation layer reads its data the same way regardless of whether the mart came from SQL or Python.

Page Mart Shape
The GOAT, in four charts mart_driver_ratings + mart_driver_ratings_by_season Cross-era teammate ratings, rolling-window career arcs, head-to-head win probability, and peak-vs-longevity scatter. Both marts come from pipeline/analysis/bradley_terry.py. Indy 500 rounds (1950-1960) excluded at source.
Era competitiveness mart_era_competitiveness HHI of constructor race-wins per season, plus unique-winners count and champion win share. Scope starts at 1958 (constructors' championship era).
DNF causes mart_dnf_causes Per-season counts and shares by dnf_category. Presentation rolls up to decade or stays per-year without re-pivoting.
Overtaking extinction mart_overtaking Two grid-to-finish proxies per season: mean |grid − finish| across classified finishers, and the Pearson correlation between grid and finish.
The LOAT, in three charts mart_teammate_reliability + mart_inherited_positions + mart_safety_car_lottery Three teammate-normalised luck lenses: mechanical-DNF delta on paired same-team starts, inherited-share delta on paired classified starts, and safety-car window delta since 2018. SC windows are contiguous runs of neutralised laps detected via gaps-and-islands on FastF1 track_status_code. The closing verdict is a percentile-sum scorecard over the joined marts. Indy 500 (1950-1960) excluded at source across all three.

A couple of confounds worth naming that affect several analyses at once:

Limits and non-claims

Honesty is part of the methodology.

None of this invalidates the ratings — it shapes how to read them. That's the whole point of having a methodology page.