Skip to content
Back to guides
Model checks

Stacking Two Sports Models Without Writing Python

Shark Snip Editorial 11 min read

Read the price, role, and market first

A click-driven tutorial for stacking two sports betting models in the browser. Meta-learner, calibration, Brier comparison, and ensemble weights.
15 sections
Stacking Two Sports Models Without Writing Python cover art

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:

  1. 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).
  2. 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:

  1. Open the Stack builder. Drag both base models onto the canvas.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

  1. 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.
  2. 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:

  1. Version each component. If you retrain a base model, give it a new version ID. The stack should reference exact versions of each base.
  2. 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.
  3. Roll back atomically. If the new stack underperforms in live betting over 30 games, roll back all components together, not just the meta-learner.
  4. 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.
  5. 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 layerWhat to inspectExample inputDowngrade when
FeatureWhether the variable maps to the sport and marketJosh Allen role data or PPR price movementThe feature is a proxy for something you can measure directly
ValidationOut-of-sample error, CLV, calibration, missing dataChiefs market movement after injury newsWins come without beating the close or improving calibration
SizingBankroll, confidence interval, correlation, market limitCLV exposure compared with related ticketsMultiple 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.

Frequently asked questions

What is the difference between blending and stacking?
Blending averages the outputs of two models (often with fixed weights — 50/50, 60/40, or weighted by recent Brier score). Stacking trains a second-level meta-learner that takes the first-level model outputs as features and learns the optimal combination, often using logistic regression with regularization. Stacking generally beats blending when the two base models have different bias structures (e.g., XGBoost on engineered features and a TF.js net on raw features), but it requires a proper out-of-fold validation scheme to avoid leakage.
Why doesn't averaging two betting models always improve Brier?
Two strongly correlated models give you almost no diversification, so the ensemble Brier looks identical to the average of the two individual scores. Worse, if one model is well-calibrated and the other is overconfident, a naive average can decalibrate the overall predictions. The fix is to calibrate each model independently (Platt scaling or isotonic regression on a holdout fold) before stacking. The meta-learner can then learn to trust whichever base model is sharper at each probability range.
How do I know if my stacked model is overfit to the meta-training fold?
Hold out a third fold the meta-learner never sees — the "stack test" fold. Train base models on fold A, generate predictions on fold B, train the meta-learner on those B-predictions vs true labels, then score on fold C. If Brier on C is meaningfully worse than on B (more than 0.005 absolute), the meta-learner memorized fold B noise. Shrink the meta-learner (L2 = 1.0 or higher), reduce the number of base models, or use a simple fixed weighted average instead.
Should the meta-learner be linear or another XGBoost?
For two or three base models on a single sport, a regularized logistic regression as the meta-learner is almost always the right call. The interpretation is clean (weights sum to one, you can read which model the stack trusts), the variance is low, and the marginal gain from a non-linear meta-learner is rarely worth the additional overfitting risk. Stack non-linear only when you have four+ base models with non-trivial correlation structure and a 5,000+ game training set, which is rare for most bettor-built stacks.

Build a free model in 60 seconds →

Go →
11m read time
28 players/teams
8 key angles
Angles in this read 6 angles

Editorial example layer

Concrete examples make the page useful: tie player and team names to role, price, matchup, and timing so the content reads like analysis instead of glossary filler.
Patrick MahomesJosh AllenLamar JacksonJoe BurrowJalen HurtsJustin HerbertC.J. StroudTua TagovailoaChiefsBillsRavensEaglesLionsBengalsclosing line valuetarget shareair yardsred-zone roleroute participation
Stacking Two Sports Models Without Writing Python data infographic
Chart view of the article's core numbers. Source: inline-lib-modelVsMarketCalibration-stacking-models-in-workshop.

Get picks in your inbox

One email, every slate — ranked edges, no touts. Unsubscribe any time.

Start free — pick a sport

Go →

We use cookies for essential site functionality. With your consent, we also use cookies for analytics and performance monitoring. See our Privacy Policy.