diff --git a/docs/blog-assets/06_routines_infographic.png b/docs/blog-assets/06_routines_infographic.png new file mode 100644 index 0000000..43cca8b Binary files /dev/null and b/docs/blog-assets/06_routines_infographic.png differ diff --git a/docs/blog-assets/06_routines_infographic.svg b/docs/blog-assets/06_routines_infographic.svg index 0b99335..49d3e42 100644 --- a/docs/blog-assets/06_routines_infographic.svg +++ b/docs/blog-assets/06_routines_infographic.svg @@ -39,7 +39,7 @@ 3 · DIGEST · 19:23 daily autopilot-digest · Sonnet - summarises, never decides + summarizes, never decides • what merged, what was held • DECIDED FOR YOU: every call the spec left open, with the rejected option diff --git a/docs/blog-assets/07_singapore_range.png b/docs/blog-assets/07_singapore_range.png new file mode 100644 index 0000000..d966b8d Binary files /dev/null and b/docs/blog-assets/07_singapore_range.png differ diff --git a/docs/blog-assets/08_singapore_podium.png b/docs/blog-assets/08_singapore_podium.png new file mode 100644 index 0000000..4b14d54 Binary files /dev/null and b/docs/blog-assets/08_singapore_podium.png differ diff --git a/docs/blog-grid-prophet-overhaul.md b/docs/blog-grid-prophet-overhaul.md index 1229cf4..0bb18a0 100644 --- a/docs/blog-grid-prophet-overhaul.md +++ b/docs/blog-grid-prophet-overhaul.md @@ -1,16 +1,22 @@ -# I let routines rebuild my F1 model while I was away, then graded it on a race that hasn't happened yet +# I rebuilt my F1 model, let routines build out parts of it, and graded it on Singapore -Oct 11, 2026 · [x] min read · Tags: Project, Ai +**TL;DR** -My first F1 predictions were two one-off repos, one for Monaco '25 and one for Hungary '25, each pulling lap times and driver history out of FastF1 and guessing a winner, and I posted them on LinkedIn with a promise that a more detailed writeup was coming. This is that writeup, a few months late, and since the project grew into something else in the meantime it also covers the rebuild, the routines that did a lot of the building, and the model's first prediction for a race that hasn't happened yet. +- Backtest, 88 rounds from 2022 to 2025: knowing qualifying cut winner log loss[^1] from 1.801 to 1.258, mostly because knowing the grid helps. +- Singapore: winner log loss 0.376, and the model's top three podium odds (Verstappen, Leclerc, Antonelli) were the podium, in a race that was easy to call. +- What broke: the warm-start refit took 1,744 seconds against a 120 second target, and I don't know why yet. -## Version one, and why I kept poking at it +## What that means + +- The new model beat every baseline in a 2022-2025 backtest (winner log loss 1.80 pre-weekend, 1.26 with qualifying known), mostly because the grid is informative. +- On Singapore it called the podium and scored 0.376, but one easy race proves little. +- The refit that normally takes about a minute took 1,744 seconds, and the cause is still open. -Grid Prophet started in early April as a season-level model for the 2026 constructors' championship, which seemed worth trying in a year where the regulations changed and nobody knew who would be fast. It was XGBoost against Ridge on ~120 rows (one per constructor per season), with leave-one-season-out cross-validation, bootstrapped confidence intervals, and a variable for how teams usually behave in rule-change years. It worked well enough (Spearman ~0.88 on that leave-one-season-out CV, which is the number the new model eventually has to beat on season-level ranking), but it could only answer one question, which was who finishes where in the standings, and it had nothing to say about a single race. +I went away for six days in late September and left scheduled agents[^2] extending the next phase of my F1 prediction model. Since then I've scored it on the Singapore GP, and this post covers what worked, what the numbers do and don't prove, and the one thing that broke. -## How it got picked again +## Version one, and why I kept poking at it -I've been trying to keep a streak going of one meaningful commit a day across whatever side project needs it most, and the hard part is never the work, it's picking *which* project deserves the day. In August I had an agent audit all ~20 of my repos for TODOs, doc claims with no code behind them, and stale checklists, and grid-prophet came back with an untrained `--ensemble` model artifact. I ended up fixing a different repo that day (a trash classifier whose production function had been throwing on every request, because the local dev server and the deployed handler were two files that had quietly drifted apart), but the lesson from that one followed me back here, and I'll get to why. +Grid Prophet started in early April as a season-level model for the 2026 constructors' championship, which seemed worth trying in a year where the regulations changed and nobody knew who would be fast. It was XGBoost against Ridge on ~120 rows (one per constructor per season), with leave-one-season-out cross-validation, bootstrapped confidence intervals, and a variable for how teams usually behave in rule-change years. It worked well enough (Spearman[^3] ~0.88 on that leave-one-season-out CV, which is the number the new model eventually has to beat on season-level ranking), but it could only answer one question, which was who finishes where in the standings, and it had nothing to say about a single race. ## The rebuild @@ -22,39 +28,39 @@ Two things had to give along the way. The spec preferred NumPyro and LightGBM, b Before I went away for six days in late September I wanted the project to keep moving without me being the bottleneck, so I broke Phase 2 of the spec into slices and set up four scheduled cloud routines, which are drawn below. Only the first three slices (post-quali conditioning, sprints, and the reliability split) were queued for that window, and the other three (a fitted overtaking parameter per circuit, wet-race noise, and some carry-forwards) were queued on October 1 once the queue ran dry. A build routine takes the next slice off a queue file and opens a pull request (one build routine for this repo and another for a different project of mine, aperture), a separate review routine checks every acceptance criterion against the diff rather than the PR description, and a digest routine writes me a summary I can read on my phone. -![How the four routines, the queue file and the branches fit together](blog-assets/06_routines_infographic.svg) +![Scheduled routines build, review and summarize work on a side branch, so main stays untouched until I review one pull request.](blog-assets/06_routines_infographic.svg) -The part I care about most is that `main` never moves, everything lands on a branch called `auto/queue`, and I review it as one pull request afterwards, so if it all went wrong I could delete a branch and lose nothing. Funnily enough the review routine earned its keep, because it blocked slice 1 outright since it couldn't run the acceptance criterion without real data, so I committed an 80-row fixture from 2024 and the review went through after that. Somewhere along the way it also came out that `gp fit` crashed on Python 3.12 (sampling worked, saving the result didn't, because a newer arviz stopped pulling in a netCDF backend), and the review of the overtaking slice found that the overtaking fit could be deleted from both forecast call sites with every test still passing, which is the same disease as the trash classifier: everything looks green and the real path isn't being exercised. At one point two agents also wrote the same merge status within minutes of each other and I still don't know if the review cron fired twice. +The part I care about most is that `main` never moves, everything lands on a branch called `auto/queue`, and I review it as one pull request afterwards, so if it all went wrong I could delete a branch and lose nothing. Funnily enough the review routine earned its keep, because it blocked slice 1 outright since it couldn't run the acceptance criterion without real data, so I committed an 80-row fixture from 2024 and the review went through after that. Somewhere along the way it also came out that `gp fit` crashed on Python 3.12 (sampling worked, saving the result didn't, because a newer arviz stopped pulling in a netCDF backend), and the review of the overtaking slice found that the overtaking fit could be deleted from both forecast call sites with every test still passing, which is the same disease I found in a trash classifier of mine, whose production handler was failing on every request while its local dev server, a separate file, worked fine: everything looks green and the real path isn't being exercised. Two agents once wrote the same merge status minutes apart, and I never found out whether the review cron fired twice. ## Does it work? For the backtest I ran 88 rounds from 2022 to 2025, refitting before every round (every round except the first of each season, which is why it's 88 and not 92), which took 2.7 hours on one laptop, and FastF1 rate-limits you at 500 calls an hour so collecting the data meant a loop of re-run, wait 20 minutes, re-run. 2025 Miami also has no qualifying times upstream so I rebuilt that round by hand from the session laps. -![Bar chart of mean winner log loss: pole always wins 3.22, plain pace rating 2.10, XGBoost challenger 2.06, Bayesian pre-weekend 1.80, Bayesian post-quali 1.26](blog-assets/01_baselines.png) +![Pre-weekend model beats three baselines; adding the qualifying results cuts winner log loss from 1.80 to 1.26 (lower is better).](blog-assets/01_baselines.png) -The pre-weekend model beats all three baselines, and post-quali mode, where the model refits on the real qualifying session before forecasting, gets down to **1.258**. I will say that number deserves suspicion, because most of that gain is simply knowing the grid and the backtest can't separate that from anything clever the refit does. +The pre-weekend model beats all three baselines, and post-quali mode, where the model refits on the real qualifying session before forecasting, gets down to a winner log loss of **1.258**. I will say that number deserves suspicion, because most of that gain is simply knowing the grid and the backtest can't separate that from anything clever the refit does. -![Bar chart of per-round change in log loss from conditioning on qualifying, better in 75 rounds and worse in 13](blog-assets/02_paired_delta.png) +![Conditioning on qualifying improved log loss in 75 of 88 rounds and worsened it in 13, with a few large misses.](blog-assets/02_paired_delta.png) It's better in 75 of 88 rounds and worse in 13, some of them badly (2022 Round 13 went from 1.51 to 3.90), and those are the rounds where the real grid pointed confidently at someone who then didn't win. The gain also isn't spread evenly, every season improved but 2022 improved the least. -![Grouped bars of mean winner log loss by season, pre-weekend against post-quali](blog-assets/03_by_season.png) +![Every season improved after conditioning on qualifying, with 2022 improving the least.](blog-assets/03_by_season.png) The refit itself never became a problem. The warm start, which starts from the pre-weekend posterior instead of fitting from scratch, averaged ~73 seconds against a 120 second target and never once had to fall back to a full refit. -![Scatter of 88 warm-start refit times against the 120 second target](blog-assets/04_warm_start.png) +![All 88 warm-start refits finished under the 120-second target, averaging about 73 seconds; none needed the full-refit fallback.](blog-assets/04_warm_start.png) ## Is this the best model I could have made? No, and the clearest evidence is the calibration plot for the pre-weekend model, which I only made while writing this post. -![Calibration plot of predicted podium probability against how often it happened, showing the model is too timid at the front and too generous at the back](blog-assets/05_calibration.png) +![Pre-weekend model is too timid at the front, too generous at the back: ~55% podium picks happened ~77% of the time; ~3% picks, ~0.5%.](blog-assets/05_calibration.png) When the model says a driver has a ~55% chance of a podium it happens ~77% of the time (though that bin only has 27 driver-rounds in it, so treat the exact number loosely), and when it says ~3% it happens ~0.5% of the time across 564 driver-rounds, so it's too timid at the front and too generous at the back. I never checked calibration for the post-quali model at all, so I can't tell you which way it leans. I also never ran a slice-by-slice ablation, which means I can't say which of the six slices are earning their keep, the 0.54 improvement is one run with one seed per round and no confidence interval, and there's no Phase 3 yet (the drivers' and constructors' championship simulation), so the old model's 0.88 hasn't actually been compared against. -## Predictions for this weekend +## The Singapore prediction -This is the Singapore GP, round 17, and since it's a sprint weekend the sprint has already run, so this is only about Sunday. I refreshed the data, refit through round 16, and conditioned on qualifying with the warm-start refit, which took 47 seconds. The race grid normally comes from the race rows, which don't exist yet, so I added a flag that takes it straight from the qualifying classification, which means **grid penalties are not in here**. +This was the Singapore GP, round 17, and since it was a sprint weekend the sprint had already run, so this is only about Sunday. Before the race I refreshed the data, refit through round 16, and conditioned on qualifying with the warm-start refit, which took 47 seconds. The race grid normally comes from the race rows, which don't exist yet, so I added a flag that takes it straight from the qualifying classification, which means **grid penalties are not in here**. - **Verstappen:** ~69% to win, ~86% for a podium, ~20.5 expected points - **Leclerc:** ~19% to win, ~79% for a podium @@ -62,6 +68,31 @@ This is the Singapore GP, round 17, and since it's a sprint weekend the sprint h - **Hamilton:** ~4.5% to win, ~51% for a podium - **Norris:** ~1% to win, ~13% for a podium -The part I find interesting is that Hamilton qualified ahead of Antonelli and the model still gives Antonelli a slightly better shot at the podium. Everyone from Alpine down is under 0.1% to win, and the DNF risk sits around 10% for most of the front, so the 17th in Verstappen's 90th percentile finish is the DNF tail rather than a slow car. +The part I find interesting is that Hamilton qualified ahead of Antonelli and the model still gives Antonelli a slightly better shot at the podium. Everyone from Alpine down is under 0.1% to win, and the DNF risk sits around 10% for most of the front, which is why Verstappen's forecast range runs all the way down to 17th. A percentile is just a cut-off in the simulations: in 90% of the 50,000 simulated races he finishes 17th or better, so the worst 10% sit beyond that, and a retirement counts as a back-of-the-field result, so that tail lines up with his chance of not finishing. In other words the 17th is the DNF tail, not a forecast that he'd be slow. + +## What actually happened + +Verstappen won from pole, Leclerc was second and Antonelli third, which is exactly the top three in the model's podium odds (86%, 79% and 55%), and Hamilton, the fourth name on that list at 51%, finished sixth. + +![The model's top three podium odds, Verstappen, Leclerc and Antonelli, finished 1st to 3rd; Hamilton, at 51%, finished 6th.](blog-assets/08_singapore_podium.png) + +Scored the same way as the backtest, the winner log loss came out at 0.376 against the backtest's post-quali average of 1.258 (better than 95% of those 88 rounds) and the Spearman against the real finishing order was 0.86 against 0.66, which sounds great until you notice that the pole-wins baseline scored 0.001 on this race and beat the model, because the pole-sitter won. A race where the favorite wins is the easiest kind to call, so I'm treating it as a good sign and not as evidence about calibration. + +![18 of 22 drivers landed inside the forecast range; Hadjar, Lindblad, Colapinto and Bortoleto did not.](blog-assets/07_singapore_range.png) + +18 of the 22 drivers landed inside their forecast range, against ~17.6 you'd expect from an 80% range, and the four outside it were Hadjar (started from the pit lane, finished 10th), Lindblad (forecast 9th, finished 15th), and Colapinto and Bortoleto (forecast ~13th to 14th, both retired and not classified). The starting grid was the other thing I knew would go wrong, because the forecast used the qualifying order and Russell took an engine penalty and started 19th instead of 6th (he finished 9th, which the forecast, expecting him around 5th, had comfortably inside its range). + +To see how much that mattered I re-ran the forecast from the real starting grid, and the answer was annoying: the winner log loss and podium Brier[^4] barely moved (0.375 and 0.025), but the points Brier got worse (0.123 to 0.180) and the Spearman dropped from 0.86 to 0.81, because with Russell starting 19th the model put him ~14th and he finished 9th. So ignoring the penalty happened to score better on this race, which is luck and not a reason to prefer the qualifying order. + +That re-run also hit the first warm-start failure I've seen. The warm-start refit took 1,744 seconds (~29 minutes) before it fell back to a full refit that took another 68, against a 120 second target and against 47 seconds for the pre-race run on the same model, and I haven't found out why yet. The spec treats a late-season miss as a real regression and not an accepted exception, so that's the first thing to look at, and until I do the "zero fallbacks in 88 rounds" line from the backtest is true but is not the whole story. + +## Where this leaves it + +Three things I'd keep from this. Green tests prove the code you tested and nothing else, and both the trash classifier and the overtaking fit looked fine until someone checked the path that actually runs. Most of the post-quali gain is simply knowing the grid, so the 1.258 says less about the model than it appears to. And one race, even one the model called well, tells you almost nothing about calibration, which is why Singapore is a good sign and not a result. + +What's next is a short list: work out why that warm start took half an hour, add Singapore to the post-quali calibration check, run a slice-by-slice ablation to see which of the six slices are earning their keep, and then build Phase 3, the drivers' and constructors' championship simulation, which is where the old model's 0.88 finally gets a fair comparison. I'll write up whatever breaks first. If you want to follow along, subscribe here or find me on X at @memeerav. -(Note to self: on Monday, score this against the actual result and write down how wrong it was, since that's the post-quali calibration data I still don't have) +[^1]: Winner log loss is a score for how much probability the model gave the driver who actually won, where lower is better (for Singapore, ~69% on the winner scored 0.376). +[^2]: These are the "routines" in the title: scheduled cloud agents, meaning AI jobs that start on a timer on someone else's machine and each do one job, like building a slice or reviewing a pull request. +[^3]: Spearman is a score from -1 to 1 for how closely a predicted order matches the real one, where 1 is identical. +[^4]: The Brier score is the average squared gap between a forecast probability and what happened (counted as 1 or 0), so lower is better and 0 would be perfect. diff --git a/docs/substack/portfolio-agent-prompt.md b/docs/substack/portfolio-agent-prompt.md new file mode 100644 index 0000000..de9dae8 --- /dev/null +++ b/docs/substack/portfolio-agent-prompt.md @@ -0,0 +1,41 @@ +# Starter prompt for the agent in the portfolio repo + +Fill in the two `<...>` values near the top (after you've made the publication and published the post), then paste everything inside the box into a Claude session opened in `C:\Users\meera\Github-Projects\MyPortfolio`. + +``` +You are working in my portfolio site repo (Next.js, deployed on Vercel at meeravshah.vercel.app). I now publish long posts on Substack, and I want them to show up in my site's /sidequests listing exactly like my other posts, except that clicking one opens the Substack post instead of a page on my site. + +Inputs (I will fill these in): +- Substack publication URL: +- First post to feature: +- The post's source and images are in C:\Users\meera\Github-Projects\grid-prophet\docs\ (blog-grid-prophet-overhaul.md and blog-assets\). Read them from there; do not copy the whole folder into this repo. + +First, orient yourself and report back before changing anything: +1. Check which repo is the deployed one. There is MyPortfolio and a nested MyPortfolio-2\MyPortfolio. Use the one whose git origin is Meerav29/MyPortfolio unless you find evidence otherwise, and tell me which you chose and why. +2. My MyPortfolio checkout has uncommitted work on branch animation-rework (components/Hero.tsx, components/PlanetCanvas.tsx, package-lock.json) and an untracked draft post (content/sidequests/2026-08-18-re-sike-beta-broken-in-prod.mdx, status "draft"). Do not stash, reset, commit, delete or publish any of that. Do all your work in a new git worktree branched from origin/main. +3. Read README_SIDEQUESTS.md, lib/sidequests.ts (zod frontmatter schema), app/sidequests/page.tsx, app/sidequests/[slug], components/sidequests/SidequestCard.tsx, and the /sidequests.xml feed route. Read any CLAUDE.md or AGENTS.md and follow it. + +Then do this in one small PR (never push to main): +A. Add an optional externalUrl to the Sidequest type and frontmatter schema. When it is set, the card links to that URL in a new tab (rel="noopener noreferrer") with a small "on Substack" marker, the internal /sidequests/ page is not generated for it, and /sidequests.xml and any sitemap use the external URL. Posts without externalUrl must behave exactly as today. +B. Make new Substack posts appear automatically: fetch /feed at build time (revalidate hourly if the site supports it), map each item to a Sidequest (title, publishedAt, excerpt from the description, thumbnail from the feed image if present else a default, tags from feed categories else ["Substack"], status "published", externalUrl from the item link) and merge it into the listing, de-duplicating on URL. If the feed fetch fails, the site must still build and show the local posts. No secrets are needed and none should be added. +C. If the feed doesn't yet contain the first post, add one MDX stub for it using externalUrl, with title "I rebuilt my F1 model, let routines build out parts of it, and graded it on Singapore", an excerpt drawn from the post's TL;DR, tags ["F1", "AI", "Project"], and the publish date from the Substack page. Copy ONE image from the blog assets (08_singapore_podium.png) to public/images/sidequests/ as its thumbnail. +D. Run the repo's lint, type check and production build, and fix what you broke. Open the PR with a description that lists what changed, how I can review it in 10 minutes, and what you did not do. Show screenshots of the listing page. + +Verify in a browser (use the browser tools available to you): +- Open the Substack post URL and confirm it loads without logging in, the 8 images render, and the footnotes show. Report anything broken rather than fixing it. +- Open the local dev server's /sidequests and confirm the Substack card appears in the right order, opens Substack in a new tab, and that an existing post still opens normally. +- Repeat on the Vercel preview URL for the PR once it exists. + +Hard rules: +- Never enter, request or store any password, token or credential. If a page asks me to log in, stop and tell me. +- Do not publish, schedule, send or edit anything on Substack, and do not change any account or site settings. Reading public pages is fine. +- Do not merge the PR. I will. +- If anything above is ambiguous or doesn't match what you find, ask me before guessing. +``` + +## Why it's built this way + +- **The RSS feed does the repeat work.** Every Substack publication has a feed at `/feed`, so once part B is merged, future posts appear on the site with no commits and no prompts. +- **The stub in step C is a fallback** for the gap between publishing and the feed updating, and for anything the feed describes poorly. +- **It works in its own worktree** so your in-progress animation changes and the draft post stay untouched. +- **It can't publish or log in for you.** Publishing to Substack stays your action. diff --git a/docs/substack/post-paste.html b/docs/substack/post-paste.html new file mode 100644 index 0000000..c414613 --- /dev/null +++ b/docs/substack/post-paste.html @@ -0,0 +1,74 @@ +Paste into Substack + + +

Copy from here

+

TL;DR

+
    +
  • Backtest, 88 rounds from 2022 to 2025: knowing qualifying cut winner log loss[1] from 1.801 to 1.258, mostly because knowing the grid helps.
  • +
  • Singapore: winner log loss 0.376, and the model's top three podium odds (Verstappen, Leclerc, Antonelli) were the podium, in a race that was easy to call.
  • +
  • What broke: the warm-start refit took 1,744 seconds against a 120 second target, and I don't know why yet.
  • +
+

What that means

+
    +
  • The new model beat every baseline in a 2022-2025 backtest (winner log loss 1.80 pre-weekend, 1.26 with qualifying known), mostly because the grid is informative.
  • +
  • On Singapore it called the podium and scored 0.376, but one easy race proves little.
  • +
  • The refit that normally takes about a minute took 1,744 seconds, and the cause is still open.
  • +
+

I went away for six days in late September and left scheduled agents[2] extending the next phase of my F1 prediction model. Since then I've scored it on the Singapore GP, and this post covers what worked, what the numbers do and don't prove, and the one thing that broke.

+

Version one, and why I kept poking at it

+

Grid Prophet started in early April as a season-level model for the 2026 constructors' championship, which seemed worth trying in a year where the regulations changed and nobody knew who would be fast. It was XGBoost against Ridge on ~120 rows (one per constructor per season), with leave-one-season-out cross-validation, bootstrapped confidence intervals, and a variable for how teams usually behave in rule-change years. It worked well enough (Spearman[3] ~0.88 on that leave-one-season-out CV, which is the number the new model eventually has to beat on season-level ranking), but it could only answer one question, which was who finishes where in the standings, and it had nothing to say about a single race.

+

The rebuild

+

The second version is a hierarchical Bayesian pace model that works per driver per race, with a latent car pace that drifts over the season, a driver skill term with a prior for rookies, and a driver-by-circuit-type interaction, all simulated forward 50,000 times so that win, podium, points and finishing-position probabilities come out of one engine instead of three separate questions. Reliability is its own piece, split into mechanical failures per team and incident DNFs per driver, because an engine failure and a first-lap collision are not the same kind of bad luck.

+

Two things had to give along the way. The spec preferred NumPyro and LightGBM, but jaxlib refuses to import on my Windows setup and LightGBM segfaults as soon as pandas is loaded, so it runs on PyMC and XGBoost, and PyMC turned out to be faster than I expected with a full fit over 2018-2026 taking ~80 seconds. One assumption also didn't survive: I had assumed clean-air race pace would track qualifying gap at ~0.8 Spearman, and after filtering out every lap spent behind another car it came out at ~0.6, which I checked as a possible bug in my filter and it wasn't, that's just how the data is.

+

The routines

+

Before I went away for six days in late September I wanted the project to keep moving without me being the bottleneck, so I broke Phase 2 of the spec into slices and set up four scheduled cloud routines, which are drawn below. Only the first three slices (post-quali conditioning, sprints, and the reliability split) were queued for that window, and the other three (a fitted overtaking parameter per circuit, wet-race noise, and some carry-forwards) were queued on October 1 once the queue ran dry. A build routine takes the next slice off a queue file and opens a pull request (one build routine for this repo and another for a different project of mine, aperture), a separate review routine checks every acceptance criterion against the diff rather than the PR description, and a digest routine writes me a summary I can read on my phone.

+
IMAGE 1 of 8: upload 06_routines_infographic.png here, then add this alt text: Scheduled routines build, review and summarize work on a side branch, so main stays untouched until I review one pull request.
+ +

The part I care about most is that main never moves, everything lands on a branch called auto/queue, and I review it as one pull request afterwards, so if it all went wrong I could delete a branch and lose nothing. Funnily enough the review routine earned its keep, because it blocked slice 1 outright since it couldn't run the acceptance criterion without real data, so I committed an 80-row fixture from 2024 and the review went through after that. Somewhere along the way it also came out that gp fit crashed on Python 3.12 (sampling worked, saving the result didn't, because a newer arviz stopped pulling in a netCDF backend), and the review of the overtaking slice found that the overtaking fit could be deleted from both forecast call sites with every test still passing, which is the same disease I found in a trash classifier of mine, whose production handler was failing on every request while its local dev server, a separate file, worked fine: everything looks green and the real path isn't being exercised. Two agents once wrote the same merge status minutes apart, and I never found out whether the review cron fired twice.

+

Does it work?

+

For the backtest I ran 88 rounds from 2022 to 2025, refitting before every round (every round except the first of each season, which is why it's 88 and not 92), which took 2.7 hours on one laptop, and FastF1 rate-limits you at 500 calls an hour so collecting the data meant a loop of re-run, wait 20 minutes, re-run. 2025 Miami also has no qualifying times upstream so I rebuilt that round by hand from the session laps.

+
IMAGE 2 of 8: upload 01_baselines.png here, then add this alt text: Pre-weekend model beats three baselines; adding the qualifying results cuts winner log loss from 1.80 to 1.26 (lower is better).
+ +

The pre-weekend model beats all three baselines, and post-quali mode, where the model refits on the real qualifying session before forecasting, gets down to a winner log loss of 1.258. I will say that number deserves suspicion, because most of that gain is simply knowing the grid and the backtest can't separate that from anything clever the refit does.

+
IMAGE 3 of 8: upload 02_paired_delta.png here, then add this alt text: Conditioning on qualifying improved log loss in 75 of 88 rounds and worsened it in 13, with a few large misses.
+ +

It's better in 75 of 88 rounds and worse in 13, some of them badly (2022 Round 13 went from 1.51 to 3.90), and those are the rounds where the real grid pointed confidently at someone who then didn't win. The gain also isn't spread evenly, every season improved but 2022 improved the least.

+
IMAGE 4 of 8: upload 03_by_season.png here, then add this alt text: Every season improved after conditioning on qualifying, with 2022 improving the least.
+ +

The refit itself never became a problem. The warm start, which starts from the pre-weekend posterior instead of fitting from scratch, averaged ~73 seconds against a 120 second target and never once had to fall back to a full refit.

+
IMAGE 5 of 8: upload 04_warm_start.png here, then add this alt text: All 88 warm-start refits finished under the 120-second target, averaging about 73 seconds; none needed the full-refit fallback.
+ +

Is this the best model I could have made?

+

No, and the clearest evidence is the calibration plot for the pre-weekend model, which I only made while writing this post.

+
IMAGE 6 of 8: upload 05_calibration.png here, then add this alt text: Pre-weekend model is too timid at the front, too generous at the back: ~55% podium picks happened ~77% of the time; ~3% picks, ~0.5%.
+ +

When the model says a driver has a ~55% chance of a podium it happens ~77% of the time (though that bin only has 27 driver-rounds in it, so treat the exact number loosely), and when it says ~3% it happens ~0.5% of the time across 564 driver-rounds, so it's too timid at the front and too generous at the back. I never checked calibration for the post-quali model at all, so I can't tell you which way it leans. I also never ran a slice-by-slice ablation, which means I can't say which of the six slices are earning their keep, the 0.54 improvement is one run with one seed per round and no confidence interval, and there's no Phase 3 yet (the drivers' and constructors' championship simulation), so the old model's 0.88 hasn't actually been compared against.

+

The Singapore prediction

+

This was the Singapore GP, round 17, and since it was a sprint weekend the sprint had already run, so this is only about Sunday. Before the race I refreshed the data, refit through round 16, and conditioned on qualifying with the warm-start refit, which took 47 seconds. The race grid normally comes from the race rows, which don't exist yet, so I added a flag that takes it straight from the qualifying classification, which means grid penalties are not in here.

+
    +
  • Verstappen: ~69% to win, ~86% for a podium, ~20.5 expected points
  • +
  • Leclerc: ~19% to win, ~79% for a podium
  • +
  • Antonelli: ~5% to win, ~55% for a podium
  • +
  • Hamilton: ~4.5% to win, ~51% for a podium
  • +
  • Norris: ~1% to win, ~13% for a podium
  • +
+

The part I find interesting is that Hamilton qualified ahead of Antonelli and the model still gives Antonelli a slightly better shot at the podium. Everyone from Alpine down is under 0.1% to win, and the DNF risk sits around 10% for most of the front, which is why Verstappen's forecast range runs all the way down to 17th. A percentile is just a cut-off in the simulations: in 90% of the 50,000 simulated races he finishes 17th or better, so the worst 10% sit beyond that, and a retirement counts as a back-of-the-field result, so that tail lines up with his chance of not finishing. In other words the 17th is the DNF tail, not a forecast that he'd be slow.

+

What actually happened

+

Verstappen won from pole, Leclerc was second and Antonelli third, which is exactly the top three in the model's podium odds (86%, 79% and 55%), and Hamilton, the fourth name on that list at 51%, finished sixth.

+
IMAGE 7 of 8: upload 08_singapore_podium.png here, then add this alt text: The model's top three podium odds, Verstappen, Leclerc and Antonelli, finished 1st to 3rd; Hamilton, at 51%, finished 6th.
+ +

Scored the same way as the backtest, the winner log loss came out at 0.376 against the backtest's post-quali average of 1.258 (better than 95% of those 88 rounds) and the Spearman against the real finishing order was 0.86 against 0.66, which sounds great until you notice that the pole-wins baseline scored 0.001 on this race and beat the model, because the pole-sitter won. A race where the favorite wins is the easiest kind to call, so I'm treating it as a good sign and not as evidence about calibration.

+
IMAGE 8 of 8: upload 07_singapore_range.png here, then add this alt text: 18 of 22 drivers landed inside the forecast range; Hadjar, Lindblad, Colapinto and Bortoleto did not.
+ +

18 of the 22 drivers landed inside their forecast range, against ~17.6 you'd expect from an 80% range, and the four outside it were Hadjar (started from the pit lane, finished 10th), Lindblad (forecast 9th, finished 15th), and Colapinto and Bortoleto (forecast ~13th to 14th, both retired and not classified). The starting grid was the other thing I knew would go wrong, because the forecast used the qualifying order and Russell took an engine penalty and started 19th instead of 6th (he finished 9th, which the forecast, expecting him around 5th, had comfortably inside its range).

+

To see how much that mattered I re-ran the forecast from the real starting grid, and the answer was annoying: the winner log loss and podium Brier[4] barely moved (0.375 and 0.025), but the points Brier got worse (0.123 to 0.180) and the Spearman dropped from 0.86 to 0.81, because with Russell starting 19th the model put him ~14th and he finished 9th. So ignoring the penalty happened to score better on this race, which is luck and not a reason to prefer the qualifying order.

+

That re-run also hit the first warm-start failure I've seen. The warm-start refit took 1,744 seconds (~29 minutes) before it fell back to a full refit that took another 68, against a 120 second target and against 47 seconds for the pre-race run on the same model, and I haven't found out why yet. The spec treats a late-season miss as a real regression and not an accepted exception, so that's the first thing to look at, and until I do the "zero fallbacks in 88 rounds" line from the backtest is true but is not the whole story.

+

Where this leaves it

+

Three things I'd keep from this. Green tests prove the code you tested and nothing else, and both the trash classifier and the overtaking fit looked fine until someone checked the path that actually runs. Most of the post-quali gain is simply knowing the grid, so the 1.258 says less about the model than it appears to. And one race, even one the model called well, tells you almost nothing about calibration, which is why Singapore is a good sign and not a result.

+

What's next is a short list: work out why that warm start took half an hour, add Singapore to the post-quali calibration check, run a slice-by-slice ablation to see which of the six slices are earning their keep, and then build Phase 3, the drivers' and constructors' championship simulation, which is where the old model's 0.88 finally gets a fair comparison. I'll write up whatever breaks first. If you want to follow along, subscribe here or find me on X at @memeerav.

+

Footnote text (turn these into Substack footnotes)

  • [1] Winner log loss is a score for how much probability the model gave the driver who actually won, where lower is better (for Singapore, ~69% on the winner scored 0.376).
  • [2] These are the "routines" in the title: scheduled cloud agents, meaning AI jobs that start on a timer on someone else's machine and each do one job, like building a slice or reviewing a pull request.
  • [3] Spearman is a score from -1 to 1 for how closely a predicted order matches the real one, where 1 is identical.
  • [4] The Brier score is the average squared gap between a forecast probability and what happened (counted as 1 or 0), so lower is better and 0 would be perfect.
+ \ No newline at end of file diff --git a/docs/substack/publishing-guide.md b/docs/substack/publishing-guide.md new file mode 100644 index 0000000..6d8f4df --- /dev/null +++ b/docs/substack/publishing-guide.md @@ -0,0 +1,72 @@ +# Publishing the Grid Prophet post on Substack + +I couldn't open Substack's editor while writing this, so button names below come from Substack's help pages and may differ slightly on screen. Where I'm unsure I say so. + +## What Substack will and won't take + +- **No file or zip upload for a single post.** Substack's importer exists to move a whole publication over from another platform (Medium, WordPress, Ghost and similar), not to load one file. A single post is typed or pasted into the editor. +- **Markdown is not reliable.** Substack's help page says its post editor doesn't support Markdown, though that page is old, so it may have changed. Don't rely on pasting the `.md` file. +- **Images are uploaded one at a time** (drag into the draft, or use the image icon). Accepted formats are avif, gif, jpg, jpeg, png and webp. **SVG is not on the list**, so the routines infographic must be uploaded as the PNG (`06_routines_infographic.png`), which is already in `docs/blog-assets/`. +- **Alt text** is set per image: hover the image, open the three-dot menu in its corner, choose "Edit alt text". + +Sources: [media in posts](https://support.substack.com/hc/en-us/articles/360037832971-How-do-I-embed-media-in-my-post-e-g-images-video-GIFs), [Markdown support](https://support.substack.com/hc/en-us/articles/360037463132-Do-you-support-Markdown), [importing](https://support.substack.com/hc/en-us/articles/360037830351-How-do-I-import-my-posts-from-another-platform-such-as-Mailchimp-WordPress-Medium-or-Ghost), [alt text](https://support.substack.com/hc/en-us/articles/48404618511508-How-do-I-add-alt-text-to-an-image-on-a-Substack-post), [RSS feed](https://support.substack.com/hc/en-us/articles/360038239391-Is-there-an-RSS-feed-for-my-publication). + +## The easy path: paste the rendered text, then add images + +1. Open `docs/substack/post-paste.html` in your browser. It is the post with the title removed, each image replaced by a numbered orange placeholder, and the footnote markers shown as superscript [1] to [4]. +2. Select everything under "Copy from here" and copy. + +## Steps in Substack + +**A. One-time setup (you have an account, so check for a publication)** +1. Go to substack.com and, if you haven't yet, start a publication (writing happens inside a publication, not just an account). +2. Pick a name and a URL. The URL becomes `yourname.substack.com`, and every post lives at `yourname.substack.com/p/`. It's hard to change later, so choose it deliberately. +3. Write a one-line description. Skip payments and paid tiers for now. + +**B. The post** +1. Start a new post. Put the title in the title field: + `I rebuilt my F1 model, let routines build out parts of it, and graded it on Singapore` +2. The subtitle field is optional. A suggestion, to edit: `A backtest, a Singapore scorecard, and one thing that broke`. +3. Click into the body and paste. Check that headings, bullets, bold and the code-style words kept their formatting. +4. Replace each orange placeholder with the image (table below), then set its alt text. Delete the placeholder text. +5. Footnotes: Substack has a native footnote feature that shows the note on hover. I couldn't confirm the menu path, so look for "Footnote" in the editor's insert (+) menu or the text-selection toolbar. Replace each superscript [n] with a real footnote using the text in the table below. Fallback if you can't find it: leave the superscripts and add the four notes as a "Notes" list at the end. +6. Preview the post, and send yourself a test email if the option is offered, so you see what a reader sees on desktop and phone. +7. Publish. You have no subscribers yet, so there's no need to email anyone; look for an option to publish to the web only. Scheduling for a set time is available from the publish step if you'd rather post in the morning. (Exact labels unconfirmed.) +8. Copy the live URL. You'll need it for the X thread and the website. + +## Images, in the order they appear + +| # | Upload this file | Alt text | +|---|---|---| +| 1 | `06_routines_infographic.png` | Scheduled routines build, review and summarize work on a side branch, so main stays untouched until I review one pull request. | +| 2 | `01_baselines.png` | Pre-weekend model beats three baselines; adding the qualifying results cuts winner log loss from 1.80 to 1.26 (lower is better). | +| 3 | `02_paired_delta.png` | Conditioning on qualifying improved log loss in 75 of 88 rounds and worsened it in 13, with a few large misses. | +| 4 | `03_by_season.png` | Every season improved after conditioning on qualifying, with 2022 improving the least. | +| 5 | `04_warm_start.png` | All 88 warm-start refits finished under the 120-second target, averaging about 73 seconds; none needed the full-refit fallback. | +| 6 | `05_calibration.png` | Pre-weekend model is too timid at the front, too generous at the back: ~55% podium picks happened ~77% of the time; ~3% picks, ~0.5%. | +| 7 | `08_singapore_podium.png` | The model's top three podium odds, Verstappen, Leclerc and Antonelli, finished 1st to 3rd; Hamilton, at 51%, finished 6th. | +| 8 | `07_singapore_range.png` | 18 of 22 drivers landed inside the forecast range; Hadjar, Lindblad, Colapinto and Bortoleto did not. | + +All eight are in `docs/blog-assets/`. + +## Footnote text + +1. Winner log loss is a score for how much probability the model gave the driver who actually won, where lower is better (for Singapore, ~69% on the winner scored 0.376). +2. These are the "routines" in the title: scheduled cloud agents, meaning AI jobs that start on a timer on someone else's machine and each do one job, like building a slice or reviewing a pull request. +3. Spearman is a score from -1 to 1 for how closely a predicted order matches the real one, where 1 is identical. +4. The Brier score is the average squared gap between a forecast probability and what happened (counted as 1 or 0), so lower is better and 0 would be perfect. + +## Before you hit publish + +- [ ] Title and subtitle read right +- [ ] All 8 images are in, in order, with alt text +- [ ] The infographic is the PNG, not the SVG +- [ ] Footnotes work (or the Notes fallback is in) +- [ ] No orange placeholder or blue instruction box is left in the body +- [ ] Preview looks right on a phone-width window +- [ ] The post still says "@memeerav" at the end (and the subscribe line makes sense now the publication exists) + +## After publishing + +- Your live post URL will look like `https://.substack.com/p/`. +- Your feed is at `https://.substack.com/feed`. That feed is what lets your website pick up new posts automatically (see `portfolio-agent-prompt.md`).