Every betting model has a blind spot. A gradient-boosted tree trained on engineered features will overfit to whatever weird outlier games shaped its splits. A neural net trained on raw inputs will catch interactions the tree missed but blow up on small-sample groups. Run them side by side and one wins this week, the other wins next week, and you stare at the leaderboard wondering which one to bet on Sunday. The answer is neither: bet the stack. This is a walkthrough of how to stack two models on Workshop in about ten minutes, with zero Python.
Why stacking beats picking
Imagine two models with the same long-run Brier score of 0.230. Model A nails road dogs but flubs primetime games. Model B is solid in primetime but mediocre on dogs. If you bet only one model, you're betting one model's blind spot every week. If you stack them and let a meta-learner figure out which one to trust per game, your effective Brier drops to roughly 0.224 — about 6 basis points, which over 500 bets is real money.
The gain compounds. With uncorrelated models you get the bigger Brier improvement, but most public sports models share data sources (the same play-by-play, the same Vegas closes), so the residuals correlate at about 0.5 to 0.7. That still leaves enough independent signal for stacking to win.
What stacking is not
Stacking is not running both models, picking whichever one shows the bigger edge, and betting that one. That approach is selection bias dressed up as ensembling — you systematically bet the model that happens to have the most extreme prediction, which over-weights tail noise. Stacking is a single combined probability produced by a meta-learner that's trained on out-of-fold predictions. The bet you place uses that combined number, not the individual base numbers.
The two base models we'll stack
For this walkthrough we'll use two NFL spread models that are already in the catalog on Workshop:
- XGBoost on engineered features. 24 inputs: team rolling EPA per play, opponent-adjusted DVOA, rest days, home/away, weather buckets, injury weight, line movement, public bet percent. Roughly 80ms inference per game. ~85MB model file (uncompressed JSON of tree splits).
- TF.js dense net. 64-32-16 hidden units, raw inputs from the last six games of each team plus the Vegas spread. Roughly 12ms inference per game. ~2.4MB model file. Trained with dropout 0.2.
Both score around 0.235 Brier on the 2024 NFL regular season. The XGBoost is better at home favorites; the TF.js net is better at division road dogs. The point of stacking is exactly this kind of complementary structure.
Latency and memory matter for browser stacks
If you're running the stack inside the browser tab on Tinker, you care about how much RAM and CPU the combined inference costs. The XGBoost-via-JS port we use loads about 85MB into memory and spikes to roughly 110MB during prediction batches due to copying. The TF.js net stays under 8MB resident. On a typical laptop with 16GB total, both fit easily. On a phone with 4GB, the XGBoost model is too heavy — for mobile-first stacks, use a quantized version of XGBoost (3-bit splits, drops to about 22MB) or skip it and stack two TF.js variants.
Combined batch inference for 16 NFL games: roughly 1.5 seconds on a mid-range laptop CPU, dominated by the XGBoost side. Plenty fast for weekly card prep; too slow for live in-play. For in-play use, prefer the all-TF.js stack.
Step-by-step in Workshop
The flow on the Workshop page once you have both base models published:
- Open the Stack builder. Drag both base models onto the canvas.
- Configure the validation fold split. Default is rolling time-series cross-validation with five folds and a one-week gap between training and validation to prevent leakage from same-week games.
- Generate out-of-fold predictions. Workshop runs each base model five times (once per fold), generating predictions on the held-out fold each time. The combined table has true labels and base-model predictions for every training game.
- Choose a meta-learner. Default: logistic regression with L2 = 0.1. Alternative: simple fixed-weight average that defaults to weights inversely proportional to each base model's holdout Brier.
- Fit the meta-learner. Workshop trains the meta-learner on out-of-fold predictions vs true labels. Output: two weights that sum to one (e.g., 0.58 XGBoost, 0.42 TF.js), plus an intercept for any systematic bias.
- Calibrate the stack. Run isotonic regression on the meta-learner outputs vs labels. This squeezes out any residual miscalibration introduced by the linear combination. The calibrated stack is what you bet.
- Score on the held-out test season. Workshop shows side-by-side Brier, log loss, calibration curve, and bankroll simulation for the stack vs each base model.
The whole flow runs entirely client-side — your data, your models, your stack. Nothing uploads. The full TF.js side is documented in the client-side vs Python comparison if you want the privacy and latency trade-off detail.
What good output looks like
For the NFL spread example above, here is the kind of result the stack produces on a held-out 2024 test set of 272 regular-season games:
- XGBoost alone: Brier 0.2348, log loss 0.6671, ATS win rate 52.6% at -110 (break-even 52.4%).
- TF.js net alone: Brier 0.2356, log loss 0.6688, ATS win rate 52.2%.
- Stack (logistic meta, 0.58/0.42): Brier 0.2289, log loss 0.6594, ATS win rate 53.4%.
- Calibrated stack (after isotonic): Brier 0.2278, log loss 0.6582, ATS win rate 53.4% but with much sharper edge distribution.
The Brier improvement of 0.0070 is what the academic literature would call a small effect. For a bettor at $50/game, it translates to about $190 of incremental expected profit per 100 spread bets at -110. Multiply that across a season and the stack is paying for itself even if you're a hobbyist.
Watching for stack rot
Meta-learner weights drift. If the underlying data distribution shifts (rules change, refs change, pace changes), one base model can age out faster than the other and the stack will lag. Set a calendar reminder to retrain the meta-learner monthly during the season, weekly during the playoffs. The retrain is cheap — about 30 seconds in Workshop — and the alternative is betting last year's stack into a market that has moved on.
Common stacking mistakes
- Leakage via shared features. If both base models include "line movement" as a feature, the meta-learner doesn't actually learn anything novel — it just smooths noise. Make sure each base model contributes at least some independent information.
- Stacking on raw probabilities without calibration. If model A always predicts in the 30-70% range and model B uses the full 5-95% range, the meta-learner gets confused. Calibrate each base model before stacking, not just the final output.
- Too many base models. Five models with 200 games of training data is a recipe for meta-overfit. Two or three base models is usually right for a single-sport bettor stack. Save the seven-model ensembles for the Kaggle competition crowd.
- Forgetting to retrain the meta-learner. The base models can stay frozen for a while; the meta-learner has to respond to changing market efficiency and changing data distributions. The relative weights matter more than the absolute model accuracy.
- Betting the stack edge without size discipline. The stack's edge looks bigger than each individual base model's edge, which makes it tempting to size up. Don't — apply the same Kelly fraction rules with the calibrated stack probability as the input.
Bridging to other surfaces
Once you have a working stack in Workshop, push it to Tinker as a single deployable brick. The brick exposes the calibrated stack probability as its output, and downstream consumers (the live picks page, the Kelly sizing brick, the CLV tracker) don't need to know it's a stack underneath. This is the right modularity: each surface trusts the brick's output without having to retrain it. You can then publish the stacked model to your public page and let it accumulate a track record, the same way the open-source release notes work in the reproducible backtest checklist.
Stacking across sports vs within a sport
An underexplored variant: stacking the same architecture trained on different sports. The intuition: cross-sport regularization. A spread model trained jointly on NFL, college football, and Australian rules football won't beat sport-specific models on accuracy, but the feature representations learned in the encoder transfer across, and the meta-learner picks the strongest signal per game.
Two reasonable approaches:
- Multi-task base model: one XGBoost or TF.js model with sport as a categorical feature, trained on the union. Stack against a per-sport specialist.
- Per-sport base models, sport-aware meta-learner: train a separate base model per sport, then a meta-learner that includes sport ID as a feature and learns sport-specific weights.
For most retail bettors, approach #2 is simpler to implement on Workshop and easier to debug. Cross-sport stacking helps most in low-data sports (NHL totals, MLS spreads) where the multi-sport signal compensates for the small per-sport sample.
Production hygiene for stacks
A stack is more fragile than a single model because three things have to stay in sync: both base models, the meta-learner weights, and the calibration mapping. Practical checklist:
- Version each component. If you retrain a base model, give it a new version ID. The stack should reference exact versions of each base.
- Test before publishing. After any change, score the new stack on the most recent holdout fold and compare to the old stack. If Brier got worse, don't ship.
- Roll back atomically. If the new stack underperforms in live betting over 30 games, roll back all components together, not just the meta-learner.
- Monitor base-model agreement. If your two base models start agreeing on every game (correlation near 1.0), the stack adds nothing. Investigate whether they've converged on the same features.
- Refit the meta-learner monthly during the season. Base models can stay frozen longer; meta-learners need to adapt.
When to retire a base model
If the meta-learner weight on one base drops below 0.10 for three consecutive months, that base is no longer pulling its weight. Either retrain it on a refreshed dataset or drop it from the stack and find a more diverse base. A stack with two weights near 0.5 each is the healthiest state.
Diagnosing stack failures
When a stack underperforms its base models on a holdout, the diagnosis path:
- Is the meta-learner over-weighted on one base? Weights of 0.95/0.05 mean stacking didn't help. Inspect whether the base models actually generate different predictions.
- Did the calibration mapping break? Recompute the calibration on a fresh fold; bad calibration can hide as bad stack performance.
- Sample size? Under 200 holdout games, expected noise can mask real improvements. Wait for more data.
- Concept drift? If the data distribution shifted (rule change, schedule format, new venue), both base models lag and the stack lags with them. Retrain bases.
Cost of stacking vs single-model
Time cost of running the stack pipeline once on Workshop: about 90 seconds for a single sport, including out-of-fold prediction generation and meta-learner fitting. Memory cost in the browser: roughly the sum of both base models plus a few MB for the meta-learner — usually well under 200MB total.
Bet-sizing impact: the calibrated stack probability replaces the raw single-model probability everywhere downstream — Kelly sizing, edge filtering, CLV tracking. Nothing else needs to change.
Bottom line
Stacking is the cheapest legitimate way to squeeze 5–10 basis points of Brier out of your modeling stack. It takes ten minutes in Workshop once you have two base models. It does not require Python or a server. The math is straightforward, the trap is leakage, and the win is real over a meaningful sample. Stack two of your sharpest models, calibrate the result, and bet the stack — not your gut between them.
Bet responsibly — set limits, never chase losses.
Named modeling examples
A model page is more useful when the feature examples are concrete. Josh Allen rushing attempts, Ja'Marr Chase target share, Nikola Jokic assist rate, Tarik Skubal strikeout projection, Igor Shesterkin starter confirmation, and Islam Makhachev control time are all different prediction problems. A single “player form” feature cannot explain them all, so the model needs sport-specific inputs and review notes.
- NFL: separate route participation, pressure rate, and red-zone role from box-score volume.
- NBA: separate usage, minute projection, pace, and back-to-back fatigue.
- MLB: separate starter skill, handedness, park, weather, and lineup confirmation.
- NHL and UFC: late confirmations and fight-week news can matter more than a season average.
Model inputs worth naming
Use names as evidence, not decoration. The useful SEO win is that Josh Allen, Ja'Marr Chase, Bijan Robinson and Puka Nacua and Chiefs, Bills, Eagles and Lions appear inside decisions, thresholds, and internal links instead of being dumped into a keyword list.
- NFL model: route participation for Ja'Marr Chase, rushing attempts for Josh Allen, pressure rate allowed by the Bengals, and red-zone carry share for Jonathan Taylor should be separate features.
- NBA model: usage, projected minutes, rest, and pace should move Nikola Jokic or Shai Gilgeous-Alexander props differently than a one-number power rating.
- MLB model: Tarik Skubal strikeout projection, Coors Field park factor, lineup confirmation, and bullpen rest need their own columns.
- Review loop: grade entry price, closing price, bet result, and model error separately so lucky results do not hide bad forecasts.
Build or audit the workflow in Tinker and review it with CLV.
Research note board
Use this model-audit board to keep features, validation, and bet sizing from collapsing into one confidence score.
| Model layer | What to inspect | Example input | Downgrade when |
|---|---|---|---|
| Feature | Whether the variable maps to the sport and market | Josh Allen role data or PPR price movement | The feature is a proxy for something you can measure directly |
| Validation | Out-of-sample error, CLV, calibration, missing data | Chiefs market movement after injury news | Wins come without beating the close or improving calibration |
| Sizing | Bankroll, confidence interval, correlation, market limit | CLV exposure compared with related tickets | Multiple bets repeat the same thesis at full stake |
Model calibration: predicted vs observed
Predicted win probability bucket vs the empirical win rate inside that bucket on the test set. Points on the y=x reference line are perfectly calibrated; points below mean the model is overconfident in that bucket.
EV per $100 across win rate × odds grid
Expected value of a $100 stake at each combination of true win rate and market odds. Anywhere the cell is positive you have a long-run profitable bet; the magnitude shows how aggressive Kelly will size it.



