fix: use validation data and support N-forecaster ensembles in WeightsCombiner - #1062
Draft
majidkhoshrou wants to merge 1 commit into
Draft
fix: use validation data and support N-forecaster ensembles in WeightsCombiner#1062majidkhoshrou wants to merge 1 commit into
majidkhoshrou wants to merge 1 commit into
Conversation
…sCombiner WeightsCombiner previously ignored the data_val argument passed to fit(), always training its per-quantile 'who wins' classifier on in-sample training predictions. This structurally biased weights towards whichever base forecaster overfits the training window hardest. fit() now trains on data_val when supplied, falling back to data otherwise. The degenerate-label guard in _validate_labels() only handled the case of exactly one forecaster ever winning, which is the only degenerate case possible with 2 base forecasters. With 3+ forecasters it's common for a proper subset to never win, which crashed predict_proba with a column count mismatch. The guard now triggers whenever fewer forecasters win than are registered, enabling ensembles of arbitrary size. The DummyClassifier fallback used for degenerate quantiles hardcoded weight 1.0 on the alphabetically-first registered forecaster name instead of the classifier's actual (most-frequent) prediction, silently mis-attributing weight whenever that forecaster wasn't the real winner. The fallback now uses the recorded majority winner. StackingCombiner.fit() also accepted data_val but always discarded it when fitting its per-quantile meta-forecaster; it now passes validation data through so meta-forecasters that support early stopping can use it. Assisted-by: GitHub Copilot (Claude Sonnet 4.5) Signed-off-by: majidkhoshrou <majid.khoshrou@gmail.com>
majidkhoshrou
requested review from
egordm and
lschilders
and
a lite review from Copilot
August 15, 2026 14:00
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes internal fitting/prediction issues in the ensemble forecast combiners so they correctly leverage held-out validation predictions and behave robustly with ensembles containing more than two base forecasters.
Changes:
- Update
WeightsCombiner.fit()to train the “best-forecaster” classifier ondata_valwhen provided, rather than always using in-sample training predictions. - Generalize
WeightsCombiner’s degenerate-label handling for N-forecaster ensembles and ensure the dummy fallback uses the true majority winner. - Fix
StackingCombiner.fit()to pass validation data through to the meta-forecaster (for early stopping / evaluation).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/openstef-meta/tests/unit/models/forecast_combiners/test_learned_weights_combiner.py | Adds regression tests for N-forecaster edge cases, dummy fallback behavior, and data_val training. |
| packages/openstef-meta/src/openstef_meta/models/forecast_combiners/stacking_combiner.py | Passes prepared validation data to meta-forecaster during stacking fit. |
| packages/openstef-meta/src/openstef_meta/models/forecast_combiners/learned_weights_combiner.py | Trains learned weights on validation predictions when available and improves N-forecaster robustness. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+93
to
+102
| # Pass validation predictions through so meta-forecasters that support early | ||
| # stopping (e.g. LGBM/XGBoost) can use them, instead of always discarding data_val. | ||
| input_data_val = None | ||
| if data_val is not None: | ||
| input_data_val = self._prepare_input(data_val, q) | ||
| input_data_val = input_data_val.pipe_pandas( | ||
| partial(pd.DataFrame.dropna, subset=[input_data_val.target_column]) | ||
| ) | ||
|
|
||
| self._models[q].fit(data=input_data, data_val=input_data_val) |
Comment on lines
291
to
+296
| if isinstance(model, DummyClassifier): | ||
| # DummyClassifier has no predict_proba — construct one-hot weights manually | ||
| weights_array = pd.DataFrame(0, index=base_predictions.index, columns=self._label_encoder.classes_) | ||
| weights_array[self._label_encoder.classes_[0]] = 1.0 | ||
| # DummyClassifier has no predict_proba — use the recorded majority winner rather than | ||
| # assuming the alphabetically-first registered forecaster name is the actual winner. | ||
| fallback_label = self._dummy_fallback_label.get(quantile, self._label_encoder.classes_[0]) | ||
| weights_array = pd.DataFrame(0.0, index=base_predictions.index, columns=self._label_encoder.classes_) | ||
| weights_array[fallback_label] = 1.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes three bugs in
WeightsCombiner(learned-weights ensemble combiner) and one inStackingCombinerthat limit the ensemble machinery to small (2-model), always-trainableensembles, even though
EnsembleForecastingModel.forecastersis already a genericdict[str, Forecaster]intended to support any number of base forecasters:WeightsCombiner.fit()ignoreddata_val. The method accepted adata_valparameter but never referenced it, so the per-quantile "who wins" classifier was always
trained on in-sample training predictions. In-sample predictions from boosted/tree
forecasters are systematically over-optimistic, biasing the combiner toward whichever
forecaster overfits the training window hardest.
fit()now trains ondata_valwhensupplied, falling back to
dataotherwise.The degenerate-label guard didn't generalize past 2 forecasters.
_validate_labels()only fell back to aDummyClassifierwhen exactly one forecasterever won (the only degenerate case possible with 2 base models). With 3+ forecasters
it's common for a subset to never win, which crashed
predict_probawith aValueError(column count mismatch) instead of falling back gracefully. The guard nowtriggers whenever fewer forecasters win than are registered, so ensembles of arbitrary
size no longer crash.
The
DummyClassifierfallback silently discarded its own answer._predict_weights()hardcoded weight 1.0 onself._label_encoder.classes_[0]— thealphabetically-first registered forecaster name — instead of asking the fitted
DummyClassifier(strategy="most_frequent")which forecaster actually wins most often.This is easy to miss with 2 models (right about half the time by chance) but becomes a
frequent, silent mis-attribution with 3+ models. The fallback now uses the recorded
majority winner.
StackingCombiner.fit()also discardeddata_val. It accepteddata_valbutalways passed
data_val=Noneto the underlying meta-forecaster'sfit(). It now passesvalidation data through so meta-forecasters that support early stopping (e.g. LGBM,
XGBoost) can use it.
Together these make the ensemble machinery robust for ensembles of arbitrary size (not
just the built-in 2-model
lgbm/gblinearpreset default) and make the learned-weightscombiner actually use held-out validation data as its name/docstring already implied.
No public API changes — all fixes are internal to
fit()/_validate_labels()/_predict_weights()behavior.Closes #
Type of change
Breaking changes checklist
N/A — internal fitting behavior only; no public API, config schema, or serialized object
changes.
AI disclosure
Assisted-by: GitHub Copilot (Claude Sonnet 4.5))Checklist
poe all --checkpasses locallygit commit -s)