Model Pipeline Overview
The ModelPipeline class trains and evaluates forecasting models with rolling-window validation.
Basic Usage
from epftoolbox2.pipelines import ModelPipelinefrom epftoolbox2.models import OLSModel, LassoCVModelfrom epftoolbox2.evaluators import MAEEvaluatorfrom epftoolbox2.exporters import ExcelExporter, TerminalExporter
pipeline = ( ModelPipeline() .add_model(OLSModel(predictors=predictors, name="OLS")) .add_model(LassoCVModel(predictors=predictors, name="LassoCV")) .add_evaluator(MAEEvaluator()) .add_exporter(TerminalExporter()) .add_exporter(ExcelExporter("results.xlsx")))
report = pipeline.run( data=df, test_start="2024-02-01", test_end="2024-03-01", target="price", horizon=7, freq="1h", # Optional. Allows sub-hourly testing like "15min" save_dir="results",)Pipeline Components
Models
Evaluators
Exporters
Predictor Specification
Predictors can be specified in four ways:
predictors = [ "load_actual", "load_actual_d-1", "price_d-1",]Use {horizon} placeholder: replaced with 1, 2, …, horizon at runtime:
predictors = [ "warsaw_temperature_2m_d+{horizon}", "is_monday_d+{horizon}", "is_holiday_d+{horizon}", "daylight_hours_d+{horizon}",]Callables receive horizon (1 param) or (horizon, hour) (2 params):
# horizon-onlypredictors = [ lambda horizon: f"weather_d+{horizon}",]
# horizon + hour: different feature per hour of daypredictors = [ lambda horizon, hour: f"load_h{hour:02d}_d+{horizon}",]Use list comprehensions for many features:
predictors = [ "load_actual", *[f"load_actual_h-{i}" for i in range(1, 169)], *[f"price_d-{i}" for i in range(1, 8)],]Parallelism
Models use a process pool with inner thread pools. Each worker process handles one forecast day (24 hours × horizon), keeping results consistent and progress tracking stable.
| Variable | Default | Description |
|---|---|---|
MAX_PROCESSES | cpu_count // THREADS_PER_PROCESS | Worker processes |
THREADS_PER_PROCESS | 16 | Threads per process |
import os
# Must be set before any importsos.environ["PYTHON_GIL"] = "0" # Enable free-threading (Python 3.13t+)os.environ["MAX_PROCESSES"] = "4"os.environ["THREADS_PER_PROCESS"] = "16" # → 4 × 16 = 64 coresos.environ["OMP_NUM_THREADS"] = "1" # Prevent BLAS oversubscriptionSee Installation for full setup including platform notes.
Feature Scaling
Models scale each task’s data with a scaler fitted on that task’s training rows only. By default this is StandardScaler:
-
Numeric features are standardized (mean=0, std=1)
-
Binary features (0/1) are auto-detected and skipped
-
Target variable is scaled, predictions are inverse-scaled
Set a model’s scaler parameter to "asinh" or "npit" to use a variance-stabilising transformation instead, which damps price spikes and handles negative prices. See Scalers and VSTs.
Run Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data | DataFrame | Required | Input data with DatetimeIndex |
test_start | str | Required | Test period start |
test_end | str | Required | Test period end |
target | str | "price" | Target column name |
horizon | int | 7 | Max forecast horizon (days) |
freq | str | "1h" | Forecasting frequency (e.g., "15min") |
save_dir | str | None | Directory for incremental results |
forecast_only | bool | False | Skip evaluators/exporters. test_start/test_end default to "today" if omitted |
Model names must be unique: they name the result files in save_dir.
Data Checks
Before any model is fitted, run() checks every model’s input and raises ValueError with a description of the problem:
- Resolution: rows must be
freqapart, with one row per local day and period (hour, or quarter-hour forfreq="15min"). Only the hour repeated when clocks fall back may occur twice. Data finer thanfreqwould put the issue day’s prices into the training set. Resample it first (ResampleTransformer) or pass the matchingfreq. - Timestamps: no duplicates. Rows are sorted by time.
- Names: the data must not contain the model’s target columns (
{target}_d+1…{target}_d+{horizon}, e.g. from aLagTransformerwith negative lags on the target) or columns namedhourorday, which the model uses internally (useCalendarSource(prefix=...)for itshourcolumn). - Predictors: every expanded predictor (for every hour and horizon) must exist, be numeric, and must not be one of the model’s targets.
Only the target and the predictors are used, so other columns (e.g. text columns such as holiday_name) can stay in the data.
Information Set
The checks above don’t know when a forecast is issued, so they can’t tell a legitimate lag from look-ahead
(same-day load_actual passes them, even though it isn’t known until after the hour it describes). Pass
issue_time to ModelPipeline to add that check:
pipeline = ModelPipeline(issue_time="11:45", issue_timezone="Europe/Brussels")This raises InformationSetError before any fit if a predictor (or the newest training target) is not known
at issue time, using built-in availability rules for the shipped sources (ENTSO-E prices/load/generation,
Open-Meteo weather, calendar). See Information Set for the
full rule reference, custom rules, and the empirical leakage_probe.
EvaluationReport
The pipeline returns an EvaluationReport:
report.summary() # Overall metrics, with a `forecasts` count per modelreport.forecast_counts() # Planned vs produced forecasts and skip reasonsreport.by_hour() # Breakdown by hourreport.by_horizon() # Breakdown by horizonreport.by_hour_horizon() # Combined breakdownSee Working with Results for what the counts mean.
YAML Serialization
Save and reload a complete pipeline configuration:
pipeline.save("model_pipeline.yaml")pipeline = ModelPipeline.load("model_pipeline.yaml")See ModelPipeline Serialization for the full YAML format and serialization rules.