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
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 pattern | Interpretation | Risk |
|---|---|---|
| High seasonal weight | Same-hour history is the main signal. | Underreacts to airport surges or disruptions. |
| High theta weight | Smooth trend and level adaptation matter. | Can miss sharp intra-day seasonality. |
| High Kalman weight | Recent level and trend should adapt smoothly. | Can underweight repeated hourly cycles. |
| Near-equal weights | Components 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:
- Score each component on the same rolling-origin splits.
- Remove members that are consistently worse than a simple seasonal baseline.
- Try a small weight grid such as seasonal-heavy, trend-heavy, and balanced.
- 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
| Rule | Details |
|---|---|
| At least one model is required | Empty models raises ValueError. |
| Weights must match model names exactly | Missing or extra names raise ValueError. |
| Components must be supported CartoBoost forecasters | Arbitrary Python estimators are not accepted. |
| Python interval arguments are not supported yet | Interval 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 pattern | Meaning | Typical 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.