The Upset Score.
One number, zero to one hundred, computed the same way across every sport in our catalog. A 90 is a 90 whether it happened in Foxborough or at Churchill Downs.
The Formula
Every upset is a weighted sum of five factors, each normalized to a 0-1 scale, then multiplied by 100 for readability. The weights are fixed and public. The inputs are cited on every entry page.
UpsetScore =
0.4 × PregameOdds // what the market thought
+ 0.25 × RankGap // quality gap between the two
+ 0.15 × StreakContext // loser was hot, winner was cold
+ 0.1 × Stakes // playoffs > regular season
+ 0.1 × Margin // blowout > squeaker
Result: 0-100, higher = bigger upset.
The Five Factors
Pregame Odds
weight 0.4The loudest signal an upset ever gets. The market — bookmakers, prediction markets, or crowdsourced ELO — has already priced what should happen. When the outcome deviates from the price, that gap is where the upset lives.
We use the losing side's implied win probability from moneyline odds. A team at +450 (18.2% implied win chance) losing is a much bigger upset than a team at -180 (64.3%) losing. Where odds are not available (some historical soccer, some older MLB), we substitute an ELO differential normalized to the same 0-1 scale.
Rank Gap
weight 0.25The underlying quality gap between the two sides. Odds move day-to-day on injuries, weather, momentum. Rankings move slower and describe the season's shape.
We use each league's power ranking or ELO where computed, and the standings-implied win percentage otherwise. The larger the gap in the loser's favor, the higher this factor.
Streak Context
weight 0.15Not every upset happens in a vacuum. When the losing side arrived on a 10-game win streak and the winning side arrived on a 4-game slide, the upset is louder — narratively and analytically. Streaks compress the story.
We reward loser hotness AND winner coldness. Both count. A cold team beating a hot team gets full credit.
Stakes
weight 0.1A June road game is not a Game 7. A group stage is not the final. When the game matters more, an upset in it means more.
Regular season = 0.4. Playoffs = 0.85. Championship / World Series / Super Bowl / World Cup Final = 1.0. Rivalry games and knockout stages sit between.
Margin
weight 0.1A one-run walk-off is a smaller upset than a wire-to-wire beatdown of a favorite. Both count, but the beatdown counts more.
We scale margin by sport: MLB's blowout threshold is six runs, NBA's is 20 points, NFL's is three touchdowns, WNBA's is 15 points, soccer's is three goals.
Source Code
The formula lives at lib/scoring/upset-score.ts in our public repository. Every factor is a small pure function with a single obvious purpose. Anyone can audit the math.
The methodology version is bumped whenever the formula changes. Existing scored upsets keep the version they were scored under; new games get the current version. Every entry page shows which version scored it.
Provenance
Every scored upset carries a provenance field with the exact source, source game ID, and the URL to the original data. We do not scrape blind. We do not manually enter numbers without citing them.
Current sources:
- • MLB StatsAPI for MLB
- • ESPN Scoreboard API for WNBA, NBA, NFL, and Soccer (FIFA WC, UCL, and top-flight European + MLS)
When a source updates a score after the fact (rare), our re-ingestion job picks up the change on the next run.
Known Limitations
Version 1.0 uses standings-implied win percentage as a proxy for pregame odds and ranking gap. That works well in mature seasons and less well in the first two weeks when records are small samples. Refinements:
- • v1.1 will layer real closing moneyline odds where available
- • v1.2 will compute rolling ELO ratings per sport
- • v2.0 will incorporate injury reports and rest day differentials
Every version is announced in the newsletter with backfill notes. A game scored 82 under v1.0 may score 78 under v1.1 if a factor changes; we archive both and show which methodology version each score was computed under.