Forecasting Quickstart#

Train a GBLinear model on real energy data and generate probabilistic forecasts with confidence intervals — all in under a minute.

What you’ll learn:

  • Load the Liander 2024 benchmark dataset

  • Configure a forecasting workflow with ForecastingWorkflowConfig

  • Train a model and inspect evaluation metrics

  • Generate quantile forecasts (P10 / P50 / P90)

  • Visualize predictions against actuals

Note

This tutorial uses a small data slice for fast execution. See examples/benchmarks/ for production-scale runs.

Key API references: ForecastingWorkflowConfig create_forecasting_workflow · LeadTime · Q

Load the dataset#

The Liander 2024 benchmark dataset contains load measurements, versioned weather forecasts, EPEX prices, and load profiles for a medium-voltage feeder in the Netherlands.

We split the data into:

  • 45 days of training data

  • 7 days for forecasting

The predict window includes 14 days of history before the forecast start so that lag features (e.g. load_lag_P7D) can be computed during prediction.

from datetime import datetime, timedelta

from openstef_core.testing import load_liander_dataset

dataset = load_liander_dataset()

train_start = datetime.fromisoformat("2024-03-01T00:00:00Z")
train_end = train_start + timedelta(days=45)
forecast_end = train_end + timedelta(days=7)

train_dataset = dataset.filter_by_range(start=train_start, end=train_end)

# Include 14 days of history before forecast start for lag feature computation
predict_dataset = dataset.filter_by_range(
    start=train_end - timedelta(days=14),
    end=forecast_end,
)

print(
    f"Training:  {train_dataset.data.shape[0]:,} rows, "
    f"{train_dataset.data.index.min():%Y-%m-%d} to {train_dataset.data.index.max():%Y-%m-%d}"
)
print(
    f"Predict:   {predict_dataset.data.shape[0]:,} rows, "
    f"{predict_dataset.data.index.min():%Y-%m-%d} to {predict_dataset.data.index.max():%Y-%m-%d}"
)
Training:  4,320 rows, 2024-03-01 to 2024-04-14
Predict:   2,016 rows, 2024-04-01 to 2024-04-21

Hide code cell source

# Quick look at the target variable
fig = cast(Any, train_dataset.data[["load"]].plot(title="Training period — energy load"))
fig.update_layout(yaxis_title="Load (MW)", xaxis_title="Time")
fig.show()
../_images/973a29d1fcb03c61742d3728b9c25380713085ae94ecdc47ad10dcefd75c4fa8.png

Configure the workflow#

ForecastingWorkflowConfig bundles all settings — model type, horizons, quantiles, and feature columns — into a single object. create_forecasting_workflow turns it into a ready-to-use pipeline with preprocessing, training, and postprocessing.

We pick GBLinear (gradient-boosted linear model) for its speed and ability to extrapolate beyond training data.

from openstef_core.types import LeadTime, Q
from openstef_models.presets import ForecastingWorkflowConfig, create_forecasting_workflow
from openstef_models.presets.forecasting_workflow import GBLinearForecaster

workflow = create_forecasting_workflow(
    config=ForecastingWorkflowConfig(
        model_id="quickstart_gblinear",
        model="gblinear",
        horizons=[LeadTime.from_string("PT36H")],
        quantiles=[Q(0.5), Q(0.1), Q(0.9)],
        target_column="load",
        # Weather features available in the Liander dataset
        temperature_column="temperature_2m",
        relative_humidity_column="relative_humidity_2m",
        wind_speed_column="wind_speed_10m",
        radiation_column="shortwave_radiation",
        pressure_column="surface_pressure",
        verbosity=0,
        mlflow_storage=None,
        gblinear_hyperparams=GBLinearForecaster.HyperParams(n_steps=50),
    )
)

Train the model#

workflow.fit() runs the full pipeline: feature engineering, data validation, model training, and evaluation on a held-out test split.

result = workflow.fit(train_dataset)

if result is not None:
    print("Training metrics:")
    print(result.metrics_full.to_dataframe())

    if result.metrics_test is not None:
        print("\nTest-set metrics:")
        print(result.metrics_test.to_dataframe())
Training metrics:
   quantile        R2  observed_probability
0       0.5  0.707408              0.427586
1       0.1  0.651366              0.103448
2       0.9  0.265259              0.910345

Generate forecasts#

The trained workflow produces a ForecastDataset with point predictions and quantile bands. The P10-P90 interval covers 80 % of expected outcomes. To improve the reliability of these quantile estimates, see Quantile Calibration.

from openstef_core.datasets import ForecastDataset

forecast: ForecastDataset = workflow.predict(predict_dataset, forecast_start=train_end)

print(f"Forecast rows: {len(forecast.data)}")
print(f"Quantiles:     {forecast.quantiles}")
forecast.data.tail()
Forecast rows: 145
Quantiles:     [0.5, 0.1, 0.9]
quantile_P50 quantile_P10 quantile_P90 load stdev
timestamp
2024-04-16 11:00:00+00:00 -550104.4375 -854959.5000 42038.531250 -390000.000000 334069.798256
2024-04-16 11:15:00+00:00 -529552.2500 -850952.2500 48707.546875 -226666.666667 334069.798256
2024-04-16 11:30:00+00:00 -484266.6875 -832215.5000 79022.046875 -553333.333333 334069.798256
2024-04-16 11:45:00+00:00 -422226.3750 -801489.4375 121177.070312 -733333.333333 334069.798256
2024-04-16 12:00:00+00:00 -364359.7500 -773782.8750 159615.703125 -320000.000000 222646.564072

Visualize the results#

ForecastTimeSeriesPlotter overlays measurements and predictions with shaded confidence bands in a single interactive chart.

Hide code cell source

from openstef_beam.analysis.plots import ForecastTimeSeriesPlotter

fig = (
    ForecastTimeSeriesPlotter()
    .add_measurements(measurements=predict_dataset.data["load"].loc[train_end:])
    .add_model(
        model_name="GBLinear",
        forecast=forecast.median_series,
        quantiles=forecast.quantiles_data,
    )
    .plot()
)
fig.update_layout(
    title="Forecast vs actuals",
    yaxis_title="Load (MW)",
    xaxis_title="Time",
    height=500,
)
fig.show()
../_images/4101c183f4a534dd925294e966cd7de859c07ab85a3ed8ad7bd1ada5b966317f.png

Next steps#