plots.plotter

plots.plotter

Module for generating interactive prediction plots.

This module provides the PredictionFigure class and make_plot function to visualize time series forecasting results, including actual values, predictions, and performance metrics.

Classes

Name Description
PredictionFigure Encapsulates the generation of an interactive Plotly figure for predictions.

PredictionFigure

plots.plotter.PredictionFigure(
    prediction_package,
    title='Energy Demand Prediction',
)

Encapsulates the generation of an interactive Plotly figure for predictions.

Parameters

Name Type Description Default
prediction_package Dict[str, Any] A dictionary containing prediction data and metrics. Expected keys include: - ‘train_actual’: pd.Series - ‘future_actual’: pd.Series - ‘train_pred’: pd.Series - ‘future_pred’: pd.Series - ‘future_forecast’: pd.Series (e.g., benchmark/ENTSOE) - ‘test_actual’: pd.Series (optional external ground truth for the forecast period, e.g. from data_test.csv; absent in genuine-future mode when no ground truth is available yet) - ‘metrics_train’: Dict[str, float] - ‘metrics_future’: Dict[str, float] - ‘metrics_future_one_day’: Dict[str, float] - ‘metrics_forecast’: Dict[str, float] - ‘metrics_forecast_one_day’: Dict[str, float] required
title str Figure title shown at the top of the plot. 'Energy Demand Prediction'

Examples

import numpy as np
import pandas as pd
import plotly.graph_objects as go

from spotforecast2.plots.plotter import PredictionFigure

rng = np.random.default_rng(0)
train_idx = pd.date_range("2026-01-01", periods=48, freq="h", tz="UTC")
future_idx = pd.date_range("2026-01-03", periods=24, freq="h", tz="UTC")
pkg = {
    "train_actual": pd.Series(rng.uniform(80, 120, 48), index=train_idx),
    "train_pred": pd.Series(rng.uniform(80, 120, 48), index=train_idx),
    "future_actual": pd.Series(rng.uniform(80, 120, 24), index=future_idx),
    "future_pred": pd.Series(rng.uniform(80, 120, 24), index=future_idx),
    "metrics_train": {"mae": 2.5, "mape": 0.02},
    "metrics_future": {"mae": 3.0, "mape": 0.03},
    "metrics_future_one_day": {"mae": 2.8, "mape": 0.025},
}
pf = PredictionFigure(pkg, title="Demo Forecast")
fig = pf.make_plot()
assert isinstance(fig, go.Figure)
assert len(fig.data) >= 3  # at least actual, prediction, last-week traces
print(f"Figure has {len(fig.data)} traces, title: '{pf.title}'")
Figure has 3 traces, title: 'Demo Forecast'

Methods

Name Description
make_plot Generate the Plotly figure with traces and annotations.
save_to_file Write the figure to a static image file (PNG, SVG, PDF, …).
write_to_file Render the figure into a standalone HTML page via a Jinja2 template.
make_plot
plots.plotter.PredictionFigure.make_plot()

Generate the Plotly figure with traces and annotations.

Traces added (always): - Total system load — actual (training window, clipped to visible range) - Total system load — model prediction (training + forecast, clipped) - Actual (last week) — time-shifted actual for seasonality context

Traces added (when data is available): - Benchmark Forecast (e.g., ENTSOE) — if future_forecast key present - Actual (test / ground truth) — if test_actual key present

The X-axis is fixed to [end_training − 1 day, future_pred.max() + 1 h] so the full forecast window is always visible, including in genuine-future mode where future_actual is an empty Series. Only the data slice in that window is serialised into the Plotly JSON, keeping HTML output small.

Examples
import numpy as np
import pandas as pd
import plotly.graph_objects as go

from spotforecast2.plots.plotter import PredictionFigure

rng = np.random.default_rng(0)
dates = pd.date_range("2023-01-01", periods=100, freq="h", tz="UTC")
train_end = dates[70]
y = pd.Series(rng.uniform(0, 100, 100), index=dates, name="load")
p = y + rng.normal(0, 5, 100)
pkg = {
    "train_actual": y.loc[:train_end],
    "future_actual": y.loc[train_end:],
    "train_pred": p.loc[:train_end],
    "future_pred": p.loc[train_end:],
    "metrics_train": {"mae": 5.0, "mape": 0.1},
    "metrics_future": {"mae": 6.0, "mape": 0.12},
    "metrics_future_one_day": {"mae": 4.5, "mape": 0.08},
}
fig = PredictionFigure(pkg).make_plot()
assert isinstance(fig, go.Figure)
print(f"Traces: {len(fig.data)}, type: {type(fig).__name__}")
Traces: 3, type: Figure
save_to_file
plots.plotter.PredictionFigure.save_to_file(path, **kwargs)

Write the figure to a static image file (PNG, SVG, PDF, …).

Thin wrapper around plotly.graph_objects.Figure.write_image(). Requires the kaleido package (declared in pyproject.toml).

Parameters
Name Type Description Default
path Union[str, Path] Destination file path. The format is inferred from the extension (.png, .svg, .pdf, …). required
**kwargs Any Forwarded to fig.write_image (e.g. width, height, scale). {}
Returns
Name Type Description
Path The resolved pathlib.Path of the written file.
Raises
Name Type Description
FileNotFoundError If the parent directory does not exist.
Examples
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd

from spotforecast2.plots.plotter import PredictionFigure

rng = np.random.default_rng(0)
train_idx = pd.date_range("2026-01-01", periods=48, freq="h", tz="UTC")
future_idx = pd.date_range("2026-01-03", periods=24, freq="h", tz="UTC")
pkg = {
    "train_actual": pd.Series(rng.standard_normal(48), index=train_idx),
    "train_pred": pd.Series(rng.standard_normal(48), index=train_idx),
    "future_actual": pd.Series(rng.standard_normal(24), index=future_idx),
    "future_pred": pd.Series(rng.standard_normal(24), index=future_idx),
    "metrics_train": {"mae": 1.0, "mape": 0.1},
    "metrics_future": {"mae": 1.0, "mape": 0.1},
    "metrics_future_one_day": {"mae": 1.0, "mape": 0.1},
}
fig = PredictionFigure(pkg)
fig.make_plot()
with tempfile.TemporaryDirectory() as tmp:
    out = fig.save_to_file(Path(tmp) / "out.png")
    print(out.exists())
True
write_to_file
plots.plotter.PredictionFigure.write_to_file(path, *, template_path=None)

Render the figure into a standalone HTML page via a Jinja2 template.

The default template wraps the Plotly figure in a minimal HTML skeleton with the figure title as <title>. Provide template_path to substitute a custom Jinja2 template that receives the variable fig (an HTML fragment) and title.

Parameters
Name Type Description Default
path Union[str, Path] Destination HTML file path. required
template_path Optional[Union[str, Path]] Optional path to a custom Jinja2 template. None
Returns
Name Type Description
Path The resolved pathlib.Path of the written file.
Raises
Name Type Description
FileNotFoundError If the parent directory or template_path does not exist.
Examples
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd

from spotforecast2.plots.plotter import PredictionFigure

rng = np.random.default_rng(0)
train_idx = pd.date_range("2026-01-01", periods=48, freq="h", tz="UTC")
future_idx = pd.date_range("2026-01-03", periods=24, freq="h", tz="UTC")
pkg = {
    "train_actual": pd.Series(rng.standard_normal(48), index=train_idx),
    "train_pred": pd.Series(rng.standard_normal(48), index=train_idx),
    "future_actual": pd.Series(rng.standard_normal(24), index=future_idx),
    "future_pred": pd.Series(rng.standard_normal(24), index=future_idx),
    "metrics_train": {"mae": 1.0, "mape": 0.1},
    "metrics_future": {"mae": 1.0, "mape": 0.1},
    "metrics_future_one_day": {"mae": 1.0, "mape": 0.1},
}
fig = PredictionFigure(pkg, title="Demo")
fig.make_plot()
with tempfile.TemporaryDirectory() as tmp:
    out = fig.write_to_file(Path(tmp) / "report.html")
    print(out.suffix)
.html

Functions

Name Description
make_plot Generate and optionally save an interactive prediction plot.
plot_actual_vs_predicted Plot actual vs predicted combined values for model comparison.
plot_with_outliers Interactive time series plot with outliers and optional bounds.

make_plot

plots.plotter.make_plot(
    prediction_package,
    output_path=None,
    title='Energy Demand Prediction',
    save=True,
)

Generate and optionally save an interactive prediction plot.

Parameters

Name Type Description Default
prediction_package Dict[str, Any] Dictionary of results (actuals, preds, metrics). required
output_path Optional[Union[str, Path]] Path to save the HTML file. If None, it defaults to ‘index.html’ in the package’s data home directory. None
title str Figure title shown at the top of the plot. 'Energy Demand Prediction'
save bool If True (default), write the figure to output_path. Pass False to obtain a figure without any disk side-effect (used by the multitask pipeline, where many figures are produced per run and the auto-write would otherwise overwrite the same file repeatedly). True

Returns

Name Type Description
go.Figure The generated Plotly Figure object.

Examples

import numpy as np
import pandas as pd
import plotly.graph_objects as go

from spotforecast2.plots.plotter import make_plot

rng = np.random.default_rng(1)
train_idx = pd.date_range("2026-01-01", periods=48, freq="h", tz="UTC")
future_idx = pd.date_range("2026-01-03", periods=24, freq="h", tz="UTC")
pkg = {
    "train_actual": pd.Series(rng.uniform(80, 120, 48), index=train_idx),
    "train_pred": pd.Series(rng.uniform(80, 120, 48), index=train_idx),
    "future_actual": pd.Series(rng.uniform(80, 120, 24), index=future_idx),
    "future_pred": pd.Series(rng.uniform(80, 120, 24), index=future_idx),
    "metrics_train": {"mae": 2.5, "mape": 0.02},
    "metrics_future": {"mae": 3.0, "mape": 0.03},
    "metrics_future_one_day": {"mae": 2.8, "mape": 0.025},
}
fig = make_plot(pkg, save=False)
assert isinstance(fig, go.Figure)
print(f"Figure type: {type(fig).__name__}, traces: {len(fig.data)}")
Figure type: Figure, traces: 3

plot_actual_vs_predicted

plots.plotter.plot_actual_vs_predicted(
    actual_combined,
    baseline_combined,
    covariates_combined,
    custom_lgbm_combined,
    html_path=None,
)

Plot actual vs predicted combined values for model comparison.

This function creates an interactive Plotly figure comparing ground truth with predictions from three different forecasting models: baseline, covariate-enhanced, and custom LightGBM. The plot includes interactive hover information and can be saved as a standalone HTML file.

Safety-Critical Features

  • Interactive visualization for model validation
  • Supports HTML export for audit trails
  • Shows all models simultaneously for easy comparison
  • Uses consistent color scheme and line styles

Parameters

Name Type Description Default
actual_combined pd.Series Ground truth combined series with datetime index. required
baseline_combined pd.Series Baseline combined prediction series. Must have same index as actual_combined. required
covariates_combined pd.Series Covariate-enhanced combined prediction series. Must have same index as actual_combined. required
custom_lgbm_combined pd.Series Custom LightGBM (optimized params) combined prediction series. Must have same index as actual_combined. required
html_path Optional[str] If set, save the plot as a single self-contained HTML file to this path. If None, displays plot interactively only. None

Returns

Name Type Description
None None. Displays plot and optionally saves to HTML file.

Raises

Name Type Description
ValueError If series indices don’t align or are empty.

Examples

import numpy as np
import pandas as pd

from spotforecast2.plots.plotter import plot_actual_vs_predicted

rng = np.random.default_rng(42)
index = pd.date_range("2020-01-01", periods=24, freq="h")
actual = pd.Series(
    100 + 10 * np.sin(np.arange(24) * 2 * np.pi / 24), index=index
)
baseline = actual + rng.normal(0, 2, 24)
covariates = actual + rng.normal(0, 1, 24)
custom = actual + rng.normal(0, 0.5, 24)

assert isinstance(actual.index, pd.DatetimeIndex)
assert (actual.index == baseline.index).all()
mae_baseline = abs(actual - baseline).mean()
mae_custom = abs(actual - custom).mean()
print(f"Baseline MAE: {mae_baseline:.2f}, Custom MAE: {mae_custom:.2f}")
assert mae_custom < mae_baseline  # tighter noise → lower error
Baseline MAE: 1.36, Custom MAE: 0.35

plot_with_outliers

plots.plotter.plot_with_outliers(
    df_pipeline,
    df_pipeline_original,
    config,
    targets=None,
)

Interactive time series plot with outliers and optional bounds.

This function generates an interactive Plotly figure that visualizes the time series data from the pipeline, highlighting any detected outliers. Regular data points are shown in light grey, while outliers are marked in red. When config.bounds is set, two horizontal reference lines in lightblue are added per plot — one for the lower bound and one for the upper bound — to indicate the acceptable value range for that target.

The plot title includes the percentage of outliers detected for each target variable.

The explicit targets argument takes precedence when provided. When omitted, config.targets is used — the supported path for standalone/notebook usage where the config carries the resolved target list (e.g. ConfigEntsoe with explicit targets). Pipeline-internal callers (PlottingMixin) always pass task.run_state.targets explicitly after prepare_data has run.

Parameters

Name Type Description Default
df_pipeline pd.DataFrame The processed DataFrame from the pipeline, which may contain NaN values where outliers have been detected and removed. required
df_pipeline_original pd.DataFrame The original DataFrame before outlier removal. required
config Any Configuration object carrying bounds (optional list of (lower, upper) tuples, one per target, in the same order as targets). config.targets is used when the targets argument is None — the supported standalone/notebook path. required
targets Optional[list[str]] Resolved list of target column names. When provided, takes precedence over config.targets. When omitted, config.targets is used — a supported convenience for callers (e.g. notebooks) where the config already carries the resolved target list. None

Returns

Name Type Description
None None. Displays one interactive Plotly figure per target variable.

Examples

import pandas as pd
import numpy as np
from types import SimpleNamespace
from spotforecast2.plots.plotter import plot_with_outliers
# Create synthetic data
dates = pd.date_range("2023-01-01", periods=100, freq="h", tz="UTC")
data = pd.DataFrame({
    "target1": np.random.rand(100) * 100,
    "target2": np.random.rand(100) * 50,
}, index=dates)
# Introduce outliers
data.loc[dates[10], "target1"] = 300  # Outlier in target1
data.loc[dates[20], "target2"] = 150  # Outlier in target2
df_pipeline = data.copy()
df_pipeline.loc[[dates[10], dates[20]], ["target1", "target2"]] = np.nan
# Config with bounds; targets passed explicitly
config = SimpleNamespace(bounds=[(-10, 200), (0, 100)])
plot_with_outliers(df_pipeline, data, config, targets=["target1", "target2"])
# Standalone path: config.targets is used when targets kwarg is omitted
config2 = SimpleNamespace(bounds=None, targets=["target1", "target2"])
plot_with_outliers(df_pipeline, data, config2)