How the forecast held up
In the winter of my sophomore year I built a forecast for the IL-9 Democratic primary, the first open race for this seat since 1998, and ran it every day until the polls closed on March 17. The rest of this site is preserved as it stood at that moment; this page is what came after, with the final forecast graded against the precinct returns, the two models set side by side, and the photos, press and people from the campaign.
Ryan McComb · Evanston, Illinois
How it did
On the topline the precinct model did about as well as one could reasonably ask: no candidate finished more than 2.3 points from the published forecast, and the order at the top of the field, including the Biss-over-Abughazaleh call that was the only real suspense of the night, came out right. I would not oversell it, since some of that precision is the ordinary luck of errors that partly cancel, and the two smallest candidates finished in the opposite order from the forecast, but a six-way forecast built on polling this thin had no business being this close.
| Candidate | Forecast | Result | Miss |
|---|---|---|---|
| Daniel Biss | 31.6 | 29.3 | +2.3 |
| Kat Abughazaleh | 24.3 | 26.4 | −2.0 |
| Laura Fine | 19.7 | 20.0 | −0.3 |
| Mike Simmons | 9.0 | 7.2 | +1.8 |
| Phil Andrew | 5.0 | 6.1 | −1.1 |
| Bushra Amiwala | 6.0 | 5.1 | +0.9 |
The model saw the district too smoothly
Where it fell down is visible the moment the two maps sit side by side. The forecast placed almost every precinct close to each candidate’s district average, so in it Biss leads four precincts in five and Abughazaleh nearly all of the rest, every one of them in Chicago; the voters drew something far more fractured, with Abughazaleh carrying most of the Chicago precincts and two-thirds of McHenry County, where the model under-forecast her by about sixteen points, Fine winning about a quarter of suburban Cook, and Biss holding two-thirds of suburban Cook and three-quarters of Lake.
What the model drew
What the voters drew
Shade shows the leader’s margin. Hover or tap a precinct.
What I would do differently
I would discount thin market days far more aggressively and let the interval widen as liquidity falls, since some of the February swings were a handful of traders and little else. I would give the precinct model real geographic variance instead of letting it collapse toward the district mean, which is a modelling choice and not a data problem. And I would write it in something other than Stata: the general-election model at the bottom of this page is cleaner and far faster in about a sixth of the code.
The two forecasts
The site ran two forecasts that never fed into each other. One read the race from prediction markets and updated every three minutes; the other built the race up from the ground, precinct by precinct, in Stata. Keeping them separate made the disagreements visible, which turned out to be the most useful thing about running both.
The live pulse
Every three minutes the site pulled contract prices from Kalshi and Manifold, blended them sixty-forty in Kalshi’s favour, and smoothed the result so that a single trader on a thin book could not throw the line around; candidates with no buy-side liquidity fell back to their last traded price rather than a meaningless midpoint.
One point every six hours. Hover or tap the chart for a date.
The markets opened with Biss at roughly two chances in three. The low came on February 10 and 11, when Fine traded near 32% and Biss fell to 52.5%; Abughazaleh peaked near 39% on March 11. By the time the polls closed the markets had Biss at 77% and Fine under 1%.
The ground up
The precinct model gave each of the 436 precincts a baseline for every candidate from a 50/50 blend of age and ideology affinities, taken from the Data for Progress crosstabs, rescaled the district to a recency-weighted polling average, and then simulated the primary 100,000 times with shared polling error, lane correlations and a random turnout draw.
159* Weights from DFP crosstab Q3 (Vote Choice by Age) 161gen age_biss = (pct_18_29*8) + (pct_30_44*19) + (pct_45_64*18) + (pct_65plus*21) 162gen age_abughazaleh = (pct_18_29*30) + (pct_30_44*24) + (pct_45_64*18) + (pct_65plus*12) 163gen age_fine = (pct_18_29*0) + (pct_30_44*4) + (pct_45_64*11) + (pct_65plus*16) 209gen composite_biss = (age_biss + ideo_biss) / 2
- Simulations per run
- 100,000
- Variables per precinct (ACS and 2024 results)
- 79
- Poll half-life
- 60 days
- Weight on an opponent’s / own internal poll
- 0.3× / 0.15×
- Systematic polling error (sd)
- 8 pts
- Turnout draw
- 150k ± 22k
Where they finished
The final run, frozen on March 5, gave Biss a 77.2% chance of winning, Abughazaleh 15.6% and Fine 7.2%. When the polls closed twelve days later the markets had Biss at 77.3%, so two models that shared no inputs ended the race within a tenth of a point of each other on the frontrunner, if not on who was chasing him.
Primary night
In the press
A local reporter tweeted about the site, Capitol Fax picked it up, and for about six weeks a surprising number of people in the district were checking a page I maintained from a lunch table at ETHS.
- Capitol Fax · Feb 9IL9Cast featured as a novel local forecasting tool
- Evanston RoundTable · Feb 10ETHS student aims to forecast 9th District congressional race using betting market data
- The Open Seat · FebETHS sophomore aims to predict congressional race using betting market data
- FOIA GrasIL-9 poll results
Thank you
This whole thing ran out of a lunch table at ETHS, where my friends sat every day while I talked about polling averages and precinct maps instead of anything normal, told me when something on the site looked off, and kept showing up anyway.
Sammy Jain, Teddy Gutstein, Ido Salant, John Spyrson and Nick Jackson
Thank you as well to Elle Zebala, who has been telling me exactly why my political theories are wrong since middle school; to Mr. Patel, my AP Gov teacher, and Julian Cronin; to Matthew Eadie, the local reporter whose tweet started all of this; and to my parents, who never once told me to stop, even on the night Railway deleted the entire data volume at three in the morning and I rebuilt it from CSV backups.
The archive
The snapshot file holds all 71,196 snapshots the scraper wrote, through the afternoon of March 18. The 63,378 taken before the polls closed are the ones the rest of this site shows; the remainder are the morning after.
The race that’s left
The primary was the hard part. The general is close to a formality in a district this blue, so the forecast for it is a deliberately small one I wrote in a single night: every precinct starts from its 2024 result and takes three shocks in each simulation, a national swing, a swing it shares with demographically similar precincts, and a little noise of its own, all applied in log-odds space so that the safest precincts move the least.
Election day: Tuesday, November 3, 2026.
Built by Ryan McComb. Market data from Kalshi and Manifold; precinct returns for the March 17 primary as posted March 22. Inspired by Nate Silver, FiveThirtyEight and Galen Druke.