Solo Analytics Project | Formula 1

f1-strategy.

The pit loss is a myth

An end-to-end analysis of 15 Formula 1 seasons (366,508 timed laps, 12,881 pit stops) that derives pit-loss and undercut models from timing data alone, and shows that the rule the pit wall leans on does not predict anything.

Solo author15 seasons · 2011-202612,881 pit stops57 tests

366,508

timed laps · 327 races · 15 seasons

0.52 → 0.82

ROC-AUC: pit-loss rule vs gap + tyre age, out of sample

62% → 6.6%

undercut conversion, under 1.5 s gap vs beyond 3 s

57

tests · Python and SQL cross-checks agree

Demo · interactive replay

The gap decides it.

Pick the gap to the car ahead before the stop. The archive says how often the undercut converted, and what the pit-wall rule would have called.

Gap before the stop

1,528 measured pairs, rival responded

Converted

Gap < 0.5 s

62.3%

n = 53 · 95% Wilson interval 48.8% to 74.1%

Pit-wall ruleundercut on whenever the gap is under the 21.5 s pit loss

Every bin here clears that test, so the rule calls all of them. That is why it scores ROC-AUC 0.52 while gap plus tyre age reaches 0.82 on races the model never saw.

Binned from the same 1,528 undercut pairs the notebook plots, rival responded within five laps. The pit-wall rule labels all four bins as on, which is the point: it does not separate them.

Undercut Success by Gap

Pit Loss Distribution

Model Performance

Undercut Success by Circuit

Team Decision Quality

Undercut Success by GapPit Loss DistributionModel PerformanceUndercut Success by CircuitTeam Decision Quality

Tech Stack

1Python
2SQL (window functions)
3Pandas
4scikit-learn
5Parquet
6SQLite
7pytest
8Matplotlib

The brief · problem → approach → result

01 / The problem

Formula 1 teams call an undercut on when the gap to the car ahead is smaller than the time a pit stop costs. It is the pit-wall shorthand, and nobody runs it against the archive to see whether it actually predicts anything.

02 / The approach

Take the public Formula 1 timing archive (366,508 laps across 15 seasons) and estimate the pit loss at every circuit from lap times alone. Then test the rule out of sample against what actually decided each move, and model the next stop one lap ahead.

03 / The result

The pit-loss rule scores ROC-AUC 0.52, no better than a coin flip, while gap and tyre offset reach 0.82 on seasons the model never saw. Nearly half of the undercuts teams attempted were long shots that converted 6% of the time. The whole thing ships as a tested pipeline: 57 tests, Parquet and SQLite storage, and two results computed twice to prove Python and SQL agree.

The Pipeline

Ingest | Validate | Model|Index / 01
📥

Ingest

CC0 Ergast archive

🧪

Validate

24 quality checks

📈

Model

pit loss & undercut

🎯

Predict

stop one lap ahead

✅

Cross-check

Python & SQL agree

A Python package ingests the CC0 Ergast-lineage archive, validates it (surfacing three genuine data defects, including 810 pit-stop durations published as lap times), and builds stint, pit-event and cohort tables into Parquet and SQLite. Two of the questions are answered twice, once in Python and once in SQL window functions, so the two implementations can be shown to agree.

What It Answers

Pit loss | Undercut | Prediction|Index / 02
⏱

Pit Loss per Circuit

Estimated what a stop costs from timing data alone: (in-lap + out-lap) minus two times recent clean pace. Median green-flag pit loss lands at 21.5 seconds across 36 circuits, and a second implementation in SQL window functions agrees on every one of them.

Bootstrap intervals36 circuitsSQL cross-check
🔀

When the Undercut Works

The pit-wall shorthand says an undercut is on when the gap is smaller than the pit loss. It turns out to be uninformative: once the rival stops too, both cars pay the loss and it cancels. What decides the move is the gap plus the tyre offset, converting 62% of the time from under 1.5 seconds and 6.6% beyond three.

ROC-AUC 0.52 vs 0.821,528 pairsWhere matters too
🎯

Predicting Stops One Lap Ahead

A leakage-safe model (features to lap N, label at N+1, split by race) ranks the next stop at ROC-AUC 0.80. It is a ranking model, not an alarm: PR-AUC 0.136 is the honest number, and the notebook reports the precision-recall trade-off alongside it.

Race-grouped splitPR-AUC 0.136Permutation importance

Built To Be Checked

Tests | Cross-checks | Honesty|Index / 03

Every derivation validated twice

  • 0157 tests cover the pit-loss, stint and undercut derivations.
  • 02Stint reconstruction: 0 disagreeing laps of 366,508 between Python and SQL.
  • 03Circuit pit loss: the SQL reimplementation agrees on all 36 circuits.
  • 04A synthetic fixture with an injected 15 s loss is recovered exactly.

What the model cannot say

A

A ranking model, not an alarm

The stop-prediction model ranks well (ROC-AUC 0.80) but PR-AUC 0.136 is the honest number: at the default threshold it catches 1.3% of stops.

W

Why circuits differ

Monaco converts 7% of its undercuts and Bahrain 49%, yet a circuit's own pit loss does not order that table (rho = -0.30, p = 0.19).

O

Observational, not chosen

A low-odds attempt can be forced by a rival's stop or the weather rather than picked freely, and the notebook says so.

Built on the CC0 Ergast/Jolpica timing archive. The notebook, not a README, is the single source of truth: dataset, method, findings, limitations and the reproduce path all live in one place.

Technology Architecture

Reproducible analytics pipeline|Index / 04
f1-strategy-analytics/
├── src/                        # python package (pipeline + models)
│   ├── pipeline.py             # ingest, validate, build tables
│   ├── metrics.py              # pit-loss & undercut estimators
│   └── ...
├── sql/                        # second implementation, in SQL
│   ├── 01_stint_reconstruction.sql
│   └── 02_circuit_pit_loss.sql
├── tests/                      # 57 tests over the derivations
├── notebooks/
│   └── analysis.ipynb          # the single notebook: story + figures
├── reports/
│   ├── findings_summary.md     # every number, with provenance
│   └── figures/                # 16 figures
└── requirements.txt