<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Elio Liberatore</title>
    <description>The latest articles on DEV Community by Elio Liberatore (@commodus67).</description>
    <link>https://dev.to/commodus67</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119915%2Fa4787c9b-3be1-48a1-b556-b475f5158135.png</url>
      <title>DEV Community: Elio Liberatore</title>
      <link>https://dev.to/commodus67</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/commodus67"/>
    <language>en</language>
    <item>
      <title>NBA play-in odds are not playoff odds: simulating the play-in tournament instead of guessing it</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:26:19 +0000</pubDate>
      <link>https://dev.to/commodus67/nba-play-in-odds-are-not-playoff-odds-simulating-the-play-in-tournament-instead-of-guessing-it-1kae</link>
      <guid>https://dev.to/commodus67/nba-play-in-odds-are-not-playoff-odds-simulating-the-play-in-tournament-instead-of-guessing-it-1kae</guid>
      <description>&lt;p&gt;In baseball, American football and hockey, "make the playoffs" is one line: finish above it and you're in. When I built an NBA version of my playoff-odds simulator, I assumed the same, and my first plan was to compare the model against the wrong market.&lt;/p&gt;

&lt;p&gt;Basketball has a band in the middle, and getting it right changes almost every number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three zones, not two
&lt;/h2&gt;

&lt;p&gt;In each 15-team conference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finish&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st – 6th&lt;/td&gt;
&lt;td&gt;Straight into the playoffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7th – 10th&lt;/td&gt;
&lt;td&gt;Into the &lt;strong&gt;play-in tournament&lt;/strong&gt;, where two of four survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11th – 15th&lt;/td&gt;
&lt;td&gt;Season over&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The play-in works like this. Seed 7 plays seed 8; the winner is the 7th seed. Seed 9 plays seed 10; the loser goes home. The loser of 7 v 8 then hosts the winner of 9 v 10, and that game decides the 8th seed.&lt;/p&gt;

&lt;p&gt;Kalshi lists these as separate markets — one for playoff qualification across all 30 teams, and one play-in market per conference — and its contract rules settle the question in one sentence: "Qualifying for the play-in tournament doesn't constitute playoff qualification."&lt;/p&gt;

&lt;p&gt;So the play-in market is not a weaker version of the playoff market. It's a band, and the two behave almost like opposites. A title contender is a near-certainty for the playoffs and a near-zero for the play-in. A 44-win team can be close to a coin flip between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulate the games, don't approximate them
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to say "finish 7th or 8th, you're probably in; 9th or 10th, probably not". That breaks the moment you need two numbers that agree with each other: the chance of reaching the playoffs &lt;em&gt;and&lt;/em&gt; the chance of landing in the play-in.&lt;/p&gt;

&lt;p&gt;So in every simulated season the model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plays out the remaining regular-season schedule.&lt;/li&gt;
&lt;li&gt;Seeds each conference.&lt;/li&gt;
&lt;li&gt;Plays the three play-in games.&lt;/li&gt;
&lt;li&gt;Runs the full bracket: four rounds of best-of-seven series with the 2-2-1-1-1 home-court pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That produces, for every team and from the same simulated seasons: top-six probability, play-in probability, playoff probability, conference finals, conference title and championship. Sanity checks come for free — the championship column sums to 1 across the league and conference finals to 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basketball-specific modelling choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pythagorean exponent.&lt;/strong&gt; Points scored and allowed predict future results better than record alone, but the exponent depends on the sport. Baseball uses about 1.83, hockey 2.0; for basketball I use 13.91. Plugging a baseball exponent into NBA scoring wildly compresses the gap between good and bad teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uncertainty about strength itself.&lt;/strong&gt; My first version drew game outcomes from fixed team ratings. In September it confidently told me some teams made the playoffs 100% of the time and others 0%. That's not how the NBA works. Now each simulated season redraws every team's rating once (a 0.22 spread by default), so the output is a distribution rather than one confident guess. That alone cut the average gap against the market from 15.2 to 12.9 points and removed every 0% and 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two missing games.&lt;/strong&gt; The 2026-27 regular season is 82 games, but ESPN's schedule currently lists 80 per team: the other two depend on the NBA Cup and are scheduled later. If you simply simulate the schedule you can see, every team's projected wins sit on an 80-game scale. I simulate the missing two against an average opponent on a neutral floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A September snapshot
&lt;/h2&gt;

&lt;p&gt;From one run on 10 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Projected wins&lt;/th&gt;
&lt;th&gt;Top six&lt;/th&gt;
&lt;th&gt;Play-in&lt;/th&gt;
&lt;th&gt;Playoffs&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oklahoma City&lt;/td&gt;
&lt;td&gt;55.3&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;99.1%&lt;/td&gt;
&lt;td&gt;23.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Antonio&lt;/td&gt;
&lt;td&gt;52.1&lt;/td&gt;
&lt;td&gt;91.6%&lt;/td&gt;
&lt;td&gt;7.8%&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;td&gt;11.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boston&lt;/td&gt;
&lt;td&gt;51.6&lt;/td&gt;
&lt;td&gt;88.2%&lt;/td&gt;
&lt;td&gt;11.0%&lt;/td&gt;
&lt;td&gt;95.9%&lt;/td&gt;
&lt;td&gt;11.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toronto&lt;/td&gt;
&lt;td&gt;44.8&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;41.0%&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;2.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlanta&lt;/td&gt;
&lt;td&gt;44.4&lt;/td&gt;
&lt;td&gt;48.2%&lt;/td&gt;
&lt;td&gt;42.4%&lt;/td&gt;
&lt;td&gt;71.7%&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miami&lt;/td&gt;
&lt;td&gt;44.3&lt;/td&gt;
&lt;td&gt;47.9%&lt;/td&gt;
&lt;td&gt;42.6%&lt;/td&gt;
&lt;td&gt;71.3%&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at Toronto, Atlanta and Miami: roughly a coin flip to avoid the play-in, and more than 40% to end up in it. For those teams, playoff odds and play-in odds are both "live" — exactly why they need separate columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I don't trust
&lt;/h2&gt;

&lt;p&gt;Against Kalshi's playoff contracts that day, most of the table lines up within a few points: Oklahoma City 99.1% in the model against 98% on the market, Miami 71.3% against 72.5%, Toronto 73.7% against 70.5%.&lt;/p&gt;

&lt;p&gt;Then there's Charlotte: 86.5% in the model, 36% on the market. A fifty-point gap.&lt;/p&gt;

&lt;p&gt;That is not an opportunity. Before opening night, the model knows only how last season ended, regressed towards average. It hasn't seen free agency, the draft, trades or injuries. The market has. When I first measured the model against Kalshi in September, the rank correlation was about 0.80 and the average gap about 13 points — and the largest gaps were teams whose whole case this year is the offseason.&lt;/p&gt;

&lt;p&gt;So the simulator refuses to call anything "value" until every team has played ten games. It still reports every gap, labelled WATCH. A model that disagrees with the market by fifty points in September is telling you what it can't see, not what the market got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  One market-side detail worth copying
&lt;/h2&gt;

&lt;p&gt;Playoff qualification is sixteen independent yes/no contracts, so prices across the league add up to about 16, not 1. Never normalise that field to 1. And price both sides of each contract: if the model says 60% and YES trades at 75 cents, the signal is on the NO side, not "no signal".&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The simulator is published as an Apify Actor: &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo" rel="noopener noreferrer"&gt;NBA Playoff Odds API — Monte Carlo Simulator &amp;amp; Value Bets&lt;/a&gt;. Standings and schedules come from ESPN and prices from Kalshi's public API, with no keys needed. Ready-made examples include &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-play-in-tournament-odds-by-conference" rel="noopener noreferrer"&gt;play-in tournament odds by conference&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-championship-odds-simulated-playoff-bracket" rel="noopener noreferrer"&gt;championship odds from a simulated bracket&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-projected-win-totals-for-all-30-teams" rel="noopener noreferrer"&gt;projected win totals&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/track-nba-playoff-odds-all-season" rel="noopener noreferrer"&gt;tracking playoff odds all season&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The season starts on 20 October. Simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>simulation</category>
    </item>
    <item>
      <title>NHL playoff odds for 2026-27: 84 games, the wild card rule, and why I don't trust my own model in September</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:26:00 +0000</pubDate>
      <link>https://dev.to/commodus67/nhl-playoff-odds-for-2026-27-84-games-the-wild-card-rule-and-why-i-dont-trust-my-own-model-in-10hm</link>
      <guid>https://dev.to/commodus67/nhl-playoff-odds-for-2026-27-84-games-the-wild-card-rule-and-why-i-dont-trust-my-own-model-in-10hm</guid>
      <description>&lt;p&gt;I build Monte Carlo simulators that turn standings and schedules into playoff probabilities. After baseball, American football and soccer, I assumed hockey would be a copy-paste job with new team names.&lt;/p&gt;

&lt;p&gt;It wasn't. Four things about the NHL break a generic season simulator, and one of them changed this summer. Here they are, followed by the part I find most interesting: what the model says a few weeks before opening night, and why most of its disagreements with the market should be ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Standings run on points, not wins
&lt;/h2&gt;

&lt;p&gt;A win is two points. An overtime or shootout loss is still worth one. Roughly 23% of NHL games go past regulation, so ranking simulated teams by wins misprices every club that lives in one-goal games.&lt;/p&gt;

&lt;p&gt;In the simulator each game has three outcomes — regulation win, overtime or shootout win, and the mirror images — and points are awarded the way the league awards them.&lt;/p&gt;

&lt;p&gt;There is a trap on the strength side too. Points percentage averages about .557 across the league because of the loser point, while win/loss averages exactly .500 because every game has a winner. If you estimate team strength from points percentage, every team looks slightly better than average. I estimate it from wins over games played, and from goals for and against with a Pythagorean exponent of 2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The playoff field is not "the top eight"
&lt;/h2&gt;

&lt;p&gt;Sixteen of 32 teams qualify, but not by conference rank. In each conference the top three of each division get in, and then the two best remaining teams take the wild cards regardless of division. A fourth-place team in a strong division and a third-place team in a weak one are not interchangeable.&lt;/p&gt;

&lt;p&gt;That rule has to run inside every simulated season. It also gives you a free sanity check: across all 32 teams, playoff probabilities must sum to exactly 16. If yours sum to 15.7, something is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Overtime is closer to a coin flip
&lt;/h2&gt;

&lt;p&gt;Three-on-three and the shootout are not sixty minutes of five-on-five hockey. A better team carries less of its edge into the extra period. I damp the strength gap by half in overtime (&lt;code&gt;overtimeDamping = 0.5&lt;/code&gt;). Setting it to 0 makes overtime a pure coin flip; 1 treats it like regulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The season is 84 games now
&lt;/h2&gt;

&lt;p&gt;The collective bargaining agreement signed in 2025 moved the NHL to 84 regular-season games from 2026-27. ESPN's event count per team shows 84 for this season and 82 for last. I never hardcoded the season length — the simulator counts the games on the schedule feed — so projected point totals landed on the right scale without a code change. If you maintain a model with &lt;code&gt;82&lt;/code&gt; somewhere in it, now is the time to look.&lt;/p&gt;

&lt;p&gt;One more data quirk: ESPN names a season by the year it ends, so 2026-27 is &lt;code&gt;season=2027&lt;/code&gt;, and before opening night the 2027 standings tree exists but has zero teams in it. You need to fall back to the previous season for the list of clubs and divisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model says right now
&lt;/h2&gt;

&lt;p&gt;Here is part of a 20,000-season run from 10 September 2026, next to the live price of Kalshi's "make the playoffs" contract for each team:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Projected points&lt;/th&gt;
&lt;th&gt;Model playoff %&lt;/th&gt;
&lt;th&gt;Kalshi&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Colorado&lt;/td&gt;
&lt;td&gt;109.6&lt;/td&gt;
&lt;td&gt;96.5%&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;+5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carolina&lt;/td&gt;
&lt;td&gt;103.5&lt;/td&gt;
&lt;td&gt;82.9%&lt;/td&gt;
&lt;td&gt;89.5%&lt;/td&gt;
&lt;td&gt;−6.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffalo&lt;/td&gt;
&lt;td&gt;101.6&lt;/td&gt;
&lt;td&gt;76.3%&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;+20.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edmonton&lt;/td&gt;
&lt;td&gt;95.7&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;−16.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vegas&lt;/td&gt;
&lt;td&gt;95.2&lt;/td&gt;
&lt;td&gt;66.4%&lt;/td&gt;
&lt;td&gt;85.5%&lt;/td&gt;
&lt;td&gt;−19.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boston&lt;/td&gt;
&lt;td&gt;97.0&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;+27.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pittsburgh&lt;/td&gt;
&lt;td&gt;95.5&lt;/td&gt;
&lt;td&gt;52.5%&lt;/td&gt;
&lt;td&gt;29.5%&lt;/td&gt;
&lt;td&gt;+23.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;89.7&lt;/td&gt;
&lt;td&gt;42.3%&lt;/td&gt;
&lt;td&gt;67.5%&lt;/td&gt;
&lt;td&gt;−25.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Boston 27 points above the market. San Jose 25 below. If you believed the model, those would be the trades of the year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I don't believe it (yet)
&lt;/h2&gt;

&lt;p&gt;In September the model knows exactly one thing about each club: how last season ended, shrunk 40% towards average. It hasn't seen a trade, a signing, an injury, a goalie change or a new coach. The market has seen all of them.&lt;/p&gt;

&lt;p&gt;So the biggest gaps in September are not edges. &lt;strong&gt;They are the offseason.&lt;/strong&gt; Betting them is betting that the summer didn't happen.&lt;/p&gt;

&lt;p&gt;When I first compared the model with Kalshi across all 32 teams, the rank correlation was 0.65 and the average gap was about 14 points, and the largest disagreements were precisely the teams whose summers changed the most. That is what a season-carryover model should look like before the puck drops.&lt;/p&gt;

&lt;p&gt;Instead of hiding that, I made it a rule in the output. Until every team has played a minimum number of games (10 by default), no row is allowed to call itself value. Every gap is still reported in full, but it's labelled WATCH. After that, the current season's record takes over from last season's gradually, rather than overnight after a hot week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fee changes which gaps matter
&lt;/h2&gt;

&lt;p&gt;Kalshi's taker fee is 0.07 × p × (1 − p) per contract. It peaks in the middle: 1.75 cents on a 50-cent contract, 0.63 cents on a 90-cent one. So a 1.5-point edge on a coin-flip contract is a losing position after fees, while the same gap on a heavy favourite isn't. Any comparison that ignores this will rank the wrong teams first.&lt;/p&gt;

&lt;p&gt;A second trap: the playoff market is sixteen independent yes/no contracts, so prices across the league add up to about 16, not 1. If you "de-vig" it the way you would a division-winner market, you divide every probability by sixteen and manufacture huge fake edges everywhere. Division winners, on the other hand, are exclusive — exactly one team wins — and there you do strip the overround.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it move
&lt;/h2&gt;

&lt;p&gt;The part I'm looking forward to is not the September table. It is the path. Each run can append its 32 rows to a named dataset that keeps growing, so a daily schedule gives you, by spring, how each team's probability moved across the season next to what the market charged for it on the same day — something you can't reconstruct afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The simulator is published as an Apify Actor: &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo" rel="noopener noreferrer"&gt;NHL Playoff Odds API — Monte Carlo Simulator &amp;amp; Value Bets&lt;/a&gt;. No API keys for the data; standings and schedules come from ESPN, prices from Kalshi's public API. There are ready-made examples for &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-projected-points-standings-all-32-teams" rel="noopener noreferrer"&gt;projected points standings&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-wild-card-odds-all-32-teams" rel="noopener noreferrer"&gt;wild card odds&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-presidents-trophy-odds" rel="noopener noreferrer"&gt;Presidents' Trophy odds&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/track-nhl-playoff-odds-all-season" rel="noopener noreferrer"&gt;tracking playoff odds all season&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>simulation</category>
    </item>
    <item>
      <title>Correct score, BTTS and Over/Under probabilities with Dixon-Coles: what I learned building it for MLS and Liga MX</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:25:35 +0000</pubDate>
      <link>https://dev.to/commodus67/correct-score-btts-and-overunder-probabilities-with-dixon-coles-what-i-learned-building-it-for-39dc</link>
      <guid>https://dev.to/commodus67/correct-score-btts-and-overunder-probabilities-with-dixon-coles-what-i-learned-building-it-for-39dc</guid>
      <description>&lt;p&gt;Most football prediction pages give you three numbers — home, draw, away — and no way to check where they came from. I wanted the opposite: one model, fitted on real results, that produces the 1X2 probabilities, the Over/Under 2.5 line, Both Teams To Score and the exact-score grid, all from the same place, so they can't contradict each other.&lt;/p&gt;

&lt;p&gt;The model I ended up with is Dixon-Coles. This post is what it does, why it beats the simpler version most tutorials start with, and what came out when I ran it on eight leagues that don't get much attention from modellers: MLS, Liga MX, Liga de Expansión MX, the Brasileirão Série B, the USL Championship, Colombia's Primera A, Uruguay's Primera División and Norway's Eliteserien.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: independent Poisson
&lt;/h2&gt;

&lt;p&gt;The classic approach gives every team an attack rating and a defence rating, adds a home advantage, and turns them into expected goals for each side of a fixture — call them λ for the home team and μ for the away team. Goals are then treated as two independent Poisson variables. The probability of a 2-1 is just P(home scores 2) × P(away scores 1).&lt;/p&gt;

&lt;p&gt;It works surprisingly well. It also has one known blind spot: &lt;strong&gt;low scores&lt;/strong&gt;. Real matches finish 0-0 and 1-1 more often than two independent Poisson draws predict, and 1-0 / 0-1 slightly less often. Anything that depends on those four cells — the draw, Under 2.5, BTTS "No" — inherits the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dixon-Coles correction
&lt;/h2&gt;

&lt;p&gt;In 1997 Mark Dixon and Stuart Coles proposed a small fix. Keep the Poisson grid, but multiply the four low-score cells by a factor that depends on one extra parameter, ρ (rho):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Adjustment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-0&lt;/td&gt;
&lt;td&gt;1 − λμρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;td&gt;1 + λρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-0&lt;/td&gt;
&lt;td&gt;1 + μρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-1&lt;/td&gt;
&lt;td&gt;1 − ρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a negative ρ, 0-0 and 1-1 go up and 1-0 / 0-1 go down — exactly the direction the data pulls.&lt;/p&gt;

&lt;p&gt;To see how much that matters, take a match with 1.35 expected goals for the home side and 1.15 for the away side, and an illustrative ρ of −0.13:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Independent Poisson&lt;/th&gt;
&lt;th&gt;Dixon-Coles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Draw&lt;/td&gt;
&lt;td&gt;26.8%&lt;/td&gt;
&lt;td&gt;30.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-0&lt;/td&gt;
&lt;td&gt;8.2%&lt;/td&gt;
&lt;td&gt;9.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-1&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home win&lt;/td&gt;
&lt;td&gt;41.3%&lt;/td&gt;
&lt;td&gt;39.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Away win&lt;/td&gt;
&lt;td&gt;31.8%&lt;/td&gt;
&lt;td&gt;30.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same expected goals, three and a half points more on the draw. If you compare model probabilities with prices, that is the difference between seeing an edge and not seeing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things the paper adds that tutorials often skip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time decay.&lt;/strong&gt; A result from three seasons ago should not count as much as last weekend's. Dixon and Coles weight each match by exp(−ξ·t), where t is its age in days. I use ξ = 0.0018, which halves a result's weight after roughly a year, and fit on three seasons of history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fit ρ from the league itself.&lt;/strong&gt; ρ is not a universal constant. After fitting attack, defence, home advantage and the baseline by weighted maximum likelihood, I fit ρ separately for each league. They come out different. On 6 September, Colombia's Primera A gave a home advantage of 0.349 (on the log scale) and ρ = −0.044; Norway's Eliteserien gave 0.276 and ρ = −0.015. Colombian home sides get a bigger boost, and the low-score correction matters less in Norway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the data comes from
&lt;/h2&gt;

&lt;p&gt;All eight leagues come from ESPN's public scoreboard endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://site.api.espn.com/apis/site/v2/sports/soccer/&amp;lt;league&amp;gt;/scoreboard?dates=YYYYMMDD-YYYYMMDD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things cost me time and might save you some:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a date range, not a single day.&lt;/strong&gt; With &lt;code&gt;dates=&lt;/code&gt; set to one day, a smaller league often returns nothing simply because it didn't play that day, which looks exactly like missing data. A week-long range removes the ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter by state, not by status name.&lt;/strong&gt; Finished matches carry &lt;code&gt;status.type.state === "post"&lt;/code&gt; and &lt;code&gt;completed: true&lt;/code&gt;; scheduled ones are &lt;code&gt;"pre"&lt;/code&gt;. That is more robust than matching &lt;code&gt;STATUS_FULL_TIME&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Slugs are not always what you'd guess. The USL Championship is &lt;code&gt;usa.usl.1&lt;/code&gt;, not &lt;code&gt;usa.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;History depth is not the bottleneck. The Premier League scoreboard answers back to at least 2002-03, and MLS and Liga MX back to at least 2004-05. Three seasons is plenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one prediction looks like
&lt;/h2&gt;

&lt;p&gt;Here is a real row from a test run on 6 September — Atlanta United at home to Orlando City in MLS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;λ home / λ away&lt;/td&gt;
&lt;td&gt;1.57 / 1.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ρ (MLS)&lt;/td&gt;
&lt;td&gt;−0.040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home / Draw / Away&lt;/td&gt;
&lt;td&gt;36.5% / 24.2% / 39.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over 2.5 goals&lt;/td&gt;
&lt;td&gt;62.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both teams to score&lt;/td&gt;
&lt;td&gt;64.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And from Liga MX the same day, Pumas UNAM against León: λ 1.94 against 0.94, so 60.1% / 23.0% / 16.9%. A strong home side, and the numbers say so.&lt;/p&gt;

&lt;p&gt;Every probability comes from one score grid (0-0 up to 10-10), normalised to 1, so 1X2 sums to 1, Over + Under sums to 1 and BTTS Yes + No sums to 1. I checked that on every row of a 28-match Série B run; the error was floating-point noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The awkward case: promoted teams
&lt;/h2&gt;

&lt;p&gt;A team with no matches in the lookback window has no rating. The honest options are to guess or to say so. I start it at league-average strength and flag every fixture it plays with &lt;code&gt;dataQuality: "partial-new-team"&lt;/code&gt;, so whoever uses the numbers can decide how much to trust them until the team has a few games on the board.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't know about injuries, suspensions, rotation, weather or a manager who has just been sacked. It treats every match in the lookback window the same apart from its age. It is a baseline, not an oracle — which is exactly what makes it useful to compare against prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without writing the model
&lt;/h2&gt;

&lt;p&gt;I packaged all of this as an Apify Actor: &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor" rel="noopener noreferrer"&gt;Soccer Match Predictions API — 1X2, Over/Under &amp;amp; BTTS Odds&lt;/a&gt;. You pick a league, it returns one row per upcoming fixture with everything above. There are ready-made examples, such as &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples/mls-correct-score-probabilities" rel="noopener noreferrer"&gt;MLS correct score probabilities&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples/liga-mx-match-predictions-1x2-over-under-btts" rel="noopener noreferrer"&gt;Liga MX match predictions&lt;/a&gt;, and the rest are listed on the &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples" rel="noopener noreferrer"&gt;examples page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather build it yourself, the table of adjustments above and the ESPN endpoint are all you need to get started. Either way: simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>api</category>
    </item>
  </channel>
</rss>
