Real F1 telemetry. Physically-grounded tyre modelling. Probabilistic strategy decisions. A full-stack race strategy simulator built on FastF1 data, a 3-phase piecewise degradation model, vectorised Monte Carlo simulation, and an undercut/overcut rival engine — with a live Streamlit dashboard inspired by F1 broadcast telemetry design.
F1 race strategy is a real-time optimisation problem under uncertainty: noisy tyre degradation, an opponent whose next move you don't control, and thousands of possible strategy combinations, all under a hard time budget.
This project builds a strategy decision-support system from the data up — not a toy pit-stop calculator, but a system that models the physics of tyre wear, the economics of a pit stop, and the game-theoretic question of when to fight for track position:
- Loads real race data via FastF1 — lap times, tyre compounds, pit stops, weather, final classification — for any season 2018–2024, not a hardcoded race
- Cleans and engineers features — removes SC/VSC laps, inaccurate laps, fuel-corrects lap times, tracks stint-relative degradation
- Fits a 3-phase piecewise-linear degradation model per compound (bedding → plateau → cliff), with the cliff lap reported as a first-class output with a 95% confidence interval
- Scales degradation by live track temperature pulled from session weather data
- Generates every valid 1-stop and 2-stop strategy under F1 compound rules and runs vectorised Monte Carlo — thousands of strategies × simulations in seconds
- Models both undercut and overcut against a configurable rival, with side-by-side expected-value comparison
- Backtests the AI-optimal strategy against what the driver actually did, using real stint data
- Visualises everything in a production-grade dark-mode dashboard with an F1-app-inspired timing tower and weather strip
FastF1 API
│
▼
[pipeline/loader.py] ← Session loading, caching, live race schedule/laps,
│ weather, final classification — no hardcoded season data
▼
[pipeline/cleaner.py] ← Remove SC/VSC laps, inaccurate laps, per-compound
│ outlier removal (vectorised, not per-group apply())
▼
[pipeline/features.py] ← Tyre age, stint number, fuel correction, DegDelta
│ (vectorised transform — no groupby().apply() column loss)
▼
[models/tyre_deg.py] ← 3-phase piecewise-linear degradation model (pwlf),
[models/pit_loss.py] cliff detection with CI, driver-weighted fitting,
│ track temperature scaling
▼
[simulator/strategy.py] ← Strategy & Stint dataclasses, generator functions
[simulator/race_sim.py] ← Deterministic race simulation, backtest reconstruction
[simulator/monte_carlo.py] ← Vectorised N-iteration Monte Carlo runner
[simulator/rival.py] ← Undercut AND overcut EV scanning against a rival
│
▼
[visualisation/] ← Dashboard, degradation/strategy/rival plots
f1-strategy-ai/
├── src/
│ ├── pipeline/
│ │ ├── loader.py # Session loading, live schedule, weather, classification
│ │ ├── cleaner.py # SC laps, inaccurate laps, vectorised outlier removal
│ │ └── features.py # Fuel correction, tyre age, vectorised stint deltas
│ ├── models/
│ │ ├── tyre_deg.py # 3-phase piecewise model, cliff detection, temp scaling
│ │ └── pit_loss.py # Data-driven pit stop time loss
│ ├── simulator/
│ │ ├── strategy.py # Strategy / Stint dataclasses + generation
│ │ ├── race_sim.py # Deterministic race simulation + backtest support
│ │ ├── monte_carlo.py # Vectorised MC engine (NumPy matrix ops)
│ │ └── rival.py # Undercut / overcut EV scanning, H2H Monte Carlo
│ ├── visualisation/
│ │ ├── dashboard.py # Streamlit app (main entry point)
│ │ ├── tyre_plots.py # Piecewise degradation curves, cliff markers
│ │ ├── strategy_plots.py # Comparison bars, lap traces, undercut heatmap
│ │ └── rival_plots.py # Undercut/overcut EV timeline, H2H, comparison chart
│ └── utils/
│ ├── config.py # Central config
│ └── logger.py # Structured logging
├── tests/ # 91 tests across pipeline, model, and rival logic
│ ├── test_pipeline.py
│ ├── test_tyre_deg.py
│ ├── test_simulator.py
│ └── test_rival.py
├── docs/
│ ├── architecture.md
│ ├── decisions.md # Real engineering decisions and bugs found & fixed
│ ├── model_notes.md
│ └── improvements.md
├── .github/workflows/test.yml # CI — runs full suite on push/PR
├── Dockerfile
├── docker-compose.yml
└── data/cache/ # FastF1 HTTP cache (auto-populated)
Real tyre degradation has three physically distinct phases: a bedding phase (new rubber, lap times can improve), a plateau (roughly linear wear), and a cliff (abrupt breakdown). A polynomial forces a single smooth curve across all three — it can't represent a phase transition.
This model fits piecewise-linear segments (via pwlf) with the breakpoints optimised jointly:
Bedding → laps 1–B₁ (often slightly negative slope)
Plateau → laps B₁–B₂ (roughly linear degradation)
Cliff → laps B₂+ (steep slope increase)
The cliff lap B₂ is a first-class model output with a 95% confidence interval (analytical standard error, falling back to residual bootstrap on sparse data), guarded against false positives on flat or U-shaped compounds:
model.cliff_lap("SOFT") # → 16.4
model.cliff_ci("SOFT") # → (14.8, 18.1)Filtering to one driver's laps alone starves the model — a single race gives ~15 clean laps per compound, borderline for a stable 3-segment fit. Instead, the selected driver's laps are up-weighted (sample_weight) 4× above the fleet, keeping the full 150+ lap fleet dataset for stability while letting the driver's own tendencies dominate the fit. Below a lap-count threshold per compound, the model automatically falls back to a simpler 2-segment fit rather than overfitting on 6 points.
Degradation excess (not baseline pace) is scaled against live session weather data — hotter track accelerates the cliff, colder track flattens it — clamped to a sane range to prevent runaway extrapolation on extreme readings.
Most public strategy tools only model the undercut. This engine evaluates both directions against a configurable rival (their compound, tyre age, and gap to you) and reports mean EV, P10–P90 range, and win rate for each candidate lap — visualised as a single comparison chart showing which strategy is favoured on which lap.
EV(lap_n) = gap_to_us − gap_after_strategy(lap_n)
Both formulas were rebuilt from scratch this cycle after a real bug was found: the original implementation mixed an absolute lap time into a relative gap delta, double-counting pace on top of the pit stop cost. See docs/decisions.md for the full story.
The simulation engine pre-computes deterministic lap-time vectors per strategy, then generates noise as a (n_laps × n_simulations) NumPy matrix in one shot — no per-iteration Python loop.
base = build_lap_time_vector(strategy, deg_model, pit_loss) # (n_laps,)
noise = rng.normal(0, noise_std, size=(n_laps, n_simulations)) # (n_laps, n_sims)
times = (base[:, None] + noise).sum(axis=0) # (n_sims,)Reconstructs what the selected driver actually did that race — compound sequence and stint durations, pulled directly from FastF1 tyre-life data — and compares deterministic race time against the AI-optimal strategy, laid over a shared lap trace.
- Python 3.11+
- ~500MB disk (FastF1 cache)
git clone https://github.com/Trixx4191/F1-Strategy-AI.git
cd f1-strategy-ai
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtPYTHONPATH=$(pwd) streamlit run src/visualisation/dashboard.pyOpen http://localhost:8501 — select a season, race, and driver in the sidebar, then hit RUN SIMULATION. Every season 2018–2024 and every event in that season's calendar is available — nothing is hardcoded.
First run: FastF1 downloads session data (~50–200MB depending on race). Subsequent runs use the local cache.
docker compose up --buildDashboard available at http://localhost:8501. The FastF1 cache persists in a named volume across restarts.
PYTHONPATH=$(pwd) pytest tests/ -v91 tests across the data pipeline, the piecewise tyre model (fitting, cliff detection, temperature scaling, driver weighting), and the rival engine (undercut, overcut, and the regression tests that caught two real bugs — see below).
All parameters are in src/utils/config.py:
N_SIMULATIONS = 1000 # MC iterations (100–2000 via dashboard slider)
LAP_TIME_NOISE_STD = 0.3 # Gaussian noise per lap (seconds)
DEG_MIN_LAPS_FULL = 12 # Minimum laps for a full 3-segment piecewise fit
DEG_MIN_LAPS_FALLBACK = 6 # Minimum laps for a 2-segment fallback fit
DEG_CLIFF_THRESHOLD = 0.12 # s/lap — minimum slope to call a segment a "cliff"Season, race, and driver are selected live in the dashboard sidebar — pulled from FastF1's event schedule, not hardcoded.
| Package | Version | Purpose |
|---|---|---|
fastf1 |
≥ 3.3 | Official F1 timing, telemetry, and weather data |
pandas |
≥ 2.0 | Lap data manipulation |
numpy |
≥ 1.26 | Vectorised Monte Carlo |
pwlf |
≥ 0.5 | Piecewise linear fitting for the 3-phase degradation model |
scikit-learn |
≥ 1.4 | Pipeline utilities |
scipy |
≥ 1.12 | Statistical distributions, confidence intervals |
streamlit |
≥ 1.32 | Interactive dashboard |
matplotlib |
≥ 3.8 | Strategy & tyre visualisations |
This project has been through several rounds of adversarial testing rather than feature-only development. Two are worth calling out specifically because they were subtle, silent, and would have shipped unnoticed without deliberate regression tests:
- A unit-mixing bug in the undercut/overcut gap formula — an absolute lap time (~91s) was being combined with a relative gap variable (~2–10s) in the same equation, effectively double-counting pace on top of the pit stop cost. Found via a new test asserting overcut should be favourable when a rival is far behind — the assertion failed, which led to the real bug rather than a test bug.
- A silent column-loss bug in the cleaning pipeline — a
pandas.groupby().apply(..., include_groups=False)call (added to silence aFutureWarning) was quietly dropping theCompound,Driver, andStintNumbercolumns from the pipeline's output entirely. Caught via an end-to-end smoke test against real FastF1 data, not a unit test — the unit tests happened to use single-driver fixtures that couldn't expose it.
Full writeup: docs/decisions.md.
- Circuit cluster multi-race training — pool laps across circuits with similar degradation character to solve the underlying data-poverty problem (15–18 laps/compound/race → 300+)
- Safety car Monte Carlo layer — sample SC events from historical
TrackStatusfrequency and re-evaluate pit timing when one fires - Strategy history tab — pit lap heatmap by year for the selected circuit, overlaid with the winning strategy
- Telemetry analysis module — track map, speed/brake/throttle traces, delta-to-reference-lap, sector breakdown
- Grounded race engineer — natural-language strategy/performance questions answered with evidence traced directly to telemetry, degradation model, and weather data
See docs/improvements.md for the full backlog.
Strategy is where races are won and lost — the undercut that works, the tyre cliff nobody saw coming, the pit call made in three seconds with imperfect information. This project exists to explore whether a rigorously-engineered data pipeline and probabilistic simulation can approximate that reasoning — and to do it in a way that's open, testable, and honest about its own limitations and past mistakes.
MIT — see LICENSE.
Built by a computer engineering student and F1 enthusiast. If you work in motorsport data, simulation, or performance engineering — let's talk.