← Back to home
Flagship project

HoopIQ.

Before every NBA game, HoopIQ answers one question: given everything known right now — how both teams have been playing, who's healthy, where the game is, how rested everyone is — what is the probability each team wins, and why? It starts as a rigorous machine-learning engine and grows, stage by stage, into a live platform: automated data feeds, in-game win probability that updates as the game unfolds, and — as the final capstone — a Monte Carlo simulator that plays out entire seasons thousands of times.

ACTIVELY BUILDING · DAILY COMMITS View on GitHub ↗
25
NBA SEASONS (2000-01 → 2024-25)
~68K
TEAM-GAME ROWS · ~34K GAMES
63.9%
BASELINE ACCURACY, VALIDATED
+8.1pts
OVER THE NAIVE HOME-TEAM PICK
5 of 16 stages shipped — early innings, and that's the point updated July 2026
This is a multi-year build, documented honestly. The shipped stages are a validated foundation — everything after them is planned, sequenced, and public before it's written.
In plain terms

HoopIQ is a prediction system that refuses to cheat. The model only ever sees information that existed before tip-off. Its accuracy claims are verified with sanity checks, not just reported. Each new layer of sophistication is kept only if it measurably beats the simpler version it replaces. And the destination isn't a single guess per game — it's a living platform: predictions that explain themselves, a win probability that updates possession by possession while the game is on, and season-level forecasts expressed as full probability distributions.

Why a recruiter should care

This project is a working demonstration of how I'd operate on your team: a documented plan, honest baselines, benchmark-gated complexity, leakage paranoia, and a repository engineered to be maintained. The roadmap below is long on purpose — I'd rather show you a serious multi-year plan being executed in order than a small finished toy.

The roadmap

Sixteen stages, benchmark-gated.

The order is deliberate: dependencies first, highest predictive impact first, and no stage built on an unvalidated foundation. Click any stage to expand it.

Shipped — the validated foundation

A Python 3.11 environment with PyTorch and CUDA, configured for GPU training on my own hardware (RTX 5070 Ti, 17GB VRAM). The data pipeline pulls 25 NBA seasons (2000-01 through 2024-25) via the NBA stats API — roughly 68,000 team-game rows across ~34,000 unique games — then cleans them: win/loss converted to numeric targets, home/away flags computed, dates parsed and sorted chronologically. This dataset is the ground truth everything else stands on.

Win-loss records are a poor predictor, so HoopIQ replaces them with opponent-adjusted, pace-adjusted net ratings — points scored minus allowed per 100 possessions — computed over rolling windows (last 3, 5, 10, and 20 games, plus season-to-date). The opponent adjustment matters most: a hot streak built against weak teams gets correctly discounted, and a close loss to a great team isn't punished as if it were a blowout. This single idea carries more predictive weight than anything else in basketball analytics, which is exactly why it was built first.

On top of the ratings: rest days, back-to-back fatigue flags, win/loss streaks, separated offensive and defensive ratings, and head-to-head matchup context, merged into one master dataset. Opponent context comes from a GAME_ID self-merge, so each row knows exactly who was on the other side of the ball.

The non-negotiable rule: the model may only see information that existed before tip-off. Every rolling feature is lagged with strict .shift(1) discipline, so look-ahead bias is structurally impossible rather than merely "checked for." Accidentally peeking at the future is the most common way people fool themselves in sports prediction — the pipeline is designed so it can't happen silently.

A calibrated XGBoost classifier: 63.9% accuracy, +8.1 percentage points over always picking the home team. The number matters less than the evidence behind it. Three checks confirm the model learns genuine signal:

Shuffle test — retrain on randomly shuffled labels; accuracy collapses to chance, proving the features aren't leaking the answer. Baseline comparison — the model must beat the naive strategy by a real margin, not a rounding error. Calibration curve — when the model says 70%, it wins about 70% of the time, so its probabilities mean what they claim.

This stage also validated everything before it: if the foundation had a subtle bug, the baseline is where it would have surfaced.

A dynamic model-testing harness where any new architecture plugs into identical data loading, chronological train/test splits, and evaluation metrics — so every comparison in this project is apples-to-apples. A shared data_utils module is the single source of truth; a full refactor removed all code duplication across training, testing, and validation scripts.

The repository itself is part of the deliverable: modular src/ layout, clean commit history with daily commits, and a proper .gitignore keeping data and environments out of version control. If someone clones it, it should read like something a team could maintain.

In progress — right now

Head-to-head benchmarking of XGBoost vs. LightGBM vs. CatBoost inside the harness, on identical splits and metrics. My standing rule for the entire project: no model choice by assertion — every architecture earns its place on a leaderboard or it goes.

This stage is also hardening the harness itself. It has already caught real bugs — a silent LightGBM crash and an interface mismatch between the data loader and the model runner — which is precisely the kind of failure a shared testing framework exists to surface.

Up next — smarter, and always current

Today the dataset updates when I run the collector. This stage makes HoopIQ self-sustaining: a scheduled pipeline that pulls fresh results every night after games end, recomputes features, and keeps the model's inputs current without a human in the loop. Unglamorous, and essential — a prediction system that goes stale the moment you stop babysitting it isn't a system. This is also what makes in-season, day-of predictions possible at all.

The current features all come from the game log itself. The next signals come from outside it: injury reports and lineup availability — arguably the biggest single piece of information the model can't currently see. A star sitting out moves a game's odds enormously, and no rolling average captures it.

Each new source is added as a measured layer: integrate it, rerun the full benchmark, and keep it only if it improves on the validated baseline. Features that sound important but don't move the number get documented and cut.

The baseline needs temporal context hand-fed to it through rolling averages. The open question: can a recurrent network (RNN/GRU) learn form, momentum, and fatigue directly from game-by-game sequences — and beat 63.9%?

I'm going in clear-eyed. Well-engineered gradient-boosted trees frequently beat neural networks on tabular sports data, so the sequence model must outperform the incumbent on the same harness before it replaces anything. If it loses, that's a finding, not a failure — and it gets written up either way.

A point prediction says "the home team wins." A useful system says "the home team wins with 61% probability — and here is how confident I am in that 61%." This stage layers Bayesian methods over the best-performing predictor so the system quantifies its own uncertainty: the difference between a classifier and a forecasting tool you would trust with a real decision. It's also a prerequisite for honest season simulation at the end — uncertainty has to propagate, not disappear.

A probability alone doesn't earn trust. Every HoopIQ forecast will ship with factor attribution — a plain-English breakdown of what drove the number: "OKC 63% — recent defensive rating (+), home court (+), second night of a back-to-back (−), key starter questionable (−)." Under the hood that's feature-attribution analysis over the model; on the surface it's the difference between a black box and a tool an analyst would actually argue with. This is the layer that turns predictions into analysis.

Going live — the dynamic platform

This is where HoopIQ stops being a pregame tool and becomes dynamic. The NBA's live play-by-play feed updates every few seconds during a game — score, possession, what just happened. HoopIQ will consume that stream and re-score the win probability after every significant event: a made three, a foul trouble situation, a star heading to the locker room.

Architecturally: live game state flows in, the model re-evaluates, and updates push to the interface in real time. It's the same shape as any live decision system — continuous data in, continuously refreshed probabilities out — which is exactly why I want to build it. Watching your own model's confidence swing possession by possession is also the fastest way to learn where it's naive.

HoopIQ is versioned like a product, not a script — each version independently useful and deployable. The arc: a web dashboard showing tonight's slate with probabilities and their explanations → a public API so the predictions are programmable → a pick tracker that records every forecast the moment it's made, so the model's track record is public and unfudgeable → and eventually user accounts and a mobile experience.

The pick tracker is the part I care about most: a forecasting system that doesn't keep receipts on itself is just opinions with extra steps.

The long game — where it converges

Teams are made of players, and the ceiling of team-level modeling is the point where that abstraction breaks. This stage models every player individually: current form, career trajectories learned by GRUs over season sequences, age-performance curves fit with Kolmogorov-Arnold networks (smooth, interpretable rise-and-decline curves), game-to-game variance, and injury risk. A separate fine-tuned transformer reads off-court sentiment signals from text. Each sub-model was chosen for its data's shape — see the architecture table below.

The most granular layer: instead of predicting a game's outcome wholesale, simulate it possession by possession, treating each possession as a probabilistic scoring event driven by the players on the floor. The engine is calibrated logistic regression — deliberately simple, because it must evaluate millions of possessions fast while keeping probabilities honest. Game outcomes then emerge from the simulation rather than being guessed at the top level — a fundamentally more truthful way to model a sport, and the natural engine behind the live in-game probabilities of stage 12.

Everything above feeds this. A GPU-accelerated Monte Carlo simulator plays out entire NBA seasons thousands of times — every game, every matchup, uncertainty included — producing win-total distributions, playoff odds, and seed probabilities instead of single-number guesses. (The widget below is a deliberately tiny preview of the idea.)

And it unlocks the two applications this project was pointed at from day one. First, market comparison: measuring the model's probabilities against live prediction-market prices to test for systematic mispricings — a genuine quantitative-research exercise in forecast skill. Second, the research crossover: roster construction is literally a package query — select a set of players under salary constraints to maximize projected value — so my DREAM Lab optimization work and HoopIQ ultimately converge into one system.

Architecture decisions

The right model for each job.

One model can't do all of this well — the data comes in different shapes. Each component gets the architecture its data actually calls for, and every assignment still has to survive the benchmark before it's final.

ComponentModelWhy this one
Game prediction (team level)XGBoost — gradient-boosted treesThe strongest family on structured, tabular data. Current validated incumbent at 63.9%.
Player career trajectoriesGRU — recurrent networkCareers are sequences. A GRU reads season-by-season history and learns rise, peak, and decline directly.
Age-performance curvesKAN — Kolmogorov-Arnold networkProduces smooth, interpretable curves — you can see the shape of aging it learned, not just trust it.
Possession outcomesCalibrated logistic regressionMust score millions of possessions quickly with honest probabilities. Simple on purpose.
Off-court sentimentFine-tuned transformerSentiment lives in text; transformers are the right tool for reading it.
Season outcomesMonte Carlo — GPU-acceleratedTurns per-game probabilities into full distributions. Uncertainty propagates instead of vanishing.
Interactive demo

Run a tiny version right now.

Monte Carlo season simulation — live in your browser

A miniature of stage 16. Choose a team's per-game win probability, then simulate 3,000 independent 82-game seasons and watch the win-total distribution take shape. Notice the spread: even a "60% team" has real odds of a disappointing year. That spread is the entire argument for simulating seasons instead of predicting them.

mean wins 10th–90th percentile P(50+ wins) seasons simulated 0

The real engine replaces this fixed slider with calibrated, per-matchup probabilities — opponent strength, rest, and form vary game to game, and the sampling runs on GPU.

The three rules I build by

Complexity has to pay rent. The tempting move is jumping straight to deep learning. The disciplined move is a validated baseline first — so when a sophisticated model ships, I can prove it earned its place instead of asserting it. Every stage on this page follows that rule.

Leakage paranoia is a feature. Sports data offers endless subtle ways to accidentally see the future. The pipeline makes look-ahead bias structurally impossible, because "we checked for it" is not a guarantee.

Built to be used, not listed. This runs during the actual season — daily commits, a clean public repository, and full refactors over quick patches when the architecture demands it. A project you actually use is a project you can't fake.

Think a stage is ordered wrong?

I genuinely want to hear it. If you've built forecasting systems, worked in sports analytics, or just see a flaw in the plan — challenge it. That kind of input is exactly why this page exists.