What each part of the site does, how our models work, and what the terms mean - then the statistics underneath, technique by technique, for anyone who wants the theory. Backtest numbers and implementation detail live in the README.
One page for the day: Beat the Streak picks (on days there are any), today's baseball game picks and this week's football picks, each next to how our picks have done.
Beat the Streak: the hitters most likely to get a hit today, for MLB's contest of picking a hitter a day and building a streak - with our own running streak. Hit chances: every hitter's chance of a hit today. Game picks: who we think wins each game, next to Vegas, with our record and any bet we'd suggest.
What feeds the picks: every hitter's WAVE, hit chance, CAVE and WOPS (as played, or adjusted for the pitchers he faced), active hit streaks, a player lookup, today's probable starters, team ratings and each team's schedule.
How a player's season compares with history at his age: the 25 most similar seasons by players the same age, and what those players did the next year - his projection.
Several published prospect and draft rankings blended into one consensus list.
Which teams get the most winning for their money, every season since 1988 (opening-day payrolls from The Baseball Cube): payroll effectiveness, expected wins from payroll (what the 10 seasons that spent most like it won, plus the front office's recent edge), wins against that, all-time franchise averages and year-by-year grids, with the whole thing downloadable as a workbook.
Game picks for the week next to Vegas, outlined by whether the pick came before the final injury report (Tuesday) or after it (the morning of, or the day before, the game). What if?: swap a starting QB, mark players out or set any unit's efficiency and see the chance move; last week's games can be replayed to test your settings against how they finished.
Every team's schedule, power rankings by net EPA per play, team profiles, the team units (QB+, Receiving+, Rushing+, Pass D+, Run D+) and each game's matchup differentials, and players' season views.
Players with the friendliest matchups for receptions, yards and sacks this week, from their recent games and what their opponent allows, plus each position's every-game floors. Places to look, not checked against sportsbook lines.
Published college big boards for the next NFL draft, blended into one ranking; players already drafted are left out.
The button in the header installs the site as an app with its own icon: one tap in Chrome, Edge or on Android; on iPhone and iPad, Share → Add to Home Screen. The latest pages and data you've opened stay readable offline.
The MLB Payroll page's numbers for NFL teams since 2014, on cash paid or cap spending (summed from OverTheCap's player contracts): which teams win the most for their money, expected wins from spending, franchise averages and year-by-year grids, with a workbook download.
Each hitter's hit rates are blended over the season and his last 81, 30 and 10 days (recent games count more), pulled toward the league for small samples, and combined with today's matchup - the opposing starter's and bullpen's PAVE and the park - in a model trained on past seasons and calibrated so its percentages mean what they say. A hitter qualifies only if both his hit chance and his games-with-a-hit rate clear 70%; the best Approach is the pick. Whether to risk the streak today is an optimal-stopping decision (Bellman, below).
Each team's Pythagorean strength (from runs scored and allowed), its schedule-adjusted rating and its hitting power, adjusted by the starter and bullpen it faces today and home field; then calibrated. Every pick is logged before the game and graded after, next to Vegas's closing line. A bet is suggested only when our chance beats the de-vigged market by enough, sized by fractional Kelly in units.
A logistic regression on each team's Pythagorean strength, schedule-adjusted confidence, offensive and defensive EPA edges, turnover margin and points per drive, plus each starting QB's QB+. Trained on every game since 2016 with each week predicted only from earlier games, the feature set chosen by walk-forward validation and checked on a season it never saw. When we disagree sharply with Vegas we defer to it (it's proven right more often there), then calibrate.
A team's run game, pass game, run defense and pass defense are rated in percent of their normal yards per play. A player out lowers his units by what an absence at his position has cost teams in results (or what Vegas charges, where results are too noisy), times his snap share; a QB change goes through the model with the new QB's rating. Yards become points (0.07 a yard) and points become win chance (0.137 log-odds a point, from how Vegas turns spreads into odds). The final injury report's Out and Doubtful players reprice each game the morning of, or the day before, it.
Each unit's EPA per play, solved for every offense and defense at once so each is credited for the opponents it faced, with recent games weighted more and reaching back into last season. Shown on a 100 = league-average scale, 30 points per standard deviation.
Props: a player's production blended over his season, last 8 and last 4 games, against what his opponent allows his position. Age curves: nearest neighbors among historical seasons at the same age. Prospects: each source's ranks blended into one list. The Stats pages (WAVE, CAVE, WOPS, QB+ and the rest) describe; only the Picks pages predict.
What it is
A way to estimate a rate (like a hit rate) from a small sample without over-trusting it. Instead of using the raw observed rate, it's pulled toward a "prior" rate (usually the population/league average), weighted by how many real observations back it up - a batter with 3 at-bats gets pulled hard toward league average; one with 300 barely moves. The prior itself is estimated from the data ("empirical"), not asserted from theory.
How we use it
helpers.shrink_rate shrinks every small-sample hitter hit
rate toward the league rate, weighted by config.WAVE_SHRINKAGE_STRENGTH
real pseudo-observations. Used in hitters.compute_wave and
compute_game_hit_probability so a hitter with 5 at-bats
against lefties doesn't get an absurd extreme rate just from noise.
What it is
A confidence interval for a proportion (a rate out of n trials) that stays well-behaved at small n and near 0%/100% - unlike the simpler normal-approximation interval, which can produce nonsense bounds outside [0, 1] on small samples.
How we use it
helpers.wilson_ci wraps statsmodels's Wilson
implementation. Computed on the raw (unshrunk) hit/at-bat counts in
hitters.compute_wave/compute_game_hit_probability -
deliberately never on the shrunk point estimate above, since shrinkage
already borrows strength from the prior and isn't a plain sample
proportion anymore. The same formula also backs
evaluation.wilson_confidence_interval (a scalar wrapper for
one aggregate backtest rate rather than a per-row Series) - applied to
day_survival_rate, market_accuracy,
beat_closing_line_rate, and
win_rate_on_advised_bets, so a bare percentage on the
dashboard is never read as more certain than its real sample size
supports. Quant-analytics item #5, "backtest scope and statistical
significance."
What it is
Blending several time windows of different lengths (e.g. last 7 days, last 30 days, full season) into one number using fixed weights, so a signal reflects both recent form and a larger, more stable sample. A simplified relative of exponential smoothing, using a handful of discrete windows with hand-set weights rather than one continuously-decaying weight.
How we use it
hitters._blend_windows/_side_window_agg is
the shared engine behind WAVE, WTB/WHOPS, pitch-family rates,
plate-discipline rates, and batted-ball-quality signals - each computes a
rate per config.WAVE_WINDOWS window, split by opposing
throwing hand, then blends by fixed weight.
The Stats page shows three such views of a hitter's season: WAVE (hits per at-bat), CAVE - Carried Average - (the share of games with at least one hit) and WOPS (OPS: on-base per plate appearance plus total bases per at-bat), each blending the full season with the last 81, 30 and 10 days. A window a hitter has no at-bats in, because he was hurt or benched, carries his next-longer window rather than counting as zero. Their vs. pitchers faced versions compare what he did with what a league-average hitter would have done against the same pitchers (each pitcher's season hits, on-base and total bases allowed, shrunk toward the league), so hits off tough pitching count for more. WAVE counts at-bats only - walks and hit-by-pitches aren't at-bats - here and in the picks alike: replaying the 2026 season, the picks hit more often that way than counting every plate appearance.
Learn more →What it is
Rescaling a value to "how many standard deviations from the mean" it
is: (x - mean) / std. Puts variables of very different raw
scales onto one comparable axis.
How we use it
Two real uses: teams.py's normalize() helper
puts team-strength composites (Pythagorean strength, SOS, confidence) on
a common ~1.0-centered scale before they're blended into one game-pick
signal; separately, scripts/train_hitter_hit_model.py/
train_game_pick_model.py z-score features before fitting
statsmodels.Logit purely so the resulting coefficients are
comparable across features of very different natural scales.
Every model here is grid-searched against at least one real alternative on identical, no-lookahead cross-validation, and only shipped if it also beats a naive baseline and the existing heuristic on an untouched holdout - see Model Validation below.
What it is
A linear model for binary outcomes: it fits a weighted sum of features and squashes it through the sigmoid function into a 0-1 probability. The workhorse baseline classifier in applied statistics - simple, fast, and its coefficients are directly interpretable.
How we use it
A candidate model (vs. gradient boosting) for "does this hitter get a
hit today" (scripts/train_hitter_hit_model.py) and for
game-level home-win probability (scripts/train_game_pick_model.py).
What it is
Ordinary linear regression with an L2 penalty added on the coefficients, shrinking them toward zero. Trades a little bias for lower variance - useful when features are correlated or numerous relative to the sample size, both common here.
How we use it
A candidate model (vs. gradient boosting) for the Age Curves page's
next-season HR9 projection
(scripts/train_age_curve_hr9_model.py).
What it is
An ensemble method that builds many shallow decision trees in sequence, each one fitting the residual errors of the ones before it. Can capture nonlinear relationships and feature interactions that a linear model can't, at the cost of interpretability. "Histogram-based" bins continuous features first, trading a little precision for large speed gains on bigger datasets.
How we use it
sklearn.ensemble.HistGradientBoostingClassifier/
Regressor is the alternative candidate in every model
selection above - it's the one that actually won and shipped for HR9
projection (beating the KNN baseline, see below), while logistic
regression/Ridge have won the others so far.
What it is
A non-parametric method that predicts by finding the k most similar historical examples (by distance on some metric) and looking at what actually happened to them, rather than fitting any equation. Simple, and naturally gives a real empirical distribution of outcomes, not just one point estimate.
How we use it
age_curve.find_comparables is a hand-implemented 1-D KNN:
for a given player-age-metric, it finds the k closest historical Lahman
seasons by that one metric and reports what those comparable players
actually did the next season (mean, 25th/75th percentile) -
project_next_season. Deliberately hand-rolled rather than
pulling in sklearn.neighbors for one simple 1-D lookup.
What it is
Cross-validation adapted for time-ordered data: instead of randomly shuffling rows into folds (which would let a model "see the future"), each fold trains only on an earlier block of time and tests on the block immediately after it, then the training window rolls forward. The only CV scheme that doesn't leak future information into training for sequential data.
How we use it
ml_models.WalkForwardDateSplit blocks by calendar date
(used for every daily hitter/game model); age_curve_ml.YearBlockedSplit
does the identical thing blocked by season instead, for year-level
Lahman data. Both plug directly into sklearn.model_selection.GridSearchCV
as a custom cv= splitter.
What it is
Setting aside a final chunk of data that no amount of model selection or hyperparameter tuning is ever allowed to touch, then evaluating the already-chosen model on it exactly once. The single honest estimate of real-world performance - anything tuned using the holdout itself isn't a real holdout anymore.
How we use it
Every scripts/train_*.py carves off the LAST N distinct
dates (config.ML_FINAL_HOLDOUT_DATES and its per-model
variants) before any grid search runs, and reports final numbers only
from that untouched block - not from cross-validation folds reused for
reporting.
What it is
A named failure mode, not a technique in itself: a model that accidentally has access to information it wouldn't have in the real world at prediction time (most often, something from the future). Leakage inflates validation numbers in a way that quietly disappears the moment the model is used for real - the single most common way a backtest lies.
How we use it
The whole pipeline is built around an explicit "as of date" cutoff
(pipeline.run()'s own module docstring) - every feature is
computed strictly before the date being predicted, walk-forward CV never
trains on a later date than it tests, and the holdout dates are excluded
from every stage of model selection, not just final scoring.
A model can rank outcomes well (good AUC) while still being over- or under-confident in its actual probability numbers. Calibration fixes the numbers without touching the ranking.
What it is
A non-parametric calibration method: it fits the best possible non-decreasing (monotonic) step function mapping raw model scores to corrected probabilities. Flexible - it can fix almost any shape of miscalibration - but needs more data than a parametric method to avoid overfitting the correction itself.
How we use it
One of two candidate calibration methods in
ml_models.fit_calibrated
(sklearn.calibration.CalibratedClassifierCV(method="isotonic")),
refit through the same walk-forward splitter as the underlying model.
Selected in scripts/train_hitter_hit_model.py:_select_calibration
by comparing holdout Brier score against the uncalibrated and sigmoid
options.
What it is
A parametric calibration method: fits a single logistic (sigmoid) curve mapping raw model scores to corrected probabilities. Needs less data than isotonic regression to calibrate reliably, but can only correct a sigmoid-shaped miscalibration pattern.
How we use it
The other candidate in ml_models.fit_calibrated
(method="sigmoid"), same selection process as isotonic
above.
What it is
After fitting a model like logistic regression, each coefficient comes with a standard error; dividing the coefficient by its standard error gives a z-statistic, which converts to a p-value - the probability of seeing a coefficient this large by chance if the true effect were zero. This is what separates "this feature looks predictive in this sample" from "this feature is actually predictive" - a different question from the Regression & Classification Models section above, which is about FITTING a model, not testing whether its coefficients are real.
How we use it
statsmodels.api.Logit, fit once per feature alone and
once with all features together, in
scripts/train_hitter_hit_model.py/train_game_pick_model.py's
significance_report. This is real inference (fit on full
history, not the holdout) - it answers "which features matter and in
which direction," a different question from the walk-forward accuracy
numbers above.
What it is
Given n real coin-flip-style trials with a known number of successes, this asks: how likely is a rate this extreme (or more extreme) if the true underlying rate were actually the null value (e.g. 0.5, "no real skill difference")? Unlike a normal-approximation test, it computes the exact binomial probability directly rather than approximating - reliable even at the small n a fresh real-money backtest actually starts with (a handful of games, not thousands).
How we use it
scipy.stats.binomtest, wrapped as
evaluation.binomial_significance. Applied to
beat_closing_line_rate - a genuinely symmetric "whose squared
error was lower on this game" comparison, where 0.5 really is the "no
skill difference" null. Deliberately NOT applied to a plain accuracy rate
(real MLB home teams win somewhat more than half their games, so 0.5
isn't a fair null there) or to a betting win rate (bets are placed at
different real prices, so win/loss counts alone can't measure edge - see
the t-test below) - see game_evaluation.py's own docstrings
for exactly which rate gets which test and why. Quant-analytics item #5,
"backtest scope and statistical significance."
What it is
Tests whether the real mean of a sample of numbers is distinguishable from some null value, given the sample's own size and spread. Unlike a win/loss count, it uses the actual magnitude of each observation - the right tool when "how often" isn't the real question, "how much" is.
How we use it
scipy.stats.ttest_1samp, wrapped as
evaluation.mean_significance, tests each advised bet's real
bet_profit_units against a null of 0 (breaking even) -
reported as roi_p_value. This is the honest answer to "did
the advised bets actually make money, or is this within noise": a bet's
win/loss outcome alone doesn't capture the real price it was placed at (a
-150 favorite winning 55% of the time and a +150 underdog winning 55% of
the time are very different real outcomes), but its real profit already
prices that in. Quant-analytics item #5.
What it is
Mean squared error between a predicted probability and the actual binary outcome (0 or 1). Lower is better; 0 is perfect; a model that always predicts 0.5 scores 0.25 on a 50/50 outcome. Rewards both good ranking AND good calibration at once.
How we use it
evaluation.brier_score/ml_models.evaluate_classifier_predictions -
the primary metric used to pick between calibration methods, and the
basis for game_evaluation._beat_closing_line_rate's
per-game model-vs-market comparison.
What it is
Negative log-likelihood of the actual outcome under the predicted probability. Like Brier score, it rewards calibrated probabilities - but it punishes a confident, wrong prediction (e.g. predicting 1% when the outcome happens) far more severely than Brier does, since the loss blows up as predicted probability approaches the wrong extreme.
How we use it
evaluation.log_loss/ml_models.evaluate_classifier_predictions -
the primary metric used to select model FAMILY (logistic regression vs.
gradient boosting) before calibration is even considered.
What it is
The probability that a classifier ranks a random positive example above a random negative one. Measures pure ranking/discrimination ability - unlike Brier/log loss, it's completely insensitive to whether the predicted probabilities themselves are well-calibrated.
How we use it
ml_models.evaluate_classifier_predictions reports it
alongside log loss for every classifier, as a check on whether a model
that scores worse on log loss might still be separating outcomes better
(or vice versa).
What it is
A model-agnostic way to measure how much each feature actually matters: shuffle one feature's values (breaking its real relationship to the outcome) and see how much the model's performance drops. A bigger drop means the model was relying on that feature more.
How we use it
scripts/train_hitter_hit_model.py:feature_importance_report
via sklearn.inspection.permutation_importance, computed only
on the untouched holdout - chosen deliberately over SHAP to avoid adding
a new dependency for one report.
What it is
A diagnostic that bins predictions by predicted probability and compares each bin's average predicted probability to that bin's actual outcome rate. A perfectly calibrated model's points fall on the diagonal - systematic over- or under-confidence shows up as a visible bend away from it.
How we use it
evaluation.calibration_table - the same underlying idea
that motivates fitting isotonic/Platt calibration in the first place
(above), used here as a plain diagnostic table rather than a correction.
What it is
A number from -1 to 1 measuring the strength and direction of a LINEAR relationship between two variables. 0 means no linear relationship (though a real nonlinear one could still exist).
How we use it
ml_models.evaluate_predictions reports it between
predicted and actual values for every regression target (DK points,
HR9), as a simple, standard companion to mean absolute error.
What it is
Estimating the sampling distribution of a statistic by repeatedly drawing new samples, WITH replacement, from the real observed data itself, rather than assuming a theoretical distribution. Especially useful when the real underlying data-generating process is unknown or hard to model directly.
How we use it
scripts/backtest_streak_decision.py resamples real
historical (probability, outcome) day-pairs 1,000 times to compare the
decision-theory play/sit rule against the status-quo rule under many
plausible real day-orderings, reporting the real win rate and mean/median
final streak across resamples - a genuine nonparametric bootstrap, no
distribution assumed on the underlying probabilities.
What it is
A way to solve sequential decision problems by working BACKWARD from the end: define the value of being in each state on the final day, then recursively define the value of each state one day earlier as the best of "stop now" vs. "the expected value of continuing," given tomorrow's already-solved values. This is Bellman's optimality principle - it collapses an exponentially large decision tree into one manageable pass through states and days.
How we use it
decision_theory.solve_reservation_thresholds models "risk
today's Beat the Streak pick, or protect the streak I already have" as a
finite-horizon optimal-stopping problem: state = current streak length,
recursion runs backward from the end of the remaining days. Because the
next day's values are already known, the optimal action collapses to a
closed-form probability threshold - should_play just
compares today's real predicted probability against it.
What it is
The general finance idea that combining imperfectly-correlated bets reduces the risk of them all failing together, compared to betting the same underlying risk twice. Formally this requires estimating a real correlation or covariance between the bets.
How we use it
Honestly labeled as a proxy, not the real thing:
predictions._diversify_second_pick never estimates an actual
correlation. It's a sign-only heuristic: if a day's #1 and #2 picks share
the same real game, and a comparably-ranked pick from a DIFFERENT game
exists nearby, prefer it - because shared-game correlation can only ever
raise the chance both picks miss together, never lower it, so the
direction of the swap is safe even without knowing the real magnitude.
Gated at 0.0 (a no-op) in production until backtest evidence justifies
turning it on.
What it is
Bill James' formula estimating a team's "true" win percentage from
its runs scored and allowed - RS^exp / (RS^exp + RA^exp) -
on the idea that runs scored/allowed are a more stable, less
luck-affected signal than actual win/loss record over a partial season.
How we use it
teams.compute_strength_metrics computes this per rolling
window (with a custom-tuned exponent, config.PYTHAGOREAN_EXPONENT,
not the classic 2), feeding the "Pythagorean strength"/"Pythagorean
confidence" signals in game_picks.py.
What it is
Also Bill James: an odds-ratio formula for combining two independent rates (a batter's rate against all pitchers, a pitcher's rate allowed to all batters) into the rate expected specifically in that matchup, given each side's rate relative to the league average.
How we use it
matchup._log5 combines a batter's WAVE hit rate with the
opposing pitcher's blended PAVE allowed-rate into a real matchup-specific
hit probability in compute_matchup_hit_probability.
What it is
A per-venue adjustment for how much a ballpark inflates or suppresses scoring relative to a league-average park, so a hitter's raw numbers aren't unfairly credited/blamed for the park they happen to play in.
How we use it
teams.compute_park_factors - each venue's average
combined runs per game, normalized to the across-all-venues mean (1.0 =
neutral), used to scale matchup rates in matchup._park_factor_multiplier.
What it is
A pitcher-evaluation stat built only from outcomes a pitcher directly controls (home runs, walks/HBP, strikeouts), deliberately excluding balls in play - since what happens to a ball in play depends heavily on the defense behind the pitcher, not just the pitcher. A cleaner signal of pitcher skill than ERA for that reason.
How we use it
age_curve.build_historical_pitcher_seasons computes real
FIP ((13*HR + 3*(BB+HBP) - 2*SO)/IP + config.FIP_CONSTANT) as
an age-curve feature/label, used in place of ERA for exactly this
reason.
What it is
MLB's own tracking-data-based metrics: "expected" stats
(xBA/xwOBA) model what a batted ball's outcome
SHOULD have been given its exit velocity/launch angle, independent of
whether a fielder happened to make a great play; "barrel" is a specific,
officially-defined combination of exit velocity and launch angle known to
produce great outcomes; wOBA weights every plate-appearance outcome by
its real run value rather than counting a single and a home run equally.
How we use it
hitters.compute_quality_of_contact consumes these as
Statcast's own precomputed per-pitch columns directly (not
independently re-derived) - averaged/rated over real batted-ball events
and recency-window-blended, same as every other windowed signal here.
What it is
A plate-discipline metric: per real pitch, was swinging or taking the better choice FOR THIS BATTER right now, given the pitch's real Statcast zone and their own recency-windowed OPS-equivalent production in that specific zone (shrunk toward their own overall OPS, never a league average - "is this a good zone for me," not "is this objectively a good pitch")? Decision_Score is the real % of pitches where a batter's actual swing/take choice matched that advice.
How we use it
decision_score.compute_decision_score builds each
batter's real windowed OBP/SLG/OPS overall and per zone
(compute_batter_overall_ops/compute_zone_ops,
shrunk via helpers.shrink_rate,
config.DECISION_SCORE_ZONE_SHRINKAGE_STRENGTH pseudo-PAs),
then compares each pitch's zone-OPS against that batter's own overall OPS
to classify the advised action. A real, no-lookahead backtest
(scripts/backtest_decision_score.py; train Mar-Jun 2025,
test Jul-Sep 2025, held out entirely from the reference it's graded
against) validated the core zone signal: PA-ending pitches where the
batter's real choice matched the advice scored a real 0.768 mean PA value
(on-base + total bases) vs. 0.624 for mismatched ones (n=86,389,
p<0.0001 pooled AND per-batter-paired). A real, honest negative
finding from that same backtest: hand-designed count-context/game-
situation threshold adjustments did NOT improve on the zone-only
baseline at any magnitude swept, so both ship at a neutral 1.0 (the
classifiers themselves - classify_count_context, the real
high-leverage flag - stay in the code, just not yet wired to move the
threshold). A separate real, no-lookahead significance test at the team
level (does a team's average Decision_Score, weighted by who actually
played, predict real game outcomes beyond the existing team model) did
NOT clear a real bar (p=0.16 controlling for pyth_Strength) - so
Decision_Score is a player-level-only metric here, not part of any team
rating or Automated Game Pick.
What it is
American moneyline odds convert to an "implied probability" per side, but a real two-sided book's implied probabilities always sum to MORE than 100% - the extra is the bookmaker's built-in margin (the "vig" or "overround"). De-vigging normalizes both sides back down so they sum to exactly 100%, recovering the book's actual best real estimate of the true probability.
How we use it
market_odds.moneyline_to_implied_probability (standard
American-odds conversion) and devig (proportional
normalization) turn real ESPN/DraftKings moneylines into a real
market_home_win_probability, logged alongside this
project's own model probability as an honest external benchmark - "did
we beat the closing line," not just our own heuristic.
What it is
Designing a write/append operation so running it twice with the same input produces the same result as running it once - usually by deduping on a real natural key rather than blindly appending. Makes a daily pipeline safe to re-run without corrupting its own history.
How we use it
Every append-only log in this project dedupes on a real composite key
before writing - predictions on (date, key_mlbam, metric)
(predictions.append_predictions), game picks on
(date, game_pk, metric) (game_predictions.append_game_predictions),
keeping the LAST (most-resolved) row on a collision so a re-run never
clobbers an already-known real outcome.