Building a Full NBA Betting Model from Scratch to Beat Polymarket
Building an NBA Betting Model from Scratch (& Putting It on the Internet!)
For years, I've wanted to build an NBA betting model. The NBA is my favorite sport to watch (I watch a LOT of basketball), & building predictive models on sports datasets is one of my favorite ways to spend time practicing Machine Learning techniques. Whenever I build & share predictive basketball models with friends, their follow-up question is almost always: "When are you going to make a model for sports betting?"
This use case felt like the perfect next project - both predicting NBA betting market imperfections while also making it live & easily accessible for realtime analysis. But I always struggled where to start with this; specifically, what data to pull, & how to compare my model's predictions against a relevant & publicly available market price. A few weeks ago, I finally cracked it: the result is a 5-model stacked ensemble predictor (trained on >10k regular & playoff NBA games since 2019-20) that pulls live NBA Polymarket, finds the games where my model & the markets disagree the most, sizes a bet against my running bankroll, & publishes betting recommendations in natural language to my GitHub website.
Speaking of AI: Claude not only helped me pull data for this project via the NBA API, conduct relevant feature engineering, & translate my model's predictions & feature-level insights into more human-readable form, but it also helped me write most of this article!
My model is so far 6-3 after testing it in the NBA Conference Finals. It has been cool to (directionally) see that my model is performing well as a betting tool, especially since it was high on the Spurs upsetting OKC!
🏀 The Idea 🏀
This project got its legs after I read this tweet by @zostaff, where he shared open-source code pulling NBA box score & player-level injury data from the NBA API alongside live game odds from the Polymarket (prediction market) API.
Since Polymarket allows for users to see real-time & historical prediction market data (to the granularity of individual anonymized bets made), this felt like the perfect way to make real-time inferences on NBA games while comparing them on a daily basis to market stats when identifying betting opportunities. Sourcing both halves of the problem - model inputs from one API, market consensus from another - engendered some really cool ideas.
Here's an end-to-end diagram of my system at a glance:
[[IMAGE: system architecture diagram]]The Raspberry Pi is a tiny computer that runs this whole pipeline daily, pulling data from three places: nba_api (NBA schedules & box scores), Polymarket for live betting odds, & the Claude API for natural-language analysis on each.
📊 The Data 📊
The model trains on regular season + playoff games from 2019-20 through 2025-26, aka >10k box scores. Leveraging this NBA API, I can retrain monthly during the season to keep current with rolling-10 game team stats, the impacts of back-to-back & home vs away trips, & in-season injuries/team changes impacting team production.
Feature categories pulled into training & inference (all prefixed for easy column-selection in pandas):
- Team ELO ratings & the gap between them (emulating the ELO ratings system in chess to rank teams head-to-head).
- Home & away per-team rolling 10 game stats: Offensive/defensive ratings, recent net rating, pace, & % of typical player production available due to injuries.
- Home vs away difference metrics: home-minus-away gaps across most feature dimensions, so the model can see relative strength directly instead of having to learn the subtraction.
- Recent head-to-head matchup history between the two specific teams.
- Days of rest, back-to-back indicators, & travel pattern signals.
- Historical player-level injuries: The percent of offensive/defensive production playing a given game based on availability.
To me, the most interesting feature set involves the player-level injury/availability data. The NBA exposes hundreds of thousands of player-level records through the nba_api. Injury data has been something I have historically struggled to represent in prior predictive models such as March Madness. I'm in turn able to roll availability up to team-level percentages (e.g. "92% of typical offensive production available"). Furthermore, for inference I can use this in realtime to track injury reports pregame!
🧠 The Model 🧠
I played around with different model types, & for fun I landed on a 5-model stacked ensemble: Logistic Regression, Random Forest, XGBoost, LightGBM, & Extra Trees. Four are tree-based (good at non-linear feature interactions) & one is linear (good for calibration). Each one looks at the same input data but tends to disagree with the others on different types of games, since some models' trees & leaves may value various features/feature interactions very differently. A meta-model on top (the overall ensemble model) learns when to trust each base model & weights them accordingly, which can lead to stronger predictions than a single model alone (though if I were productionalizing this for a work project I would just stick to one model for simplicity). My model code can be found here, and my inference code here!
Why mostly tree-based: NBA outcomes are interaction-heavy. "ELO of 1700" means very different things on the road vs at home, on a back-to-back vs rested, or against a strong defense vs a weak one. Trees pick up these kinds of interactions natively without me having to hand-craft cross-features. Logistic regression is in there to anchor calibration on the cleaner features (ELO gap, point differential). I tested other model types such as Neural Networks & Support Vector Machines, but landed on this tree-based + logistic regression focus since they tended to engender lower log-loss metrics overall with tight margins of error.
Why optimize for log loss instead of accuracy: This was one of the most important design decisions in the project. For betting specifically, you don't just care if your pick is right … you care if your probability/confidence is right. A model that says "90% chance" & is correct 70% of the time is overconfident, & overconfidence usually leads to the house eventually winning. Proper scoring rules like log loss & Brier score penalize miscalibration directly. Moreover, I can leverage these confidence scores directly in terms of the bet magnitudes that the model recommends (higher bets when confidence is higher & vice-versa) - hopefully allowing me to maximize profit while betting less when the model sees less of an opportunity.
Where the models landed (out-of-fold, time-series CV):
[[IMAGE: Brier score comparison chart]]All 5 base models cluster in a tight Brier-score range, with the meta-model on top learning when to weight each one.
What Brier ~0.22 means: A coin flip gets Brier 0.25 (the random baseline) whereas Vegas closing lines sit around 0.18 to 0.20. Thus, my model lands in the middle (meaningfully better than random but nowhere near professional-grade). For betting, the gap between my model & the market predictions represents how much of an edge exists.
Interestingly, Brier scores plateaued in the 0.215 to 0.222 range across all 5 models after exhaustive tuning. The biggest gains came from feature engineering (better lag windows, opponent-adjusted stats, injury aggregations), not from picking the perfect model hyper-parameters. I'll definitely lean further into feature engineering in future iterations, especially in terms of thinking through what other datasets might be out there that my current dataset categories currently do not represent regarding NBA game predictions.
I also tested including older seasons (2010-2018) to expand the training set, but including more training data did not actually help - Brier scores stayed flat, training time ballooned, & the confidence intervals around predictions actually got wider. My theory is that the NBA has changed pretty drastically in terms of overall play style (pace, three-point rates, switching defenses, etc.) pre-2019 so including this data would have just added data drift.
🎯 The Betting Strategy 🎯
End-to-end inference runs multiple times daily on a Raspberry Pi 4 via cron schedule:
- Weekdays: 6:00 PM & 8:30 PM ET (catches East Coast tipoffs & West Coast tipoffs respectively)
- Weekends: 12:30 PM, 3:30 PM, & 6:30 PM ET (since weekend slates include afternoon tipoffs in addition to the evening ones)
Each run does the following:
- Fetch today's NBA schedule from nba_api
- Build feature vectors for each matchup using current team state
- Run the ensemble to get each team's win probability
- Fetch matching Polymarket markets (gamma-api for game discovery, CLOB for prices, liquidity, order book depth)
- Compute the edge: model probability minus market probability, on each side
- Categorize the game as FAVORITE_BET (positive edge with market consensus) / OPPORTUNITY (positive edge against market consensus to bet on the underdog) / PASS (edge below margin-of-error for the model) / AVOID (illiquid market)
- Compute a bet confidence/attractiveness score (0-10) combining edge magnitude, liquidity, money flow confirmation, & calibration confidence
- Size the bet as (attractiveness/10) × current_bankroll, with multiplicative haircuts (× 0.25 for underdog picks, × 0.50 for low-confidence picks, both can compound), capped at 50% of bankroll per game
- Generate natural-language analysis via the Anthropic API (Claude Sonnet 4.6), using the same numbers I'd want an NBA quant analyst to look at, structured into 3-5 bullets plus a TL;DR per game
- Render the dashboard as static HTML & commit to a GitHub Pages repo via a deploy key at the aforementioned cron job schedule
💻 The Website 💻
My predictions are live on my GitHub Pages website, which I branded "Hidden Lines." What lives on the page:
- P&L strip at the top: running bankroll, W-L record with win rate, net cash flow, starting bankroll, plus W-L broken out by confidence tier (high, medium, low).
- Today's picks: one-line bullets per game ("NYK @ SAS · spread SAS -6.0 · $15.30 wager on the San Antonio Spurs").
- Summary stats: games on slate, actionable bets, total $ recommended.
- Per-game deep dives for up to ~10 games per day. Each card shows model vs market probability side by side, projected margin, expected value per $1 wagered, Kelly stake, liquidity, top SHAP features pushing toward each team, attractiveness score with calibration bin, & a Claude-generated bullet analysis plus TL;DR.
- Historical Results: every past bet, color-coded green or red, with actual MOV vs projected MOV.
- Performance charts: cumulative net P&L line chart by date, daily wins-vs-losses bar chart.
Historical results table with color-coded outcomes, plus cumulative P&L & daily W-L charts that grow as more bets resolve. This allows me to keep track of my results over time!
The site updates multiple times per day on game days. Off-days leave yesterday's site up but still update the bankroll if a previous-day bet resolved.
📈 Early Results 📈
At the time of writing this post, the model's sitting at 6-3 overall, with my simulated bankroll up from $50 to ~$63 (+26% return). I'm running this as a paper-trading sim through the 2025-26 playoffs to validate before deploying real money for the 2026-27 season.
[[IMAGE: early results screenshot]]Though a small sample size so far, a 6-3 record tells me that the pipeline works & the predictions are directionally very promising. It does not tell me the model is consistently beating the market, but it's at least fun to see that it is so far!
I also predict (pun intended) that this model could excel in the regular season, where factors like injuries/back-to-back games/home vs road trips/etc. might matter more than in the playoffs, & larger opportunities may exist between good & bad teams than what existed in the Conference Finals. But with that being said, it's been interesting to see that the model picked both the Spurs (4x, 1x as an underdog opportunity) & OKC Thunder (2x) throughout the series thus far, which makes me feel confident that it factors in competitive advantages like being home vs away well already (& that it might have seen an edge in the Spurs as Vegas underdogs even though they ended up winning the series). Additionally, I derive spreads for games based on the model's predictions probabilities, & it's been cool to see that these lines often fall 0-2 points of Vegas spreads.
A few things I'm planning to do to actually pressure-test this:
- Backtest over the offseason. Walk-forward simulation across the full 2019 to 2025 dataset to see what the system would have produced day-by-day with realistic liquidity assumptions. Right now I have point-in-time predictions but no rigorous historical P&L curve. That's the gap I want to close before next October.
- Scale up volume next regular season. Right now I'm betting one game per day because we're deep in the playoffs & the daily slate is small (1-2 games). Next regular season, the model will be predicting on ~10 games per night. More bets per day means faster signal-to-noise convergence on whether real edge exists.
- Track closing line value (CLV). Comparing my model's price at prediction time to Polymarket's closing price right before tipoff. CLV is a much more reliable performance metric than W/L because it removes the variance of single-game outcomes - if I'm consistently picking sides that the market drifts toward by tip, that's strong signal even when individual games don't hit.
💡 What's Next 💡
- Create an autonomous Polymarket betting bot. Since Polymarket functions via a blockchain, there is the ability to create a bot that would trigger these bets for me, which thus far I have not been doing (I have not been making these actual bets either, just tracking them with a simulated $50 bankroll). After further pressure testing the model's results this would be a really fun way to see in realtime how the model is doing.
- Create a spread-specific model. As aforementioned, I am deriving spread from the model's probability predictions that the home or away team will win. I would love to couple this model with an actual points predictor model given all of this rich box score data at my disposal prior to next season.
- Apply the same architecture to other sports. NFL is the obvious next one (mature data ecosystem via nflfastR, higher per-game variance creates more model vs market dispersion). MLB has the deepest historical dataset of any sport & a daily cadence that fits the daily-update site format perfectly.
- Player-level injury modeling. As mentioned in the data section, further encoding which player is out (with their usage rate, offensive load, defensive impact) instead of aggregating to a team-level percentage. The infrastructure exists in nba_api, & the feature engineering is mainly what's missing.
- Auto-A/B test framework on monthly retrains. When I retrain monthly, ship both the old & new model. Route some predictions through each, learning which feature engineering changes actually improved out-of-sample performance, versus which I just imagined helped.
Thanks Claude for the last 3 recommendations on future next steps!
~~~ Please Bet Responsibly ~~~
One important caveat I want to be clear about: I'm not really a bettor. I built this because NBA is my favorite sport to watch & I love building models on sports datasets - the real-money loop is just a fun way to stress-test a model in a live environment. If you act on any of these picks, bet responsibly. Past performance doesn't predict future results, & a small sample of correct picks is not yet evidence that the model has actually beaten the market.
If you've built something similar, or have thoughts on improving calibration, feature engineering, or anything else about the approach - please comment or DM me with your ideas! I'd love to keep iterating & learning here, & combining two of my favorite things (NBA + data science) has been a really enjoyable way to spend my free time these last few weeks.
Comments
Post a Comment