01The short version
BoxBox solves a supervised classification problem. Each example is one driver in one race, and the model answers: what's the chance this driver wins, finishes 2nd or 3rd, or misses the podium?
Think of it as a pure function f(features) → [p_win, p_podium, p_rest] whose parameters nobody writes by hand: they are fitted by looking at 2,234 real examples (from 2022-R1 to 2026-R18, 110 races). That's all training is: finding the parameters that make the function as right as possible on data we already know, hoping it stays right on data it hasn't seen.
That last sentence is the whole craft. A model that's only right on its training data is useless. Almost everything below (how features are built, how it's validated, why the model is small) exists so the model generalises to races it hasn't seen.
02The pipeline, end to end
If you come from software, an ML system looks more like a build than a feature: an offline phase produces an artefact (the trained model) and an online phase loads it and serves requests. Every step lives in the monorepo: boxbox-ml/ (data and training), backend/ (API) and frontend/ (this site).
- 1Extract
boxbox_ml.extractRace results and qualifying for every GP from Jolpica (the Ergast API's successor), and the weather for the first two hours of the race from Open-Meteo. Stored as Parquet, one row per driver per race.
- 2Features
boxbox_ml.features
03The data
One row of the dataset is one driver in one Grand Prix: starting slot, qualifying position, gap to pole in %, team, circuit, weather and how they finished. That's 2,234 rows from 110 races.
We start in 2022 on purpose. That year the technical rules changed (ground-effect cars), and what used to hold, such as who was quick or how much overtaking happened, doesn't carry over well. In ML, more data isn't always better: data from a different world than the one you're predicting teaches things that are no longer true. This is called distribution shift.
Historical weather comes from ERA5 reanalysis, an hour-by-hour reconstruction of the atmosphere, averaged over the two hours from the start. Upcoming races use the forecast, with the same aggregation code. More than 1 mm of rain in that window makes the race count as wet. The threshold was chosen from real cases: Suzuka 2025 was dry and ERA5 showed 0.5 mm.
What we don't have: telemetry, lap times, tyre strategy or pit stops. FastF1 offers them, but it needs a paid F1 TV subscription. It's a real limit of the model, and worth keeping in mind when you read its predictions.
04What exactly we predict
The target (or label) is what the model learns to predict. We could predict the exact position (1, 2, 3… 22), but that's very noisy: finishing 14th or 15th comes down to near-random things we don't care about. So the outcome is grouped into three classes:
- Class 0 · WinFinished 1st. Roughly 1 row in 22.
- Class 1 · Podium, no winFinished 2nd or 3rd. About 2 in 22.
- Class 2 · RestEverything else, retirements included.
For each driver the model returns three probabilities that add up to 1. The podium chance is simply p(win) + p(2nd–3rd), so it can never be lower than the win chance. Two separate models (one for wins, one for podiums) could contradict each other.
Notice the imbalance: winning is rare, about 4–5 % of rows. A dumb model that said “nobody ever wins” would be 95 % accurate and completely useless. That's why accuracy isn't our main metric; more on that in the metrics section.
05Features: where it's won or lost
A feature is a numeric column the model can use to decide. The algorithm only sees numbers: it doesn't know what a driver is or what Monaco is. Feature engineering means translating domain knowledge into useful columns. With little data, as here, it usually matters more than the choice of algorithm.
These are the model's 18 features, grouped by the question they answer:
Where do they start?
- Grid slot
grid - By far the strongest feature. Pit-lane starters are placed behind the last car.
- Qualifying position
quali_position - Usually equal to the grid slot unless there are penalties. When they differ, it gives away a quick car starting further back.
- Gap to pole (%)
quali_gap_pct - How much slower they were in qualifying, as a percentage. Tells a 0.05 % pole from a 0.5 % one.
What's the weather?
06The algorithm: XGBoost
The model is a gradient-boosted ensemble of decision trees (the XGBoost library). It's the go-to for tabular data, meaning rows and columns like a spreadsheet. On problems like this it usually beats neural networks, especially with little data.
A decision tree is a stack of if/else statements learned from data. The algorithm tries splits like “grid ≤ 2?” or “rain = 1?” and keeps the ones that best separate winners from everyone else. One tree on its own is readable but clumsy.
if grid ≤ 2:
if team_avg_points_5 > 30: add +1.2 to “win”
else: add +0.4 to “win”
else:
if rainfall = 1: add +0.1 to “win”
else: add −0.9 to “win”Boosting means training trees in sequence, each one small and focused on fixing the errors left by the ones before. It's a bit like refactoring in iterations: the first tree catches the big picture (starting up front helps), the next one looks at the cases the first got wrong (the driver who started 6th and won in the rain), and so on. The final prediction is the of every tree, turned into three probabilities with a .
07Validating without cheating
To know whether a model works you have to test it on data it didn't see during training. The textbook recipe is to shuffle the rows and hold out a random 20 %. Here that would be a serious mistake: the model would train on 2026 races to “predict” 2024 ones. That's peeking at the future, and the future (which car is quick this year) is exactly what we're trying to predict.
When data depends on time, validation has to respect the order. We use a race-by-race walk-forward backtest, which mirrors exactly how the model would be used in real life:
- 1Before every GP of the last three seasons, a new model is trained only on earlier races.
- 2That model predicts the GP's whole field.
- 3The prediction is saved and compared with the real result.
- 4
08How we measure it
There's no single metric: each one answers a different question. These come from the backtest (66 races), compared with a baseline.
Log loss
lower is betterChecks whether the probabilities are good, not just the order. It punishes being confidently wrong very hard. Say 95 % and the driver doesn't win: huge penalty. Say 30 % and they don't win: barely anything. It's the metric training optimises.
09From loose probabilities to a coherent board
The model scores each driver independently: to it, every row stands alone. So if you add up all 22 drivers' win chances you don't get exactly 100 %. It might be 93 % or 108 %, which makes no sense in a race with one winner.
The API fixes that after predicting: it rescales the win chances to add up to 100 % and the podium chances to add up to 300 % (there are three places). It respects two rules: no podium chance can go above 100 %, and none can be lower than the win chance. If a driver would go over 100 %, they're held at the cap and the rest is shared among the others (known as water-filling).
It's a small step, but it shows something important: a model's raw output is almost never what you show the user. There's always a layer of product logic on top.
10Which features matter most
XGBoost keeps track of how much the prediction improved each time it used a feature to split a tree. This is called gain. Shared out as percentages, it hints at what the deployed model pays attention to:
- Grid slot
grid30% - Qualifying position
quali_position29% - Average points (last 5)
drv_avg_points_55.7% - Team points (last 5)
team_avg_points_5
11In production
The trained model is a model.joblib file of about 300 KB, versioned in the repository. It holds the trees, the list of known circuits and the results history needed to compute recent form. The API (FastAPI, on Render) loads it once at startup, and from then on each prediction takes milliseconds.
The calendar comes from Jolpica and each race's forecast from Open-Meteo. Both are kept in memory for an hour, so the site doesn't depend on those services answering on every visit. The forecast only arrives once the race is 14 days away or less; before that, the model works without weather and assumes a dry track.
Every prediction requested is logged in Postgres (Neon) with the model version. Later on, that makes it possible to measure whether the model gets worse over time (model drift), which in F1 happens as soon as a team brings car upgrades.
12Automatic retraining after every GP
An F1 model goes stale fast. The form features (points over the last 5 races, average finish, retirements…) are only current if the model knows the latest race, and every weekend brings something new: a car that improves, a driver losing pace. Without retraining, the model keeps predicting with the world of weeks ago.
So retraining is automated with GitHub Actions, the repository's own CI, in .github/workflows/retrain.yml. It's exactly what you'd do by hand, but run by a machine, with checks that stop a worse model from shipping.
What it does, step by step
13Honest limits
- Little data. About 2,000 rows and only around a hundred winners. Each rare win (a driver winning from 9th in the rain) weighs a lot, and it's hard to tell pattern from noise.
- Half the sport is missing. Without telemetry, race pace, strategy, tyres or safety cars, the model can't see many of the things that decide a race.
- The world changes. The 2026 rules changed the cars, and team form shifts with every upgrade. Weighting recent races more, or detecting these changes automatically, is a natural next step.
- You set the grid. Before qualifying, the model uses the last race's starting order as a guess. Its quality depends on that grid looking like the real one.
- Probabilities are probabilities. 40 % means that, in similar situations, that driver wins about 4 times in 10. Losing today doesn't prove the model wrong, and winning doesn't prove it right either. To judge it you have to look at many races: that's what the Track record is for.
14Quick glossary
- Feature
- An input column the model uses to decide.
- Target / label
- What you want to predict; here, win, podium or rest.
- Training
- Fitting the model's parameters so it's right on data we already know.
- Hyperparameter
- Algorithm configuration set before training (tree depth, learning rate…).
- Overfitting
- Memorising training noise: good in tests, bad on new data.
- Data leakage
- Information that wouldn't exist at prediction time sneaking into training.
- Walk-forward
- Validation that respects time: predict each moment using only the past.
- Baseline
- A simple solution to compare against; if you can't beat it, the model isn't worth it.
- Calibration
- When the model says 30 %, it really happens about 30 % of the time.
- Early stopping
- Stop adding trees once the error on held-out data stops falling.