behind the scenes · a fun ML prediction research project

How I built it — and every way it went wrong first

On 19 July 2026, Argentina and Spain walk out at MetLife Stadium for the World Cup final. My model gives Spain about a 54-in-100 chance of lifting the trophy, Argentina about 46. This page is the workshop behind that number: ten acts, four genuine wrong turns, seven expert agents built to mark my homework, one dead model exposed by Malta, and a betting market I learned to stop throwing away. Every number here traces to a real dataset, a real bug, or a real test — nothing is invented, including the mistakes.

— research by Ajai Raj
⛓ TIME-STAMPED & VERIFIED USING BLOCKCHAIN TECHNOLOGY — this work's existence and integrity before kickoff are cryptographically provable (Bitcoin-anchored SHA-256 · proofs beside this page)
HOW THIS PAGE TEACHES — three layers, and please don't miss the third:
① THE STORY — just read; every term is explained the moment it appears.
② THE FIGURES — touch everything; the pulsing 👇 START HERE chips mark exactly where to click first.
③ THE GOLD ▸ GO DEEPER BOXES — these are closed on purpose and most readers scroll past them. Don't. Every one opens with a tap and contains the full mathematics worked by hand with this project's real numbers — the Poisson formula computed line by line, Elo's update rule with my actual ratings, the exact ledger that turns 38.2 into 53.8. The story works without them; the mastery lives inside them.

00The warm-up: three tools, no magic

the road ahead — twelve forks, none taken yet

Start with the only three ideas this whole project runs on. No machine learning yet. No football knowledge required. If these three click, nothing later can lose you.

Tool one: a single match is nearly a coin-flip. The better team loses football matches all the time — one deflection, one offside toe, one keeper's afternoon. So an honest forecaster never says "Spain will win." The honest output is a probability — and here is what that number really is. Picture a parallel-world machine simulation: the computer plays the same match ten thousand times, and I simply count who wins how many — "Spain 54%" just means Spain won 5,400 of 10,000 simulated finals. That counting picture is the single most important idea on either of these pages. Hold onto it.

The road aheadteaser — unlocks at Act i
123 456 789 101112
Twelve forks. At each one I took a road and pruned a road — the little stubs hanging below the line are the roads that died. By the end of this page you'll know why every one of them died. The full map, with every reason, is the final figure.

Tool two: goals are counts of rare events. A team takes a dozen-ish shots; each shot scores with some small chance; the goals that result are a small count built from many small-chance tries. There is a hundred-year-old formula purpose-built for exactly that shape — the Poisson — and it needs just one number: the team's scoring rate, written λ (lambda). Give the formula λ and it tells you how often that team scores 0, 1, 2, 3 goals. Give it both teams' λs and it prices every possible scoreline of the final — and from the scorelines, the win, the draw, the loss.

The parallel-world machine — what a probability physically isinteractive
👇 START HERE — run 200 simulated finals
Two hundred simulated finals, each fully played out by the machine. Colour each one by who lifts the trophy, then count — that count is the probability. (Rendering illustrative; the split is my real 53.8 / 46.2.) A probability is a count of simulated outcomes, not a verdict — hold that picture for the whole page.

Tool three: training is just "least surprised." When I say a model is trained, I mean something plain: the fit chooses the λs that make the matches that actually happened look least surprising. If Spain keeps winning 2–0 and 3–0, a low Spain λ makes those results look like miracles — so the fitting process raises it. That's all training is. No magic, no black box: a knob turned until reality stops looking surprising.

One knob, every scoreline — the Poisson machineinteractive
👇 START HERE — set λ to each finalist's real rate
or drag freely:
Goals are counts of rare events, so one number — the scoring rate λ — unfolds into the full distribution of 0, 1, 2, 3… goals. Two λs, one per team, price every scoreline of the final. This is the entire engine; everything else on this page is about earning the right values of λ.
GO DEEPER — the Poisson formula, computed by hand with Spain's real rate
The formula is P(k goals) = λᵏ · e⁻λ ÷ k! — and every piece of it says something plain. e⁻λ is the chance of scoring nothing (the higher your rate, the smaller it gets). Each extra goal multiplies by λ (another helping of scoring rate) and divides by k! (1, 2, 6, 24… — because goals don't care about order). Now watch it work with Spain's real final rate, λ = 1.17: chance of 0 goals = e⁻¹·¹⁷ = 31.0% · 1 goal = 1.17 × 0.310 = 36.3% · 2 goals = 1.17² ÷ 2 × 0.310 = 21.2% · 3 goals = 8.3%. Do the same for Argentina (λ = 1.04: 35.3, 36.8, 19.1, 6.6) and — because the two attacks are treated as independent — the chance of any exact scoreline is one number from each list multiplied: P(1–1) = 36.8% × 36.3% ≈ 13.4%, the single most likely score of the final. Sum every cell where Argentina's number is bigger: their win chance. The diagonal: the draw. That's the entire engine — one formula, two rates, every future.

Armed with exactly these three tools, I built my first machine. It looked great. It was lying to me twice.

aThe first build: it looked fine

forks 1, 2 & 4 — the claim, the data, the model family

First decision, before any code: what do I even claim? A confident "I called it" was never on the table — fork one, taken in the first minute. The claim would be a calibrated probability, audited hard enough to deserve printing. That choice quietly dictated everything that follows, because a probability can be tested — and testing is where all my trouble, and all my credibility, came from.

Second decision: data. I went keyless — sources that need no login, no API key, no paywall. Keyless does not mean fake. ESPN's public scoreboard API returns every real 2026 World Cup match — 102 completed games with real goals, shots, possession, and (I'd only later realise how much this mattered) real betting odds. I checked the paid alternatives honestly: API-Football wants a key, football-data.org locks its statistics behind a paywall, Sportmonks' free tier covers Danish and Scottish leagues — fine leagues, wrong continent of relevance. Verified, declined, logged.

Then I built two models side by side. One was the Poisson goals model from the warm-up — two knobs per team, attack and defence. The other was XGBoost, a powerful modern learner that stacks thousands of tiny if-this-then-that rules, with hundreds of effective knobs, fed rolling form, shot conversion, possession differentials. Flexible, fashionable, and on my data, about to embarrass itself.

The flip-flop — overfitting, caught on camerainteractive
👇 START HERE — commit a prediction first
I restore 8 missing matches to a 94-match training set. How much should a sane model's final-prediction move?
94 matches
102 matches (+8)
XGBoost
~hundreds of knobs
Poisson
2 knobs per team
Eight games in, the winner flipped: Argentina 70% became Spain 63%. The Poisson barely stirred. That swing is overfitting — it was memorizing, not learning.
Blue = Argentina · violet = draw · red = Spain. The flexible model's opinion inverted on eight added matches; the two-knob model held steady. On small data, capacity is a liability.

Here is what happened. Trained on the tournament's first 94 matches, XGBoost called the final Argentina 70%. Then a bug-fix restored eight missing matches — games that had gone to extra time, which my ingestion had silently dropped — and on 102 matches the same model said Spain 63%. Eight added games. The winner flipped. That swing has a name: overfitting. The model was never holding an opinion about strength. It was memorizing the last thing it saw.

The signal purse — why fewer knobs win herestatic
Poisson · 2 knobs per team
💰💰
102 matches of signal ÷ a handful of knobs = plenty per knob. The only thing these knobs can learn is attack and defence — strength.
XGBoost · ~hundreds of knobs
🪙🪙🪙🪙🪙🪙🪙🪙🪙🪙🪙🪙
The same 102 matches ÷ hundreds of knobs = crumbs per knob. Crumbs can't tell signal from coincidence — so the knobs memorize whatever arrived last.
Not a law of machine learning — a law of budgets. With millions of matches, the flexible model wins. With a hundred, the humble one does. Matching model capacity to data size is the craft.

The deeper reason is arithmetic, not mystique. A hundred matches is a tiny purse of signal. Spend it across hundreds of knobs and every knob buys mostly noise; spend it on two knobs per team and the only thing those knobs can learn is strength. That's why, at this data size, the humble model with fewer parameters beats the clever one — fork four. The Poisson, for its part, barely blinked when the eight matches arrived: Argentina 37, Draw 25, Spain 37 before, near enough the same after. Stable. Symmetric. Satisfying.

GO DEEPER — how the trees actually think (and what "hundreds of knobs" means)
A decision tree is a stack of yes/no questions: "is the rating gap bigger than 120? is the home side's recent scoring above 1.8 a game?" Each answer routes a match left or right until it lands in a leaf, which holds a small vote — "matches that ended up here were home wins 61% of the time." Boosting is the clever part: you grow trees one at a time, and each new tree is trained mostly on the matches the previous trees got most wrong — a specialist in the current mistakes. Add up hundreds of small corrective votes and you get one sharp prediction. Every split point, every leaf value, is a knob the data must set — that's how "hundreds of knobs" happens, and why 102 matches can't fill them all with truth, only with coincidence. The safety rope is early stopping: hold out the most recent slice of training data, watch the fines on it as trees are added, and stop the moment they stop falling. In my retrial the rope pulled at tree 173 of a possible 600 — the machine itself saying "beyond here I'd be memorizing."

I benched XGBoost and trusted the steady Poisson: 37–25–37. Stable, symmetric, satisfying. Before publishing, I did one paranoid thing — I created seven agents in their respective domains and asked them to mark my homework.

bThe red-team: nobody marks their own homework

fork 9 — the blend dies here

It looked fine. That is precisely the moment to build agents whose only job is to break it, because the one reviewer who is structurally incapable of finding your blind spots is you. So I created seven agents, each an expert in its respective domain — one hunting leakage, one judging the football-domain modelling, one auditing the statistics, one reading the data engineering, one testing market realism, one asking what was missing entirely, one guarding teaching honesty — and asked them to mark my homework, with a lead reviewer to synthesize the verdict. None of them wrote the model. None of them owed me anything.

The war room — seven agents, seven killsinteractive
👇 START HERE — open an agent's seat
pick a seat
Seven independent experts, none of whom wrote the model, each attacking from one angle. Their combined verdict: right direction — rebuild the spine.
Nobody marks their own homework. Every kill above became a fix below — and one seat's best finding was a clean bill: the leak I feared most was verified not firing.

The verdict came back: right direction — rebuild the spine. Five findings carried the force. One: the model was answering the wrong question — a final cannot end in a draw, yet a quarter of its probability sat on "draw" with nowhere to go. Two: the cruellest — the deep history was never actually wired in; the model had trained on 102 matches, roughly seven per finalist, and the beautiful symmetric 37/37 might be silence, not parity. Three: real DraftKings odds sat cached for every match, used for nothing — while a borrowed Opta figure wore the label "the market." Four: no model had ever been scored on matches it hadn't seen. Five: the headline blended the trustworthy Poisson fifty-fifty with the very XGBoost I had just documented as broken — never average in a model you've called unreliable. Fork nine died in that sentence. And ten smaller findings besides.

One piece of good news, worth exactly as much as the bad: the leak I feared most — the final itself contaminating training — was checked and verified not firing. An audit that only ever finds problems is a mood, not a measurement.

The cruellest accusation was the second: your stable 37/37 isn't parity — it's silence. I refused to believe a number that stable could mean nothing. So I tested it.

cThe 37/37 mirage and the Malta test

fork 5 — the leash

I started the rebuild — the full history, thousands of matches, the works (Act d has the details). And here is the scare that taught me the most valuable lesson of the whole project: the number barely moved. Still ~37/37. Two teams, a hundred-plus real matches each now, and the model shrugged: dead even. A number that refuses to move when you change its world is not being stable. It is being absent.

So I ran a five-second test that beats a week of mathematics. Don't ask the model about Argentina and Spain — ask it about a mismatch so absurd that any working model must get it emphatically right. I asked it for Spain versus Malta.

⭐ The Malta test — ask the dead model yourselfinteractive
👇 START HERE — first, commit your own scoreline
The world's #1 team versus the 168th. What does a working model expect? Lock your guess, then ask mine.
> awaiting query…
The five-second test that beat a week of mathematics. Any working model must get a mismatch this absurd emphatically right — which is exactly what makes it the perfect stethoscope.

It answered: Spain 1.35, Malta 1.30. Read that again. The model expected the world's number-one side to edge Malta by five hundredths of a goal. It thought every team on Earth was average. The 37/37 was never parity between two giants — it was a dead model outputting the global mean for everyone, and looking profound while doing it.

The autopsy took one line of code. Regularization is a leash: it stops a model bolting after every noisy match, and every sensible fit wears one. But pull the leash too tight and the model cannot reach the data at all. My library's default tension — alpha = 1.0, a perfectly reasonable default for other problems — was, at my sample size, strangling every team's strength number — its coefficient — down to nearly zero. Over-regularization: the equal-and-opposite failure to Act a's overfitting. XGBoost memorized because its leash was too loose; the Poisson flatlined because its leash was too tight.

The leash dial — every point actually measuredinteractive
👇 START HERE — drag the dial looser, watch Malta
alpha 1.0 (library default — tight)0.10.010.0010.0001 (OOS-tuned — loose)
the stethoscope · Spain vs Malta
1.36 – 1.34
flatlined — every team is “average”
out-of-sample fine (102 unseen matches)
not in the tuning sweep
Regularization is a leash: too tight and the model can't reach the data at all. Loosening it wakes the model up — and the value of the knob isn't chosen by taste but by the smallest fine on unseen matches. Every number on this dial was measured on disk; nothing is interpolated.

Loosen the dial and watch the model wake up. At alpha = 0.001, Spain–Malta separates to 3.9 – 0.3 — a scoreline a football fan would actually recognise — and the final, asked again, moves to Argentina 34.3, Draw 27.5, Spain 38.1. Alive, discriminating, and leaning Spain — the same direction as the market. The deep history had mattered all along. The bug had been hiding it.

Alive at last — and leaning Spain, like the market. But 0.001 was a guess. What earns a knob its value? The same thing that earns any number: matches the model never saw. First, though, I needed something to be measured against.

dThe rebuild: 8,218 matches and a bug wall

fork 3 — the electorate

A dead model means the data never got a vote. So: rebuild the electorate. There is a public, keyless dataset — a community-maintained CSV — holding 49,520 real international results from 1872 to this morning. I filtered it to the current era, 2018 through kickoff, and weighted what remained twice over: recency (an exponential half-life of three years, so last summer outvotes 2019 — and the clock is anchored to the final's date, never to "now", so the build reproduces identically forever) and importance (a World Cup knockout counts fully; a friendly counts 0.4 of a vote). What survived: 8,218 weighted matches. Argentina now speaks with 110 real results, Spain with 111 — not seven.

The electorate — from 49,520 matches to two λsstatic
49,520 real internationals · 1872 → today · public CSV, keyless era filter: 2018 → kickoff (older matches: real, but mostly irrelevant — near-zero weight) 8,218 matches survive × recency (half-life 3y, clock anchored to the final — never “now”) × importance (World Cup 1.0 · continental 0.9 · qualifier 0.8 · friendly 0.4) Argentina: 110 matches → one attack λ, one defence λ Spain: 111 matches → one attack λ, one defence λ every match strictly before kickoff — the hard date wall
The rebuilt electorate. Not seven matches per finalist — a weighted hundred-plus each, with recent competitive games outvoting old friendlies, and nothing at or after kickoff allowed anywhere near the fit.

Rebuilds surface bugs, and I kept mine on the wall deliberately, because every one of them produced plausible output while quietly poisoning the numbers. The extra-time bug: ESPN tags matches that finish after 120 minutes with a different status string, so my filter silently dropped eight real knockout games — including two of Argentina's — until a routine data-proof caught the dataset at 94 matches instead of 102. The label bug: a shootout win was being recorded as a draw, corrupting the form features. The dead feature: my "neutral venue" flag was constant in training and hardcoded at prediction — a correction that corrected nothing, silently. The off-by-one: the prediction row's form window stopped one match early, omitting each finalist's semifinal — the single most relevant game either had played.

The bug wall — kept on display, on purposestatic
① The vanishing knockouts
Bug: extra-time finishes carry a different status string; my filter silently dropped 8 real games — two of Argentina's.
Caught by: counting rows — 94 ≠ 102.
Fix: trust the boolean completed flag, not a string.
② The shootout “draw”
Bug: a penalty-shootout win was recorded as a draw, corrupting win-rate form features.
Caught by: the red-team's label audit.
Fix: goals models score the 90 minutes; advancement is a separate fact.
③ The dead feature
Bug: the neutral-venue flag was constant in training but hardcoded on at prediction — a “correction” correcting nothing, silently.
Caught by: checking feature variance.
Fix: a constant feature is furniture; delete it or make it vary.
④ The missing semifinal
Bug: an off-by-one stopped the form window one match early — omitting each finalist's semifinal, their most relevant game.
Caught by: printing the window's last row.
Fix: assert the window ends where you think it ends.
Four bugs, four fixes, one meta-lesson: data bugs don't crash — they hand you plausible numbers. Count your rows. Test your flags. Distrust your defaults. Print the last row.

The meta-lesson outranks any single fix: bugs in data pipelines do not crash — they hand you plausible numbers. Count your rows. Test your flags. Distrust your defaults. And when a paranoid five-minute check feels beneath you, remember it caught what a week of modelling had missed.

The engine was rebuilt and honest. Now — measured against what? It turned out I'd been holding the answer since day one and throwing it away.

eThe yardstick: what the market actually is

fork 6 — the benchmark

Sitting in my own cache, ignored since day one: DraftKings' closing prices for all 102 matches. Betting odds are probabilities wearing a disguise, and learning to undress them honestly is one genuinely transferable skill of this project. Take the final itself. The closing prices were Spain +125, Draw +200, Argentina +255. A $100 bet on Spain at +125 returns $225 — so that price only breaks even if Spain wins about 44.4% of the time. Convert all three prices the same way and add them up: 105.9%. The maths didn't break. The extra 5.9% is the bookmaker's fee, baked directly into the prices — the house edge, hiding in plain sight. De-vigging simply squeezes that fee back out so the three outcomes describe one honest set of probabilities again: Spain 41.9, Draw 31.5, Argentina 26.6. Across the whole tournament the fee averaged 4.5%. The house always gets paid.

The de-vig squeeze — real closing prices, real feeinteractive
👇 START HERE — predict the sum before converting
DraftKings' closing prices for the final. Convert each to its break-even probability and add all three. What should the sum be?
Spain
+125
→ 44.4%
Draw
+200
→ 33.3%
Argentina
+255
→ 28.2%
sum
?
fee: 5.9%
honest probabilities: Spain 41.9% · Draw 31.5% · Argentina 26.6% = 100.0%
The maths didn't break — the extra 5.9% was the bookmaker's fee, hiding inside the prices. The house always gets paid.
Odds are probabilities wearing a disguise, plus a fee. De-vigging removes the fee so the three outcomes describe one honest set of probabilities — the yardstick every model on this page is measured against.

One category error to kill while I'm here, because I committed it myself. Early on, I "benchmarked against the market" using a published Opta figure. But Opta is not the market — Opta is a rival model, a fellow forecaster with its own opinions. The market is something rarer: a price, formed by people staking real money, that eats information for a living. Three different objects — a price, a peer model, a rating ladder — and only the price is a benchmark. I keep all three on screen from here on, each in its own labelled lane, never blended.

Three different objects — never conflate themstatic
a price · THE BENCHMARK
The market
De-vigged DraftKings closing prices. People staking real money — a price that eats information for a living. Win-in-90: 42 / 31.5 / 27.
a rival model · A PEER
Opta's supercomputer
A fellow forecaster with its own buckets (Spain 45.1 · Arg 29.4 · level-after-90 25.4). Interesting company — not a benchmark, and I once mislabelled it as one.
a rating ladder · A CROSS-CHECK
World Football Elo
Decades-old, updated match by match, fully independent of me. Spain #1 · Argentina #2 — its verdict waits in Act h.
“The experts' number” is three different objects. Only the price is a yardstick; the peer is company; the ladder is a second opinion. Keeping the lanes separate is half of intellectual honesty.

A yardstick needs a scale, and "how often was it right" is the wrong one for a coin-flip sport. The right scale is log-loss — a fine for misplaced confidence. Every match, the forecaster pays a fine based on the probability it gave the outcome that actually happened: say 70% and be wrong, the fine is heavy; say 55% and be wrong, it barely stings; be certain and wrong, the fine is catastrophic. Over 102 matches, the honest forecaster is the one with the smallest total fine.

The fine meter — feel what log-loss doesinteractive · real matches
👇 START HERE — set your confidence, then play match 1
Three real group-stage matches. For each, choose how confident you are that the favourite wins. Then the real result lands — and you pay the fine on whatever probability you gave what actually happened.
Match 1 of 3 — Mexico (favourite) vs South Africa
your total fine: 0.000 (0 matches)
random ⅓-guessing: 1.099/match · the market averaged 0.794
Confident-and-wrong burns; humble-and-wrong stings. Over 102 matches the honest forecaster is the one with the smallest total fine — a scale that can rank forecasters accuracy can't tell apart.
GO DEEPER — the fine's exact formula, and why it's shaped that way
The fine for one match is −ln(p) — the negative logarithm of whatever probability you gave the outcome that actually happened. Three worked fines: say 70% and be right → −ln(0.70) = 0.36, a gentle tap. Say 70% and be wrong — you gave the real outcome only 30% — → −ln(0.30) = 1.20, a proper sting. Say 95% and be wrong → −ln(0.05) = 3.00, catastrophe. The logarithm is what makes the scale honest: it explodes as your probability for the truth approaches zero, so certainty is only ever worth risking when you're actually certain. And here is the beautiful property — the strategy with the lowest expected fine is to report exactly what you truly believe. You cannot game it by hedging or by bluffing. Statisticians call this a proper scoring rule; it's the reason the whole project keeps score in log-loss and not in "how often were you right."

The market's average fine over the tournament: 0.794. For scale, guessing ⅓–⅓–⅓ every match costs 1.099. That 0.794 is the line — the number any model claiming skill must approach. And held against it, my then-current numbers showed exactly the tilt the red-team had predicted: overrating Argentina, underrating the draw.

Yardstick in hand. Time for the exam — one the model cannot cheat.

fThe backtest: a nose behind Vegas, inside the noise

fork 7 — the exam

The exam has two rules. Rule one: the model may study only what happened before the tournament — it trains on the 8,116 internationals up to 11 June, then must predict all 102 World Cup matches blind. Rule two: the wall is a date, not a flag. Leakage — the classic silent killer of published results — is letting the model sit the exam with the answer key in its pocket; a mutable "completed" flag can leak, a hard timestamp cannot. Nothing on or after kickoff enters any fit, the final itself included. I verified it: zero rows past the wall.

This exam also settles Act c's open debt. That alpha dial I loosened by eye? Now it's tuned by measurement: try each tension, score each on the 102 unseen matches, keep the one with the smallest fine. The data chose 0.0001 — looser still than my guess. A knob earns its value on matches the model never saw, or it hasn't earned anything. One asterisk, worn openly: those same 102 matches both sat the exam and chose the knob — which tints the 0.840 slightly optimistic. The leash is a single monotone dial, so the tint is small; the footnote still belongs on the record.

The verdict board — total fines over 102 unseen matcheslower is better
random ⅓⅓⅓ 1.099 base rate 1.050 OUR MODEL 0.840 THE MARKET 0.794 ← the line to beat the gap, mine − market: +0.046 · 95% CI [−0.003, +0.094] 0 = tied with the market whisker crosses zero, mass mostly above → “a nose behind, not distinguishable from level”
Real skill, honestly bounded: my model (0.840) crushes both naive baselines on matches it never saw — trained only on pre-tournament data, while the market watched every game. The gap to the market's 0.794 carries a confidence interval that straddles zero, most of its mass on the market's side — a nose behind, too close for 102 matches to distinguish from level. Hit-rate says 67% vs 69% — and that blunt two-point shrug is why accuracy can't referee a coin-flip sport.

The verdict board, in full. Random guessing: 1.099. Always predicting the tournament's overall averages — the same win/draw/loss frequencies for every match, blind to who is playing: 1.050. My model: 0.840. The market: 0.794. Two readings, both true. First — the model has real skill: it beats both naive baselines by a distance, on games it never saw, despite training only on pre-tournament data while the market watched every match happen. Second — the gap to the market is +0.046. How sure can a gap measured on only 102 matches be? I re-drew and re-scored those matches a thousand times — a resampling trick called the bootstrap — and it put the gap's 95% confidence interval at [−0.003, +0.094]. That interval straddles zero — though most of its mass sits above it. Read it the honest way round: my model is probably a nose behind the market, by a gap that 102 matches cannot distinguish from zero.

So here is the phrasing law I adopted, said proudly rather than sheepishly: my model does not beat Vegas — nobody's should claim to. It sits just behind, closer than the data can tell apart. (Hit-rate tells the same story with a blunter instrument: 67% for me, 69% for the market. If you ever needed proof that accuracy can't referee a coin-flip sport, two points of hit-rate separating forecasters whose fines differ by noise is it.)

GO DEEPER — cross-validation and the bootstrap, the two honesty machines
Cross-validation answers "did my tuning cheat?" Cut the 102 matches into five slices. Hide slice one; choose your settings using only the other four; then score on the hidden slice — which had no say in the choosing. Rotate so every slice gets a turn being hidden, average the five scores, and you have an estimate of how the procedure performs on genuinely unseen data. This is how the weight search was policed: the weights were chosen five separate times, never once seeing the matches that graded them — and all five times they said "market, 100%." The bootstrap answers a different question: "how much does my answer wobble?" Re-deal the dataset — give every match a fresh random weight, as if history had sampled itself slightly differently — refit the entire model, re-read the prediction. Do it a hundred times. The spread of those hundred answers is your uncertainty: for me, Spain's 53.8 wobbled across roughly 42 to 66. Neither machine adds information; both stop you from claiming information you don't have.

One exam passed. One embarrassment left: my beautiful validated number answered a question nobody asked. A final cannot end in a draw.

gThe right question: who lifts the trophy

forks 8, 10 & 11 — the target, the caginess, the coin

Refit through kickoff with the tuned dial, the probabilities read: Argentina 32.1, Draw 29.0, Spain 38.9 — expected goals 1.04 against 1.17. A validated forecast of the ninety minutes. And a forecast with 29% of its worlds ending in a result the World Cup final does not permit. Somebody lifts the trophy in every single tomorrow. My target was wrong, exactly as the red-team's first finding said.

Two refinements fix it. The first is a humility about finals: they are cagey, low-scoring, nobody-wants-the-mistake affairs, so I apply the Dixon–Coles correction — a standard, gentle reshaping that shifts a little probability toward low-scoring outcomes (rho = −0.05, mild). Watch what it does: my draw moves from 29.0 to 30.3 percent. The market's draw price, which I never fit to, sits at 31.5. When a correction you chose for football reasons lands your number beside a price you never touched — unforced agreement — that is validation, not coincidence.

Unforced agreement — the draw, three waysstatic
29.0raw Poisson 30.3+ Dixon–Coles (mine) 31.5the market (never fit to) draw probability, % — a football-reasons correction landed beside a price I never touched
I chose the cagey-finals correction for football reasons, not to chase the market — and it moved my draw to within a point of a price I never fit to. Unforced agreement is validation, not coincidence.

The second refinement resolves the draw worlds. In every tomorrow that ends 90 minutes level, the final continues: thirty minutes of extra time — which I model as the same two teams at one-third rates, the Poisson machinery unchanged — and if still level, a shootout, which I score as a fair coin. Not because shootouts are fair coins, but because every tempting tilt — Argentina's celebrated shootout keeper included — is numerically speculative, and this project does not bake anecdotes into arithmetic. The tilt lives in a footnote, on purpose — and the footnote has numbers: hand Argentina a 55/45 shootout edge and the trophy moves 53.8 → 53.0. Under a point, because only 17% of worlds ever reach the coin. Restraint is part of the method, and here it was also cheap.

The trophy funnel — where the draw-worlds gointeractive
👇 START HERE — guess Spain's trophy chance first
Spain wins the 90 minutes in 38.2% of worlds. Once every draw-world is resolved through extra time and penalties, where does Spain's lifts-the-trophy number land?
the 90-minute probabilities (validated, Dixon–Coles applied)
the 30.3% of level worlds play on: extra time at ⅓ rates → if still level, a fair coin
who lifts the trophy — every world resolved
Spain 53.8% · Argentina 46.2% — with a 90% band of roughly 42–66 for Spain. The famous shootout-keeper tilt stays a footnote, not a coefficient: restraint is part of the method.
A final cannot end in a draw, so the violet mass must flow somewhere. Spain's slight edge in extra time (more expected goals) plus an even coin turns 38.2-in-90 into 53.8-lifts-the-trophy.

Now let the draw mass flow downhill. 30.3% of worlds go to extra time; extra time resolves some and passes the rest to the coin; the coin splits what remains. Summed across all worlds: Spain 53.8%, Argentina 46.2% — and, re-running the entire fit a hundred times on gently reshuffled data (the bootstrap again), a 90% band of roughly 42–66 for Spain. The number fans actually ask for, carrying its own honesty about how much one hundred-ish matches per team can pin down.

GO DEEPER — the exact arithmetic ledger: how 38.2 becomes 53.8
Follow every point of probability to its destination. The ninety minutes end: Spain 38.2 · Draw 30.3 · Argentina 31.5. The 30.3 draw-points enter extra time, where both teams play at one-third rates (λ 0.35 and 0.39) — small rates, so the most likely ET result is no goals at all: 54.8% of these worlds stay level. Spain's higher rate wins 24.0% of ET worlds outright, Argentina's 21.2%. Convert to points: of the 30.3 → Spain takes 30.3 × 0.240 = 7.3, Argentina 30.3 × 0.212 = 6.4, and 30.3 × 0.548 = 16.6 points march on to penalties, where the fair coin grants 8.3 to each. Final ledger — Spain: 38.2 + 7.3 + 8.3 = 53.8. Argentina: 31.5 + 6.4 + 8.3 = 46.2. Nothing hidden, nothing invented: the trophy number is the ninety-minute probabilities with every draw-path walked, step by step, to whichever door it exits through.

One model, one earned number. But one model agreeing with itself proves little. I went looking for strangers.

hConvergence: three strangers agree

fork 12 — the cross-check

The last fork asked: more data? Any new source had to be keyless and genuinely independent — another opinion, not my own data echoed back. The World Football Elo ratings qualify: a rating ladder maintained for decades — after every match the winner takes rating points from the loser, more for an upset — built by people who have never heard of my model. On the eve of the final it ranked Spain #1 at 2232 and Argentina #2 at 2200 — the top two teams on Earth, thirty-two points apart. Feed that gap through Elo's own win-expectancy formula and it says Spain ~54.6, Argentina ~45.4.

GO DEEPER — Elo's update rule, worked with the real ratings
Elo is beautiful because the whole system is one line: after every match, the winner takes points from the loser, and how many depends on how surprising the result was. The expected score is E = 1 ÷ (1 + 10^(−gap/400)) — a smooth curve where a 0-point gap gives 50%, +200 gives 76%, +400 gives 91%. The update is new rating = old + K × (result − E). Worked, with my hand-built ratings: Spain 2115 vs Argentina 2110 — gap +5, so E = 50.7%, a true coin-flip by the ladder's eyes. If Spain win tonight (result = 1, K = 32 for a World Cup): they gain 32 × (1 − 0.507) = +15.8 points, Argentina lose the same. Win by two goals and a margin multiplier (×1.5) makes it +23.7. Had this been a friendly (K scaled to 12.8), the same win would move only +6.3 — the ladder listens harder when the stakes are real. Replay that one line over 26 years of matches and the ratings settle into a living ranking. The last step is mine: a small regression (a fitted S-curve) that converts any rating gap into win/draw/loss probabilities — trained, like everything here, only on matches before the tournament.

Line up the three instruments. My goals model, built from scratch out of public data and audited within an inch of its life: Spain 53.8. The Elo ladder, decades old, method entirely different: ~54.6. The betting market, pricing the ninety minutes with real money: Spain 41.9 against 26.6 on its own win-in-90 scale — the same lean, priced in its own currency. Three strangers, three methods, one direction. Convergence of independent instruments is stronger validation than any single model's confidence — and it is as close to "right" as an honest forecaster gets to feel the night before the world finds out.

Three strangers, one directionstatic · each on its own honest scale
OUR MODEL lifts-the-trophy Spain 53.8 band 42–66 ELO LADDER win expectancy · 2232 v 2200 Spain ~54.6 50 = coin-flip THE MARKET win-in-90 · its own scale Spain 41.9 Draw 31.5 Argentina 26.6
A goals model built from scratch, a decades-old rating ladder, and a market moving real money — three genuinely independent instruments, each on its own correctly-labelled scale, all leaning Spain by a little. No market trophy-number is shown because none exists: inventing one would be fabrication.

One last experiment closed the loop. I tried chaining the three instruments — a weighted blend, every one of 903 weight combinations, two mixing rules, cross-validated so the weights couldn't cheat. The search's verdict: put 100% on the market, every fold, nothing on me. That is not a defeat — it is the cleanest lesson in the project. A blend only beats its parts when the parts make different mistakes; the models are built from the same results the market already digested, so there was no independent error left to harvest. What building them bought instead was trust — you only know the market is worth 100% after you've matched it from scratch — and reach: the market prices ninety minutes, and the extra-time-and-penalties machinery is what turns its price into a trophy number (Spain 58.2 / Argentina 41.8, beside the independent 53.8 / 46.2 — the gap between them is honest information, not embarrassment). And yes — I even staged XGBoost's retrial. Re-fed with 19,617 matches of honest features, the Act-a exhibit came back sane: no flip-flop, a reasonable near-even call on the final. It also finished last (0.881), and the four-way weight search handed it zero. The signal here is smooth — strength plus a little form — and trees buy variance, not insight, where there are no sharp corners to find. Right data made it sane. Right structure is what the Poisson had all along. (One detail for the record: the Elo in that search was not the official ladder from earlier — I built my own, replaying 26 years of matches with the winner taking rating points from the loser, more for upsets and big margins, then a small regression turning each rating gap into win/draw/loss probabilities. It reached the same verdict as the official one: Spain 2115, Argentina 2110 — a four-point whisper.)

The full roster — every model, one examlog-loss on 102 unseen matches · lower is better
predictorthe idea, in one linefinefate
The marketreal DraftKings prices, bookmaker's fee squeezed out0.794won the weight search at 100%
Poisson (mine)two knobs per team — attack & defence — over 8,218 weighted matches0.840the independent headline; best of mine
Elo-logit (mine)my hand-built rating ladder + a small gap→probability regression0.858third; agreed Spain by a whisper
XGBoost, retriedthousands of if-then rules, finally fed 19,617 honest matches0.881sane at last — still last
base ratesthe same average frequencies for every match, blind to teams1.050cleared by all models
random ⅓ guessno information at all1.099the floor
XGBoost v1the same trees fed only 102 matches— unmeasurableflipped its winner; benched as the exhibit
The whole project in one table. Around these sat the machinery — Dixon–Coles caginess, the extra-time-and-penalties resolution, the bootstrap band, the weight search — and the referees: a seven-agent red-team, and finally six more agents — three ML experts, three football experts — convened as an external jury to mark the finished work: they voted 6–0 to ship with small fixes (all applied) and split my two headline numbers down the middle: panel median Spain 56 / Argentina 44.

Now walk the whole road at once.

iThe map of every fork

all twelve — the road complete

Strip away the drama and this is what modelling actually is: a sequence of forks, each with a road taken, a road refused, and a reason you should be able to say in one line. Here is the whole journey as one map — every green branch taken, every red stub pruned, and the receipt for each. If you take one artifact away from these two pages, take this: not the answer, but the shape of how an answer gets earned.

⭐ The decision tree — twelve forks, every reasoninteractive
👇 START HERE — click any numbered fork on the line
✅ taken  
✖ pruned 
The whole project as one map. Green line: the road taken. Red stubs: the roads pruned — click a fork (or use the buttons) to read what died there and why, and which act lived it. This is what modelling is: forks, with reasons you can say in one line.

And the honest closing panel, because a map that hides its edges is a brochure. This model sees nations, not line-ups: it cannot know who is rested, who is carrying a knock, who starts on the bench — projected elevens live on the showcase page, clearly labelled as projected. My historical knockout scores fold extra-time goals into the total, faintly inflating knockout scoring rates. The shootout is a coin by choice, not by evidence of fairness. And no audit changes the deepest fact in the room: a single football match is irreducibly noisy. A calibrated 54% forecaster is supposed to be wrong 46% of the time — which means tonight proves nothing either way, and was never going to. What I owe you is not the winner. It is a number that meant what it said.

Spain, about 54. Argentina, about 46. Whatever happens at MetLife — that was the honest count.

◈ a note from the author

I'm an electrical engineer who walked into ML research through the game I love, and this page is the honest record of that walk — every dead model, every leash pulled too tight, every audit that stung. I'm trying to understand everything in this space, and the fastest way anyone learns is being corrected in public. So if a derivation is loose, a number doesn't reconcile, or a whole act deserves to be deeper — tell me directly, or use the response box at the end of the showcase page. Support it, break it, improve it. That's the engineering way.

Built end-to-end as a research/teaching project: real keyless data (ESPN public API · martj42 international results · World Football Elo · DraftKings closing prices via ESPN), a weighted Poisson goals model tuned and validated out-of-sample, and every wrong turn kept in the record on purpose. The companion page shows the final itself — the forecast, the tournament, and the ten-thousand-world census: The Final, Forecast → · 🔒 provenance: prediction committed publicly at 20:34 UTC, 18 Jul 2026 — 22h26m before kickoff (verify commit a87a8a0 · SHA-256 anchored in Bitcoin via OpenTimestamps — .ots proofs beside this page; a beginner-friendly step-by-step verify guide lives on the showcase page) · research by Ajai Raj · iolinked labs · 2026