Coder's Bracket 2025
4It’s been a long time since I’ve filled out a March Madness bracket. The last time I used code to fill one out back in 2016 we won @mfladd a trip to Disney World (see @shawn, You Sweet Inglorious Bastard!). And then I got a sweet t-shirt out of the deal that his kids picked out for me during their trip as a thank you!
This all started back in 2014 when I created an algorithm based on the statistical winning percentage over the past 29 years of each seed broken down by round. This approach was a disaster (see Code Bracket. Because Science.).
In 2015 I made a small tweak that also takes into account the team’s winning percentage (adjusted for strength of schedule using the Rating Percentage Index). This approach got me into the top 90.2% of all brackets in a code competition (see Coders Bracket 2015).
In 2016 I made a tweak to further adjust based on the deltas of the two team’s 3-point percentage, field goal percentage, and free throw percentage. This was the Disney award winning approach (see Coder’s Bracket 2016).
The code bracket competition shut down in 2017 so I stopped doing them because gathering all the data is quite a chore…
…but now we have AI.
Luckily, the Wayback Machine has a saved copy of my 2016 algorithm: https://web.archive.org/web/20160825233719/https://www.codersbracket.com/code_bracket/56e700edb3c99cd90063074a
The bigger challenge was gathering all the data needed for this year’s March Madness tournament. A few prompts with ChatGPT 4.5 and “deep research” that was released earlier this year (https://openai.com/index/introducing-deep-research/) and I have a JSON file that looked like this:
[ | |
{ | |
"name": "Auburn", | |
"seed": 1, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 33.5, | |
"three_point_percentage": 30, | |
"turnover_percentage": 11.5 | |
}, | |
{ | |
"name": "AlabamaState", | |
"seed": 16, | |
"defensive_rebound_percentage": 68.9, | |
"free_throw_rate": 29.9, | |
"three_point_percentage": 32.9, | |
"turnover_percentage": 13.2 | |
}, | |
{ | |
"name": "SaintFrancis", | |
"seed": 16, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": null, | |
"three_point_percentage": 33, | |
"turnover_percentage": null | |
}, | |
{ | |
"name": "MichiganState", | |
"seed": 2, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 30.599999999999998, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Bryant", | |
"seed": 15, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": null, | |
"three_point_percentage": 34, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "IowaState", | |
"seed": 3, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": null, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Lipscomb", | |
"seed": 14, | |
"defensive_rebound_percentage": 67, | |
"free_throw_rate": 38.4, | |
"three_point_percentage": 37, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "TexasAM", | |
"seed": 4, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 36.4, | |
"three_point_percentage": 34, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Yale", | |
"seed": 13, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 29.9, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Michigan", | |
"seed": 5, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 31, | |
"three_point_percentage": 33, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "UCSD", | |
"seed": 12, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 38.3, | |
"three_point_percentage": 38, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "OleMiss", | |
"seed": 6, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 30.599999999999998, | |
"three_point_percentage": 34, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "SanDiegoState", | |
"seed": 11, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 28.7, | |
"three_point_percentage": 34, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "NorthCarolina", | |
"seed": 11, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 32.6, | |
"three_point_percentage": 32, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Marquette", | |
"seed": 7, | |
"defensive_rebound_percentage": 71, | |
"free_throw_rate": 27.6, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "NewMexico", | |
"seed": 10, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 31.2, | |
"three_point_percentage": 33, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Louisville", | |
"seed": 8, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": 33, | |
"three_point_percentage": 34, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "Creighton", | |
"seed": 9, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 28.299999999999997, | |
"three_point_percentage": 36, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "Duke", | |
"seed": 1, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 28.999999999999996, | |
"three_point_percentage": 37, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "American", | |
"seed": 16, | |
"defensive_rebound_percentage": 69, | |
"free_throw_rate": 28.000000000000004, | |
"three_point_percentage": 35, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "MountStMarys", | |
"seed": 16, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 29.799999999999997, | |
"three_point_percentage": 33, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "MississippiState", | |
"seed": 8, | |
"defensive_rebound_percentage": 77, | |
"free_throw_rate": 34.4, | |
"three_point_percentage": 31, | |
"turnover_percentage": 19 | |
}, | |
{ | |
"name": "Baylor", | |
"seed": 9, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 33.800000000000004, | |
"three_point_percentage": 35, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Oregon", | |
"seed": 5, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": 32, | |
"three_point_percentage": 34, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Liberty", | |
"seed": 12, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 28.000000000000004, | |
"three_point_percentage": 39.5, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Arizona", | |
"seed": 4, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 29.9, | |
"three_point_percentage": 35, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Akron", | |
"seed": 13, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 31.4, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "BYU", | |
"seed": 6, | |
"defensive_rebound_percentage": 71, | |
"free_throw_rate": 33.1, | |
"three_point_percentage": 37, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "VCU", | |
"seed": 11, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 32, | |
"three_point_percentage": 34, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Wisconsin", | |
"seed": 3, | |
"defensive_rebound_percentage": 76, | |
"free_throw_rate": 28.1, | |
"three_point_percentage": 35, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "Montana", | |
"seed": 14, | |
"defensive_rebound_percentage": 69, | |
"free_throw_rate": 29.799999999999997, | |
"three_point_percentage": 34, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "SaintMarys", | |
"seed": 7, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 27.800000000000004, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Vanderbilt", | |
"seed": 10, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 28.999999999999996, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Alabama", | |
"seed": 2, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 34.2, | |
"three_point_percentage": 35, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "RobertMorris", | |
"seed": 15, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 30, | |
"three_point_percentage": 37, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Houston", | |
"seed": 1, | |
"defensive_rebound_percentage": 78, | |
"free_throw_rate": 34.4, | |
"three_point_percentage": 39.8, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "SIUEdwardsville", | |
"seed": 16, | |
"defensive_rebound_percentage": 71, | |
"free_throw_rate": 32.9, | |
"three_point_percentage": 34, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Gonzaga", | |
"seed": 8, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 33, | |
"three_point_percentage": 37, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Georgia", | |
"seed": 9, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 32, | |
"three_point_percentage": 34, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "Clemson", | |
"seed": 5, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": 30.2, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "McNeese", | |
"seed": 12, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 32, | |
"three_point_percentage": 35, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Purdue", | |
"seed": 4, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 29.599999999999998, | |
"three_point_percentage": 33, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "HighPoint", | |
"seed": 13, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 31, | |
"three_point_percentage": 35, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "Illinois", | |
"seed": 6, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 32.6, | |
"three_point_percentage": 33, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Texas", | |
"seed": 11, | |
"defensive_rebound_percentage": 71, | |
"free_throw_rate": 33.5, | |
"three_point_percentage": 34, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Xavier", | |
"seed": 11, | |
"defensive_rebound_percentage": 69, | |
"free_throw_rate": 31.4, | |
"three_point_percentage": 38.8, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "UCLA", | |
"seed": 7, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 32.9, | |
"three_point_percentage": 34, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "UtahState", | |
"seed": 10, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 32, | |
"three_point_percentage": 38, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "Tennessee", | |
"seed": 2, | |
"defensive_rebound_percentage": 77, | |
"free_throw_rate": 28.999999999999996, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Wofford", | |
"seed": 15, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 30.3, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Florida", | |
"seed": 1, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 32, | |
"three_point_percentage": 38, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "NorfolkState", | |
"seed": 16, | |
"defensive_rebound_percentage": 67, | |
"free_throw_rate": 31, | |
"three_point_percentage": 33, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "UConn", | |
"seed": 8, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": 34, | |
"three_point_percentage": 35, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Oklahoma", | |
"seed": 9, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 32.2, | |
"three_point_percentage": 34, | |
"turnover_percentage": 18 | |
}, | |
{ | |
"name": "StJohns", | |
"seed": 2, | |
"defensive_rebound_percentage": 79, | |
"free_throw_rate": 28.9, | |
"three_point_percentage": 34, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Omaha", | |
"seed": 15, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 32, | |
"three_point_percentage": 35, | |
"turnover_percentage": 19 | |
}, | |
{ | |
"name": "TexasTech", | |
"seed": 3, | |
"defensive_rebound_percentage": 74, | |
"free_throw_rate": 31.8, | |
"three_point_percentage": 38, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "UNCWilmington", | |
"seed": 14, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 31.5, | |
"three_point_percentage": 33, | |
"turnover_percentage": 16 | |
}, | |
{ | |
"name": "Maryland", | |
"seed": 4, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 30, | |
"three_point_percentage": 34, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "GrandCanyon", | |
"seed": 13, | |
"defensive_rebound_percentage": 69, | |
"free_throw_rate": 32.2, | |
"three_point_percentage": 36, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Memphis", | |
"seed": 5, | |
"defensive_rebound_percentage": 70, | |
"free_throw_rate": 31, | |
"three_point_percentage": 34, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "ColoradoState", | |
"seed": 12, | |
"defensive_rebound_percentage": 71, | |
"free_throw_rate": 30, | |
"three_point_percentage": 36, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Missouri", | |
"seed": 6, | |
"defensive_rebound_percentage": 68, | |
"free_throw_rate": 33.1, | |
"three_point_percentage": 35, | |
"turnover_percentage": 14 | |
}, | |
{ | |
"name": "Drake", | |
"seed": 11, | |
"defensive_rebound_percentage": 73, | |
"free_throw_rate": 29.799999999999997, | |
"three_point_percentage": 37, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Kansas", | |
"seed": 7, | |
"defensive_rebound_percentage": 75, | |
"free_throw_rate": 30, | |
"three_point_percentage": 34, | |
"turnover_percentage": 15 | |
}, | |
{ | |
"name": "Arkansas", | |
"seed": 10, | |
"defensive_rebound_percentage": 72, | |
"free_throw_rate": 33.1, | |
"three_point_percentage": 31, | |
"turnover_percentage": 17 | |
}, | |
{ | |
"name": "Kentucky", | |
"seed": 3, | |
"defensive_rebound_percentage": 76, | |
"free_throw_rate": 31.5, | |
"three_point_percentage": 36.5, | |
"turnover_percentage": 14 | |
} | |
] |
This didn’t give me exactly everything we had before (we’re missing RPI ratings) but we picked up a few new bits of data (free-throw rate, defensive rebound percentage, etc). Using this data I coded up some quick tweaks to our 2016 algorithm. Here’s the 2025 version:
const teams = require('./2025.json'); | |
const percentages = { | |
round1: { 1: 100, 2: 94, 3: 85, 4: 78, 5: 68, 6: 66, 7: 60, 8: 48, 9: 52, 10: 40, 11: 34, 12: 35, 13: 22, 14: 15, 15: 6, 16: 0 }, | |
round2: { 1: 87, 2: 69, 3: 61, 4: 56, 5: 52, 6: 51, 7: 27, 8: 18, 9: 8, 10: 46, 11: 38, 12: 49, 13: 24, 14: 12, 15: 14, 16: 0 }, | |
round3: { 1: 79, 2: 72, 3: 50, 4: 35, 5: 21, 6: 33, 7: 37, 8: 70, 9: 40, 10: 33, 11: 33, 12: 5, 13: 0, 14: 0, 15: 0, 16: 0 }, | |
round4: { 1: 59, 2: 46, 3: 47, 4: 72, 5: 75, 6: 23, 7: 0, 8: 57, 9: 50, 10: 0, 11: 60, 12: 0, 13: 0, 14: 0, 15: 0, 16: 0 }, | |
round5: { 1: 57, 2: 48, 3: 64, 4: 23, 5: 50, 6: 67, 7: 0, 8: 50, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 0 }, | |
round6: { 1: 67, 2: 33, 3: 44, 4: 33, 5: 0, 6: 50, 7: 0, 8: 50, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 0 } | |
}; | |
function predictWinner(game, team1, team2) { | |
let lowerSeededTeam = team1; | |
let higherSeededTeam = team2; | |
if (team1.seed === team2.seed) { | |
lowerSeededTeam = (team1.net_ranking < team2.net_ranking) ? team1 : team2; | |
higherSeededTeam = (lowerSeededTeam === team1) ? team2 : team1; | |
} else if (team2.seed < team1.seed) { | |
lowerSeededTeam = team2; | |
higherSeededTeam = team1; | |
} else { | |
lowerSeededTeam = team1; | |
higherSeededTeam = team2; | |
} | |
let percentage = percentages['round' + game.round][lowerSeededTeam.seed]; | |
// 3-point percentage | |
percentage += (lowerSeededTeam.three_point_percentage - higherSeededTeam.three_point_percentage) * 1.5; | |
// Free Throw Rate (FTA/FGA) | |
if (lowerSeededTeam.free_throw_rate && higherSeededTeam.free_throw_rate) { | |
percentage += (lowerSeededTeam.free_throw_rate - higherSeededTeam.free_throw_rate) * 1.4; | |
} | |
// Defensive rebound percentage | |
percentage += (lowerSeededTeam.defensive_rebound_percentage - higherSeededTeam.defensive_rebound_percentage) * 1.3; | |
// Turnover rate (negative adjustment since fewer turnovers are better) | |
percentage += (higherSeededTeam.turnover_percentage - lowerSeededTeam.turnover_percentage) * -1.2; | |
const random = Math.random() * 100; | |
if (random <= percentage) { | |
return lowerSeededTeam; // lower seed advances | |
} else { | |
return higherSeededTeam; // higher seed advances | |
} | |
} |
And here’s the results:
How are you all filling out your brackets this year?
- 1 comment, 2 replies
- Comment
I clicked thinking this might be interesting but I got sportsball instead.
@yakkoTDI
@blaineg I have that shirt.