Ensembler (Blending Math)¶
ensembler
¶
Consensus across base models — calculated + learned — pure logic, all in pandas.
The outer loop combines every base model's forecast per ts_id into a
consensus. Two families, both pure here (no GCP calls):
- Calculated (
mean,median,inverse_error,champion,horizon_specialist) — heuristics and per-series selectors that need no cross-series meta-learner training.combine_calculatedblends the baseforecast_predictionsin pandas and returnsensemble_<s>prediction rows the caller appends via the Storage Write API — the same append path the learned strategies use (every append-only cell-table write goes through the Write API, noINSERT…SELECTDML). - Learned (
nnls,ridge,xgb) — meta-learners that train on the backtest OOF to learn per-model trust weights.fit_learnedfits them; because it only ever seesbacktest_oof(never in-sample fits) leakage is structurally impossible, and it refuses to run when backtesting is off.
A run may request several strategies at once (ensemble.strategies is a list); each yields
its own weights/rows so calculated and learned consensuses sit side-by-side in one run.
Public surface: combine_calculated, combine_oof, fit_learned,
calibrate_ensemble_predictions, ensemble_oof_bounds (plus the pure combine helpers
mean_combine, median_combine, inverse_error_weights).
calibrate_ensemble_predictions
¶
Calibrate ensemble future prediction intervals from ens_oof & apply output constraints.
When cfg.output.calibrate_intervals is True and ens_oof has >= 5 residual rows for a
(ts_id, model_type) cell, replaces the linearly blended bounds with empirical 10th/90th
residual quantiles from calibration.calibrate_from_oof. Always enforces physical output
constraints (calibration.apply_output_constraints) when configured.
Source code in src/scale_forecasting/ensembler.py
1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 | |
combine_calculated
¶
Blend base predictions into ensemble_<s> rows for every calculated strategy (pure).
Builds the ensemble_<s> rows in pandas rather than via SQL INSERT…SELECT: reads the base
forecast_predictions (long-format ts_id, model_type, forecast_date, yhat, yhat_lower,
yhat_upper) and returns prediction-row dicts the caller stamps with run_id/ensemble_id
and appends via the Storage Write API. Supports mean, median, inverse_error,
champion (per-series single winner), and horizon_specialist (per-series,
per-horizon-step inverse-error blend when oof_df is supplied, falling back to series-level
inverse_error otherwise), as well as top_k and parsimony_margin pruning.
Source code in src/scale_forecasting/ensembler.py
875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 | |
combine_oof
¶
Blend base-model OOF forecasts into ensemble OOF, per requested strategy (pure).
The scoring counterpart to the future-prediction consensus: because the base
forecast_predictions are a true beyond-data forecast (no actuals to join), each ensemble is
scored on the backtest OOF window — exactly the window the base models are scored on. This
applies the same consensus rules in OOF space (where y_true lives) and returns long-format
(ts_id, model_type='ensemble_<s>', fold_id, cutoff_date, forecast_date, y_true, yhat) for
the caller to score with metrics.compute_metrics. The cutoff is carried out as well as joined
on, because the caller cuts each series' history there to get the fold's MASE denominator.
Blends over whichever base models are present per (ts_id, cutoff_date, forecast_date) key
(see _fold_key for why the cutoff and not the fold ordinal):
mean/median are unweighted; inverse_error weights each model per ts_id by
1/WAPE over that model's OOF (self-contained — the same signal forecast_metadata would
carry); learned strategies apply learned_weights[strategy] (skipped if absent). Returns an
empty frame when the OOF is empty or no strategy produces a blend.
Bounds are not blended, so an ensemble's coverage/pinball/interval_score are
NaN. The base models' are not, any more — the OOF frame now carries yhat_lower and
yhat_upper per fold, and the base cells score them. Blending them is a separate question
with a real answer to choose (averaging two models' 80% intervals does not give an 80%
interval), so the columns stay NaN rather than being filled with something plausible and
wrong. An ensemble is compared to its base models on the point metrics.
Source code in src/scale_forecasting/ensembler.py
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 | |
ensemble_oof_bounds
¶
Empirical 80% prediction intervals for one fold of an ensemble's OOF rows (pure).
Uses leave-one-fold-out OOF residuals (y_true - yhat from all other folds in
ens_oof_group, falling back to all folds when only a single fold exists) so every
ensemble_* pseudo-model earns non-NaN interval metrics (coverage_80, pinball_loss,
winkler_score, interval_width, wis) on the same footing as base models. Applies
calibration.apply_output_constraints when configured.
Source code in src/scale_forecasting/ensembler.py
filter_variants_for_ensemble
¶
Select the ensemble candidate pool across <base_model>@<tag> variants (pure).
Controlled by cfg.ensemble.variant_policy:
- "all": keep all configured model variants.
- "primary_only": keep at most one variant per base_model_name(m) — preferring the
untagged base model if present, else the first variant of that base model in config order.
- "best_per_base_model" (default): when multiple variants of the same base_model exist,
keep the variant with the lowest inner-fold OOF loss (or mean decision_metric loss in
metric_df when oof_df is absent), preventing collinear multi-variant dilution.
Source code in src/scale_forecasting/ensembler.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
fit_learned
¶
Fit every learned strategy in cfg on the backtest OOF's inner folds.
Returns (weights, artifacts, scoring_basis):
* weights[strategy] maps base-model name → learned weight,
* artifacts[strategy] is the pickled, uploadable meta-learner payload,
* scoring_basis is "holdout" when the newest fold was successfully reserved from the
fit and "in_sample" when it could not be — the caller writes it to the registry beside
the metrics it qualifies.
The newest fold is not in X. A meta-learner that trains and is scored on the same folds
reports a fit statistic, and it lands in the same leaderboard column as the base models'
genuinely out-of-fold numbers, where nothing distinguishes the two. Holding the newest fold out
of the fit is what makes ensemble_nnls comparable to theta — see inner_fold_mask.
Refuses to run when backtesting is off — a learned ensemble has no legitimate,
leakage-free data to train on (the guard that keeps stacking honest). Base models come
from cfg.models in order; native vs python doesn't matter — only their OOF forecasts do.
Source code in src/scale_forecasting/ensembler.py
inner_fold_mask
¶
Which OOF rows a fit is allowed to see, and what that says about the resulting numbers.
Returns (mask, basis). mask is True on the rows belonging to a fold other than
backtest.holdout_fold_id — the folds a stacker, an inverse_error weight or a
hyperparameter search may learn from. basis is "holdout" when a real split exists and
"in_sample" when it does not, in which case the mask is all-True and the caller fits on
everything it will later be scored on.
Two ways the split can fail to exist, and both are ordinary rather than exceptional: the run
asked for n_folds: 1, or a short series in a ragged panel achieved only the newest fold.
Falling back is the right behaviour — a stacker with no rows is worse than an optimistic one --
but it has to be said, because an in-sample number is indistinguishable from an honest one
once it is sitting in a metric column. That is what basis is written to the registry for.
Source code in src/scale_forecasting/ensembler.py
inverse_error_weights
¶
Weights ∝ 1/error, normalized to sum to 1.
Lower-error models get more weight. A zero-error model would divide by zero, so zeros are treated as "perfectly trusted" and share the weight equally among themselves. Falls back to uniform weights when no error is finite and positive.
errors must be losses — non-negative, zero is perfect — not raw metric values. That is
what metrics.loss_of produces, and every caller here routes through it. Handing this function
a metric where larger is better inverts the weighting silently, which is exactly what used to
happen with decision_metric="coverage".
Source code in src/scale_forecasting/ensembler.py
mean_combine
¶
median_combine
¶
Row-wise median across models — robust to a single wild base forecast.