Skip to main content
Open llms.txtCopy tools

Weighted Ensembles

WeightedEnsembleForecaster combines CartoBoost forecasting models with explicit fixed weights. The ensemble is intentionally simple: each member is fitted, each member predicts the same horizon, and the final forecast is the weighted average of aligned predictions.

Interactive Example

Seasonal member forecast for ensemble design

Runs seasonal_naive against a bundled route-demand sample.

Ready to run in this page.

Use the embedded examples on the seasonal naive, theta, ETS, ARIMA, and Kalman pages to inspect candidate members before choosing fixed ensemble weights in Python.

When To Use

Use an ensemble when different models capture different parts of a demand pattern. For example, seasonal naive can preserve repeated hourly cycles, theta can adapt to a changing level, and Kalman can update a noisy level/trend from recent observations.

Do not use an ensemble just to add complexity. It should clear the best individual member on a fixed validation split or encode a clear operating policy such as "mostly seasonal, with a smaller trend correction."

Scientific Role

A weighted ensemble is a fixed mixture of scientific hypotheses. Each component should represent a different defensible mechanism, such as persistence, seasonal repetition, smooth trend, state-space updating, spatial borrowing, or shared supervised lag structure. The ensemble is useful only when those mechanisms make complementary errors under the same validation design.

Choose it when validation shows that no single component dominates all horizons or all panels, and when the selected weights can be explained. The weights are part of the model claim; they are not learned automatically by the ensemble.

Assumptions And Failure Modes

The ensemble assumes component predictions are aligned to the same series ids, timestamps, and horizons. It cannot create a signal that none of its members learned. If every component misses a rush-hour disruption, averaging will miss it too.

When every active member returns the same prediction-interval levels on the same forecast index, the native ensemble combines matching lower and upper quantiles with the normalized member weights. A partial or mismatched interval grid is an error, so uncertainty is not silently discarded. Native forecast details, when supplied by at least one member, include each member mean, weight, weighted contribution, and nested member detail for auditability.

Failure modes include keeping a weak member because one split moved by chance, changing component parameters while tuning weights, or reporting only ensemble metrics without component metrics. Compare against the best individual member and inspect horizon-specific errors before claiming that averaging adds scientific value.

Python Example

from cartoboost.forecasting import (
KalmanForecaster,
SeasonalNaiveForecaster,
ThetaForecaster,
WeightedEnsembleForecaster,
)

models = {
"seasonal": SeasonalNaiveForecaster(season_length=24),
"theta": ThetaForecaster(theta=2.0, alpha=0.2),
"kalman": KalmanForecaster(),
}

ensemble = WeightedEnsembleForecaster(
models=models,
weights={
"seasonal": 0.55,
"theta": 0.30,
"kalman": 0.15,
},
metadata={"purpose": "panel demand baseline"},
)

ensemble.fit(hourly_pickups)
forecast = ensemble.predict(12)
print(ensemble.get_metadata())
print(forecast.predictions())

Weights are normalized, so {1.0, 3.0} becomes {0.25, 0.75} in metadata.

ForecastFrame Example

from cartoboost.forecasting import ForecastFrame, KalmanForecaster
from cartoboost.forecasting import SeasonalNaiveForecaster, ThetaForecaster
from cartoboost.forecasting import WeightedEnsembleForecaster

frame = ForecastFrame.from_pandas(
hourly_zone_demand.query("series_id == '161'"),
timestamp_col="pickup_hour",
target_col="demand",
freq="h",
)

ensemble = WeightedEnsembleForecaster(
models={
"seasonal": SeasonalNaiveForecaster(season_length=24),
"theta": ThetaForecaster(),
"kalman": KalmanForecaster(),
},
weights={"seasonal": 0.6, "theta": 0.3, "kalman": 0.1},
)
ensemble.fit(frame)
forecast = ensemble.predict(24)

Runnable Visual Example

Run the committed example to compare component forecasts against the weighted blend for a panel dataset:

uv run python examples/forecasting/weighted_ensemble_visualization.py

It writes target/examples/weighted_ensemble.png and prints JSON with component weights plus RMSE and MAE for each component and the ensemble. The example uses synthetic hourly pickup counts and does not download data.

The core pattern is:

from cartoboost.forecasting import (
ForecastFrame,
KalmanForecaster,
SeasonalNaiveForecaster,
ThetaForecaster,
WeightedEnsembleForecaster,
)

frame = ForecastFrame.from_pandas(
train,
timestamp_col="pickup_hour",
target_col="demand",
series_id_col="lane_id",
freq="h",
)

ensemble = WeightedEnsembleForecaster(
models={
"seasonal": SeasonalNaiveForecaster(season_length=24),
"theta": ThetaForecaster(theta=2.0, alpha=0.25),
"kalman": KalmanForecaster(),
},
weights={"seasonal": 0.55, "theta": 0.30, "kalman": 0.15},
)
ensemble.fit(frame)
forecast = ensemble.predict(12)

Weight Interpretation

Weights should be chosen from validation results or a fixed policy. They are not learned by WeightedEnsembleForecaster.

Weight patternInterpretationRisk
High seasonal weightSame-hour history is the main signal.Underreacts to airport surges or disruptions.
High theta weightSmooth trend and level adaptation matter.Can miss sharp intra-day seasonality.
High Kalman weightRecent level and trend should adapt smoothly.Can underweight repeated hourly cycles.
Near-equal weightsComponents have similar validation strength.May hide that one member is consistently worse.

Record the normalized weights with every benchmark result. A weighted ensemble without its member list and weights is not reproducible.

Choosing Weights

A pragmatic workflow is:

  1. Score each component on the same rolling-origin splits.
  2. Remove members that are consistently worse than a simple seasonal baseline.
  3. Try a small weight grid such as seasonal-heavy, trend-heavy, and balanced.
  4. Pick the simplest blend that clears the best individual member on average and does not fail badly on any panel or segment.

Example grid:

candidate_weights = [
{"seasonal": 0.70, "theta": 0.20, "kalman": 0.10},
{"seasonal": 0.55, "theta": 0.30, "kalman": 0.15},
{"seasonal": 0.40, "theta": 0.45, "kalman": 0.15},
]

Keep the component model settings fixed while comparing weight grids. Changing both the member parameters and the weights at the same time makes the result hard to explain.

Rules

RuleDetails
At least one model is requiredEmpty models raises ValueError.
Weights must match model names exactlyMissing or extra names raise ValueError.
Components must be supported CartoBoost forecastersArbitrary Python estimators are not accepted.
Python interval arguments are not supported yetInterval arguments raise NotImplementedError; native member intervals are retained only when every active member returns the same interval grid.

Supported ensemble members currently include naive, seasonal naive, theta, optimized theta, ETS, ARIMA, AutoARIMA, Kalman, and CartoBoostLagForecaster.

Visual Diagnostics

Plot the observed series with every component and the ensemble. Useful patterns:

Visual patternMeaningTypical next step
Ensemble sits between two plausible members.Blend is behaving as expected.Validate the average RMSE or MAE.
Ensemble follows a visibly bad member.That member has too much weight.Lower the weight or remove the member.
All members miss the same rush-hour turn.The ensemble cannot create a signal no member learned.Add a model with the missing calendar, lag, or event behavior.
Ensemble is smoother but less accurate.Averaging reduced variance but added bias.Compare by horizon and by lane before keeping the blend.

Validation

Report every component model and its weight. Compare the ensemble against the best individual component, not only against the weakest baseline.

For taxi benchmarks, include:

  • component model classes and parameters;
  • normalized weights;
  • split timestamps and horizon;
  • per-component and ensemble RMSE and MAE;
  • per-zone or per-lane error summaries when the frame is a panel;
  • training and prediction time when making performance claims.

Use rolling-origin splits and keep the exact train/test rows fixed across all members. If the ensemble only passes one horizon but misses most others, document that horizon-specific behavior instead of presenting it as general.

Limitations

  • An ensemble cannot add information absent from every component.
  • Weights selected on the final holdout leak evaluation information.
  • Correlated members may add runtime without improving robustness.
  • Version every component artifact together with the ensemble weights.