Baseline Models#

Source Files
  • twiga/models/baseline/naive_model.py - NAIVEModel / NAIVEConfig

  • twiga/models/baseline/seasonal_naive_model.py - SEASONALNAIVEModel / SEASONALNAIVEConfig

  • twiga/models/baseline/window_average_model.py - WINDOWAVERAGEModel / WINDOWAVERAGEConfig

  • twiga/models/baseline/drift_model.py - DRIFTModel / DRIFTConfig

  • twiga/models/baseline/context_parrot_model.py - CONTEXTPARROTModel / CONTEXTPARROTConfig

Twiga’s baseline domain provides five reference models that require no training data beyond shape information. They serve as lower-bound benchmarks when evaluating learned models, as inputs to skill score calculations, and as lightweight forecast components for rapid prototyping. All baseline models share the same interface as ML and NN models and can be composed freely with TwigaForecaster.

For the full model catalogue see the Model Catalog Overview.

When to use baseline models

Use case

Recommended model

Standard persistence benchmark

NAIVEModel(strategy="window_last")

Fixed-value reference (e.g. training mean)

NAIVEModel(strategy="mean")

Daily or weekly seasonality benchmark

SEASONALNAIVEModel(period="1D") / SEASONALNAIVEModel(period="7D")

Level-adapted mean baseline

WINDOWAVERAGEModel(window_size=24)

Trending signal benchmark

DRIFTModel()

Non-parametric motif matching

CONTEXTPARROTModel() (default “parrot” method)

Autoregressive k-NN forecast

CONTEXTPARROTModel(method="simplex")

Skill score denominator

Any of the above, chosen to match the naive reference in your domain

Baseline models are especially valuable in energy forecasting: a well-tuned seasonal naive forecast is a strong competitor for load, and any learned model that does not clearly outperform it on skill score should be scrutinised before production use.

Common Interface#

All four baseline models extend BaseRegressor and override its key methods with statistic-only logic. There are no trainable parameters.

Constructor Pattern#

Model(model_config: Config | None = None)

Each model defaults to its own config class when model_config is omitted.

Shared Attributes#

Attribute

Type

Description

model

None

Always None - no underlying sklearn estimator.

num_targets

int | None

Number of target variables. Set during fit() from y.shape.

horizon

int | None

Forecast horizon length. Set during fit() from y.shape.

Shared Methods#

Method

Signature

Description

fit

fit(X, y, eval_set=None, verbose=False) -> self

Stores num_targets and horizon from y.shape. Validates that X and y are 3-D arrays. eval_set is accepted for API compatibility and silently ignored.

predict

predict(x: np.ndarray) -> np.ndarray

Returns predictions of shape (B, horizon, num_targets). Raises ValueError if the model has not been fitted or if x is not 3-D.

forecast

forecast(x: np.ndarray) -> np.ndarray

Returns the raw prediction array, identical to predict. Baseline models do not return a dict.

update

update(trial) -> None

Rebuilds the config from an Optuna trial’s sampled parameters (or no-op for models with no hyperparameters).

Feature-column convention

All baseline models read the observed target series directly from the input array X. They assume that X[:, t, :num_targets] contains the raw target values at time step t within the lookback window - i.e. the first num_targets feature columns at every step are the target variable(s) in chronological order.

Twiga’s DataPipeline satisfies this convention by default: target lag features are placed as the leading columns of the feature matrix. This must be preserved when using custom pipelines with baseline models.

NAIVEModel#

NAIVEModel implements four persistence strategies, ranging from the classical per-window naive forecast ("window_last") to a fixed zero reference ("zero"). It is the simplest and most commonly used baseline.

Prediction Strategies#

Strategy

Formula

Description

"window_last" (default)

ŷ_{t+h} = x[:, -1, :num_targets] for all h

True persistence: repeats the last observed value in each input window across all forecast steps.

"last"

ŷ = y_train[-1, :] broadcast

Uses the very last value seen during training, constant across all test samples and steps.

"mean"

ŷ = mean(y_train) broadcast

Per-output training-set mean, constant across all test samples and steps.

"zero"

ŷ = 0

Predicts zero everywhere. Useful for centred or differenced targets.

The "window_last" and "window_average" strategies adapt to the local level of each test window; "last" and "mean" are global constants derived from the training set.

NAIVEConfig#

Field

Type

Default

Description

name

Literal["naive"]

"naive"

Model identifier. Excluded from parameter dumps.

domain

Literal["baseline"]

"baseline"

Domain identifier. Excluded from parameter dumps.

strategy

Literal["last", "window_last", "mean", "zero"]

"window_last"

Prediction strategy.

search_space

BaseSearchSpace

(see below)

Default hyperparameter search space. Excluded from parameter dumps.

Default Search Space:

Parameter

Values

Type

strategy

["last", "window_last", "mean", "zero"]

categorical

Predict Logic#

For "window_last":

last_obs = x[:, -1, :num_targets]              # (B, num_targets)
preds = broadcast(last_obs, (B, horizon, num_targets))

For "last", "mean", and "zero", a constant vector stored at fit time is broadcast to all samples and horizon steps.

API Reference#

class twiga.models.baseline.naive_model.NAIVEModel(model_config=None, *, num_targets=None, horizon=None)#

Bases: BaseRegressor

Naive baseline for point multi-horizon forecasting.

Implements four persistence strategies that serve as reference baselines for benchmarking learned models. The model has no trainable parameters; fit only stores summary statistics from the training targets.

For the "window_last" and "zero" strategies the model is self-contained and can produce predictions without a prior fit() call, as long as horizon and num_targets are supplied at construction time:

model = NAIVEModel(num_targets=1, horizon=24)
preds = model.predict(X_test)  # no fit() needed

For "last" and "mean" strategies, training data are required — call fit(X_train, y_train) explicitly.

The eval_set argument in fit() is accepted for API compatibility and silently ignored.

Parameters:
  • model_config (NAIVEConfig | None) – Configuration object. Defaults to NAIVEConfig (strategy "window_last").

  • num_targets (int | None) – Optional hint for the number of target columns. Used for zero-shot prediction without fit(). Defaults to 1 when not provided.

  • horizon (int | None) – Optional forecast horizon hint. Required for zero-shot use. Ignored once fit() has been called.

Example:

model = NAIVEModel()
model.fit(X_train, y_train)
preds = model.predict(X_test)  # shape (B, H, T)
fit(X, y, eval_set=None, verbose=False, trial=None)#

Compute and store the training-set constant.

Parameters:
  • X (ndarray) – Shape (B, L, F) - ignored except for shape information.

  • y (ndarray) – Shape (B, L, H) - training targets.

  • eval_set (tuple[ndarray, ndarray] | None) – Accepted for API compatibility; silently ignored.

  • verbose (bool) – Unused; included for interface consistency.

  • trial (Any | None) – Optuna trial passed through from the forecaster during HPO (accepted but not used).

Return type:

NAIVEModel

Returns:

Self for method chaining.

forecast(x)#

Return naive point forecast (raw array, no wrapping dict).

Return type:

ndarray

property min_seq_len: int#

Minimum lookback window length required by this model.

All naive strategies work with any L ≥ 1, so this always returns 1. Provided for API consistency with other baseline models.

predict(x)#

Return naive predictions for the input batch.

For "window_last" and "zero" strategies, fit() is not required when horizon and num_targets are set at construction.

Parameters:

x (ndarray) – Shape (B, L, F).

Return type:

ndarray

Returns:

Predictions of shape (B, H, T).

Raises:

ValueError – If the model has not been fitted and the strategy requires training data, or if x is not 3-D.

set_fit_request(*, eval_set='$UNCHANGED$', trial='$UNCHANGED$', verbose='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the fit method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

eval_setstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for eval_set parameter in fit.

trialstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for trial parameter in fit.

verbosestr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for verbose parameter in fit.

Returns#

selfobject

The updated object.

set_predict_request(*, x='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

xstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for x parameter in predict.

Returns#

selfobject

The updated object.

set_score_request(*, sample_weight='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

sample_weightstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for sample_weight parameter in score.

Returns#

selfobject

The updated object.

update(trial)#

Rebuild config from an Optuna trial’s suggested strategy.

Return type:

None

SEASONALNAIVEModel#

SEASONALNAIVEModel implements the classical seasonal naive forecast. For each forecast step h, the prediction is the value observed exactly m steps before that step in the input window, where m is the resolved seasonal period. For forecast horizons longer than one season (H > m), the pattern wraps modularly.

Period Resolution#

The period parameter accepts either a raw integer (number of data steps) or a pandas duration string, which is resolved against the freq sampling frequency via _resolve_period(period, freq).

period

freq

Resolved steps

Interpretation

"1D"

"1h"

24

Daily periodicity, hourly data

"1D"

"30min"

48

Daily periodicity, 30-min data

"7D"

"1h"

168

Weekly periodicity, hourly data

"7D"

"30min"

336

Weekly periodicity, 30-min data

48

(ignored)

48

Direct integer specification

_resolve_period raises ValueError if period is not a whole multiple of freq, or if the resolved step count is not positive.

SEASONALNAIVEConfig#

Field

Type

Default

Description

name

Literal["seasonal_naive"]

"seasonal_naive"

Model identifier. Excluded from parameter dumps.

domain

Literal["baseline"]

"baseline"

Domain identifier. Excluded from parameter dumps.

period

int | str

"1D"

Seasonal period: positive integer or pandas duration string.

freq

str

"1h"

Data sampling frequency, used to resolve string periods.

search_space

BaseSearchSpace

(see below)

Default hyperparameter search space. Excluded from parameter dumps.

Default Search Space:

Parameter

Values

Type

period

["1D", "7D"]

categorical

Fit Validation#

fit() resolves and stores the period in steps, then validates:

seq_len >= period_steps

A ValueError is raised if the lookback window is shorter than the resolved period. Increase lookback_window_size in DataPipelineConfig or reduce the period to fix this.

Predict Logic#

For each forecast step h (0-indexed), the source index into the input window is:

indices = [seq_len - m + (h % m) for h in range(horizon)]
preds = x[:, indices, :num_targets].copy()   # (B, horizon, num_targets)

The modular wrap h % m ensures that when horizon > m, the seasonal pattern repeats correctly.

        flowchart LR
    X["x: (B, L, F)"] --> IDX["indices: [L-m, L-m+1, ..., L-1,\n L-m, L-m+1, ...]"]
    IDX --> GATHER["gather x[:, indices, :T]"]
    GATHER --> PRED["preds: (B, H, T)"]
    

API Reference#

class twiga.models.baseline.seasonal_naive_model.SEASONALNAIVEModel(model_config=None, *, num_targets=None, horizon=None)#

Bases: BaseRegressor

Seasonal naïve baseline for multi-horizon forecasting.

Repeats the value observed exactly m data steps before each forecast step, where m is the resolved seasonal period. For forecast horizons longer than one season, the seasonal pattern wraps.

The model has no trainable parameters. fit validates the sequence length and resolves the period; all inference work happens in predict.

Parameters:

model_config (SEASONALNAIVEConfig | None) – Configuration object. Defaults to SEASONALNAIVEConfig (period="1D", freq="1H").

Example:

# Daily naive on 30-min data (48 steps)
model = SEASONALNAIVEModel(SEASONALNAIVEConfig(period="1D", freq="30min"))
model.fit(X_train, y_train)
preds = model.predict(X_test)  # (B, L, H)

# Weekly naive on hourly data
model = SEASONALNAIVEModel(SEASONALNAIVEConfig(period="7D", freq="1H"))
fit(X, y, eval_set=None, verbose=False, trial=None)#

Resolve the seasonal period and validate the sequence length.

Parameters:
  • X (ndarray) – Shape (B, L, F). L must be ≥ resolved period.

  • y (ndarray) – Shape (B, L, H).

  • eval_set (tuple[ndarray, ndarray] | None) – Accepted for API compatibility; silently ignored.

  • verbose (bool) – Unused; included for interface consistency.

  • trial (Any | None) – Optuna trial passed through from the forecaster during HPO (accepted but not used).

Return type:

SEASONALNAIVEModel

Returns:

Self for method chaining.

Raises:

ValueError – If the resolved period exceeds the sequence length L, or if X or y are not 3-dimensional.

forecast(x)#

Return seasonal naïve point forecast (raw array).

Return type:

ndarray

property min_seq_len: int#

Minimum lookback window length required by the configured period.

Use this to validate your data pipeline’s lookback setting before fitting. Raises ValueError if the period cannot be resolved (e.g. incompatible freq string).

Example:

model = SEASONALNAIVEModel(SEASONALNAIVEConfig(period="7D", freq="1h"))
assert pipeline_config.seq_len >= model.min_seq_len
predict(x)#

Return seasonal naïve predictions.

fit() is not required when horizon and num_targets are set at construction time.

For forecast step h (0-indexed), the prediction is taken from position x[:, L - m + (h % m), :num_targets] in the input window, where L is the sequence length and m is the period.

Parameters:

x (ndarray) – Shape (B, L, F). L must be ≥ resolved period.

Return type:

ndarray

Returns:

Predictions of shape (B, horizon, num_targets).

Raises:

ValueError – If no horizon hint was provided and fit() was not called, or if the sequence length is shorter than the period.

set_fit_request(*, eval_set='$UNCHANGED$', trial='$UNCHANGED$', verbose='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the fit method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

eval_setstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for eval_set parameter in fit.

trialstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for trial parameter in fit.

verbosestr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for verbose parameter in fit.

Returns#

selfobject

The updated object.

set_predict_request(*, x='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

xstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for x parameter in predict.

Returns#

selfobject

The updated object.

set_score_request(*, sample_weight='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

sample_weightstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for sample_weight parameter in score.

Returns#

selfobject

The updated object.

update(trial)#

Rebuild config from an Optuna trial’s suggested period.

Return type:

None

WINDOWAVERAGEModel#

WINDOWAVERAGEModel predicts the local mean of the last window_size observed target steps in the input context, broadcast to all forecast horizon steps. Because it adapts to the current level of each window, it is a stronger baseline than the global NAIVEModel(strategy="mean").

WINDOWAVERAGEConfig#

Field

Type

Default

Description

name

Literal["window_average"]

"window_average"

Model identifier. Excluded from parameter dumps.

domain

Literal["baseline"]

"baseline"

Domain identifier. Excluded from parameter dumps.

window_size

int | None

None

Number of most-recent steps to average. None uses the full lookback window. Must be a positive integer when specified.

search_space

BaseSearchSpace

(see below)

Default hyperparameter search space. Excluded from parameter dumps.

Default Search Space:

Parameter

Values

Type

window_size

[4, 8, 12, 24, 48, 96, 168]

categorical

Predict Logic#

w = window_size or seq_len          # None → full window
window = x[:, -w:, :num_targets]    # (B, w, num_targets)
avg = window.mean(axis=1)           # (B, num_targets)
preds = broadcast(avg, (B, horizon, num_targets))

All forecast horizon steps receive the same local mean. fit() validates that window_size <= seq_len when specified.

Choosing window_size

The default search space covers common hourly and sub-hourly granularities: 4h, 8h, 12h, 24h (daily), 48h, 96h, and 168h (weekly). Use Optuna HPO with tune() to select the best window_size for your target signal. For trending signals consider DRIFTModel instead, which captures the local slope.

API Reference#

class twiga.models.baseline.window_average_model.WINDOWAVERAGEModel(model_config=None, *, num_targets=None, horizon=None)#

Bases: BaseRegressor

Window-average baseline for multi-horizon forecasting.

Predicts the local mean of the last window_size observed target values in the input context for all forecast horizon steps. The mean adapts to the current level of each window, unlike a global training mean.

Parameters:

model_config (WINDOWAVERAGEConfig | None) – Configuration object. Defaults to WINDOWAVERAGEConfig (full-window average).

Example:

# Average over the last 24 steps (1 day at hourly resolution)
model = WINDOWAVERAGEModel(WINDOWAVERAGEConfig(window_size=24))
model.fit(X_train, y_train)
preds = model.predict(X_test)  # (B, L, H)
fit(X, y, eval_set=None, verbose=False, trial=None)#

Store target shape information.

Parameters:
  • X (ndarray) – Shape (B, L, F).

  • y (ndarray) – Shape (B, L, H).

  • eval_set (tuple[ndarray, ndarray] | None) – Accepted for API compatibility; silently ignored.

  • verbose (bool) – Unused; included for interface consistency.

  • trial (Any | None) – Optuna trial passed through from the forecaster during HPO (accepted but not used).

Return type:

WINDOWAVERAGEModel

Returns:

Self for method chaining.

Raises:

ValueError – If X or y are not 3-dimensional, or if window_size exceeds the sequence length L.

forecast(x)#

Return window-average point forecast (raw array).

Return type:

ndarray

property min_seq_len: int#

Minimum lookback window length required by the configured window_size.

Returns window_size when set, or 1 when window_size=None (full-window average adapts to any L).

Example:

model = WINDOWAVERAGEModel(WINDOWAVERAGEConfig(window_size=168))
assert pipeline_config.seq_len >= model.min_seq_len
predict(x)#

Return window-average predictions.

fit() is not required when horizon and num_targets are set at construction time.

Parameters:

x (ndarray) – Shape (B, L, F).

Return type:

ndarray

Returns:

Predictions of shape (B, horizon, num_targets). All horizon steps receive the same local mean value.

Raises:

ValueError – If the model has not been fitted and no horizon hint was provided, or if x is not 3-D.

set_fit_request(*, eval_set='$UNCHANGED$', trial='$UNCHANGED$', verbose='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the fit method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

eval_setstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for eval_set parameter in fit.

trialstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for trial parameter in fit.

verbosestr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for verbose parameter in fit.

Returns#

selfobject

The updated object.

set_predict_request(*, x='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

xstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for x parameter in predict.

Returns#

selfobject

The updated object.

set_score_request(*, sample_weight='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

sample_weightstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for sample_weight parameter in score.

Returns#

selfobject

The updated object.

update(trial)#

Rebuild config from an Optuna trial’s suggested window_size.

Return type:

None

DRIFTModel#

DRIFTModel implements the random walk with drift forecast. It computes the per-window linear slope from the first to the last observed value in the input context, then extrapolates that slope forward for all forecast horizon steps.

Formula#

For each sample b and target t:

slope   = (x[b, -1, t] − x[b, 0, t]) / max(L − 1, 1)
pred[b, h, t] = x[b, -1, t] + (h + 1) * slope    for h = 0, …, H−1

When L = 1, the denominator max(L 1, 1) = 1, making the slope zero, and the model falls back to pure persistence: ŷ_{t+h} = y_t for all h.

DRIFTConfig#

Field

Type

Default

Description

name

Literal["drift"]

"drift"

Model identifier. Excluded from parameter dumps.

domain

Literal["baseline"]

"baseline"

Domain identifier. Excluded from parameter dumps.

search_space

BaseSearchSpace

(empty)

Empty search space - drift has no tunable hyperparameters.

DRIFTModel.update(trial) is a no-op: it calls get_optuna_params to satisfy the interface but makes no config changes.

Predict Logic#

last  = x[:, -1, :num_targets]          # (B, num_targets)
first = x[:,  0, :num_targets]          # (B, num_targets)
slope = (last - first) / max(L - 1, 1)  # (B, num_targets)
steps = np.arange(1, horizon + 1)        # (horizon,)

# Vectorised outer product
preds = last[:, np.newaxis, :] + steps[np.newaxis, :, np.newaxis] * slope[:, np.newaxis, :]
# shape: (B, horizon, num_targets)
        flowchart LR
    X["x&#58; (B, L, F)"] --> FIRST["first = x[:,0,:T]"]
    X --> LAST["last = x[:,-1,:T]"]
    FIRST --> SLOPE["slope = (last - first) / max(L-1,1)"]
    LAST --> SLOPE
    SLOPE --> PRED["pred[b,h,t] = last + (h+1) * slope"]
    LAST --> PRED
    

Drift vs. persistence

DRIFTModel is most valuable when the target signal has a consistent local trend within the lookback window (e.g. a ramp-up at dawn for solar irradiance, or a weekday morning load rise). When the signal is mean-reverting or stationary, the drift extrapolation will overshoot, and NAIVEModel or WINDOWAVERAGEModel will typically be more accurate.

API Reference#

class twiga.models.baseline.drift_model.DRIFTModel(model_config=None, *, num_targets=None, horizon=None)#

Bases: BaseRegressor

Drift (random walk with drift) baseline for multi-horizon forecasting.

Computes the per-window linear slope from the first to the last observed target value in the input context, then extrapolates it forward for all forecast horizon steps.

This is equivalent to StatsForecast’s RandomWalkWithDrift, applied to each sliding window independently.

Parameters:

model_config (DRIFTConfig | None) – Configuration object. Defaults to DRIFTConfig.

Example:

model = DRIFTModel()
model.fit(X_train, y_train)
preds = model.predict(X_test)  # (B, L, H)
fit(X, y, eval_set=None, verbose=False, trial=None)#

Store target shape information.

Parameters:
  • X (ndarray) – Shape (B, L, F).

  • y (ndarray) – Shape (B, L, H).

  • eval_set (tuple[ndarray, ndarray] | None) – Accepted for API compatibility; silently ignored.

  • verbose (bool) – Unused; included for interface consistency.

  • trial (Any | None) – Optuna trial passed through from the forecaster during HPO (accepted but not used).

Return type:

DRIFTModel

Returns:

Self for method chaining.

Raises:

ValueError – If X or y are not 3-dimensional.

forecast(x)#

Return drift point forecast (raw array).

Return type:

ndarray

property min_seq_len: int#

Minimum lookback window length for a meaningful drift estimate.

Returns 2 - the model requires at least a first and a last observation to compute a slope. L=1 is handled gracefully (slope falls back to zero / pure persistence), but min_seq_len reflects the minimum for non-degenerate behaviour.

predict(x)#

Return drift predictions.

fit() is not required when horizon and num_targets are set at construction time.

For each sample b and target t:

  • last  = x[b, -1, t]

  • first = x[b,  0, t]

  • drift = (last first) / max(L 1, 1)

  • pred[b, h, t] = last + (h + 1) · drift

Parameters:

x (ndarray) – Shape (B, L, F).

Return type:

ndarray

Returns:

Predictions of shape (B, horizon, num_targets).

Raises:

ValueError – If no horizon hint was provided and fit() was not called, or if x is not 3-D.

set_fit_request(*, eval_set='$UNCHANGED$', trial='$UNCHANGED$', verbose='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the fit method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

eval_setstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for eval_set parameter in fit.

trialstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for trial parameter in fit.

verbosestr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for verbose parameter in fit.

Returns#

selfobject

The updated object.

set_predict_request(*, x='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

xstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for x parameter in predict.

Returns#

selfobject

The updated object.

set_score_request(*, sample_weight='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

sample_weightstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for sample_weight parameter in score.

Returns#

selfobject

The updated object.

update(trial)#

No-op: drift has no tunable hyperparameters.

Return type:

None

CONTEXTPARROTModel#

CONTEXTPARROTModel implements non-parametric forecasting through delay-coordinate embedding. It searches for patterns in the input window similar to recent observations, then uses the continuation of those patterns as forecasts. Two strategies are available: parrot (motif-following) and simplex (autoregressive k-NN).

Unlike the simpler baselines above, Context Parrot adapts to complex recurring patterns in the time series without learning parameters, making it a bridge between classical statistics and machine learning.

Delay-Coordinate Embedding#

Both strategies rely on delay-coordinate embedding: reconstructing the state of a dynamical system from a univariate time series using a sliding window of lagged observations.

For a series x and embedding parameters D (dimension) and τ (delay/lag), the embedding creates vectors:

[x_t, x_{t+τ}, x_{t+2τ}, ..., x_{t+(D-1)τ}]

This representation captures repeating patterns and is the foundation for motif matching.

Parrot Method#

The “parrot” method (default) implements motif-following:

  1. Embed the query (most recent D lagged observations) into the delay-coordinate space.

  2. Find the best-matching historical motif (highest-dimensional nearest neighbor) using Euclidean distance.

  3. Return the continuation of that historical pattern as the forecast.

Aspect

Description

Matching

Finds the closest historical embedding to the current state.

Forecast

Returns the next horizon steps that followed that historical state, repeating if necessary.

Adapts to

Recurring patterns, cycles, and repeating events.

Fails on

Unique or unprecedented states not seen in the training window.

Simplex Method#

The “simplex” method implements autoregressive k-NN Sugihara-style projection:

  1. Construct a library of embedded state vectors and their one-step-ahead targets.

  2. For each forecast step, find the k nearest neighbors in embedding space.

  3. Predict the next value as an exponentially-weighted average of the k targets, weighting by inverse distance.

  4. Append the prediction to the series history and iterate for the full horizon.

Aspect

Description

Matching

Weighted ensemble of k nearest neighbors per step.

Forecast

Autoregressive: each prediction feeds into the next step’s state.

Adapts to

Smooth continuations; better for extrapolation beyond the training window.

Fails on

High-dimensional data; may struggle with chaotic or unpredictable signals.

CONTEXTPARROTConfig#

Field

Type

Default

Description

name

Literal["context_parrot"]

"context_parrot"

Model identifier. Excluded from parameter dumps.

domain

Literal["baseline"]

"baseline"

Domain identifier. Excluded from parameter dumps.

embedding_dim

int

10

Delay embedding dimension D (motif length). Must be positive.

tau

int

1

Delay (lag) between embedding coordinates. Must be positive.

method

Literal["parrot", "simplex"]

"parrot"

Forecasting strategy.

k

int | None

None

Number of nearest neighbors for simplex. Defaults to embedding_dim + 1 when None. Ignored for parrot.

search_space

BaseSearchSpace

(see below)

Default hyperparameter search space. Excluded from parameter dumps.

Default Search Space:

Parameter

Values

Type

embedding_dim

[5, 10, 24, 48, 96]

categorical

Sequence Length Requirement#

The model requires a minimum input sequence length:

seq_len >= 2 * embedding_dim + 1

This ensures enough data points to construct embedding_dim-dimensional embeddings with sufficient candidates for motif matching. fit() validates this constraint and raises ValueError if violated.

Example: Parrot Method#

from twiga.models.baseline.context_parrot_model import CONTEXTPARROTModel, CONTEXTPARROTConfig

# Default parrot method with embedding dimension 10
config = CONTEXTPARROTConfig(embedding_dim=10, method="parrot")
model = CONTEXTPARROTModel(model_config=config)

model.fit(X_train, y_train)  # X_train shape: (B, L, F), L >= 21
preds = model.predict(X_test)  # shape: (B, H, num_targets)

Example: Simplex Method#

# Autoregressive simplex with k=8 neighbors
config = CONTEXTPARROTConfig(
    embedding_dim=10,
    tau=1,
    method="simplex",
    k=8,  # or None to default to embedding_dim + 1
)
model = CONTEXTPARROTModel(model_config=config)

model.fit(X_train, y_train)
preds = model.predict(X_test)

Hyperparameter Tuning#

The default search space covers embedding dimensions from 5 to 96. Use tune() with Optuna to select the best embedding dimension for your signal:

forecaster = TwigaForecaster(
    data_params=data_config,
    model_params=[CONTEXTPARROTConfig()],
    cv_params=train_config,
)

forecaster.fit(train_df=train_df)
forecaster.tune(train_df=train_df, val_df=val_df, num_trials=5)  # tuning embedding_dim

API Reference#

class twiga.models.baseline.context_parrot_model.CONTEXTPARROTModel(model_config=None, *, num_targets=None, horizon=None)#

Bases: BaseEstimator, RegressorMixin

Context-parroting forecaster supporting multiple target columns.

Implements two non-parametric forecasting strategies based on delay- coordinate embedding of the input window. Each target column is processed independently using the selected strategy.

The first num_targets columns of x are treated as the target series. The model has no trainable parameters; fit only validates shape and stores metadata.

Parameters:

model_config (CONTEXTPARROTConfig | None) – Configuration object. Defaults to CONTEXTPARROTConfig (strategy "parrot").

Example:

model = CONTEXTPARROTModel()
model.fit(X_train, y_train)
preds = model.predict(X_test)  # shape (B, H, num_targets)
fit(X, y, eval_set=None, verbose=False, trial=None)#

Validate input shape and store metadata.

Parameters:
  • X (ndarray) – Shape (B, L, F) - lookback features; first T columns are the target series used for matching.

  • y (ndarray) – Shape (B, H, T) - targets used to determine horizon and T.

  • eval_set (tuple[ndarray, ndarray] | None) – Ignored; accepted for interface compatibility.

  • verbose (bool) – Ignored.

  • trial (Any | None) – Optuna trial passed through from the forecaster during HPO (accepted but not used).

Return type:

CONTEXTPARROTModel

Returns:

Self.

forecast(x)#

Alias for predict.

Return type:

ndarray

property min_seq_len: int#

Minimum lookback window length required.

Derived from the constraint that n_candidates = L - 2*D >= 1, which gives L >= 2*D + 1.

predict(x)#

Forecast using the configured method ('parrot' or 'simplex').

fit() is not required when horizon and num_targets are set at construction time.

Parameters:

x (ndarray) – Shape (B, L, F). The first num_targets columns are the target series used for pattern matching.

Return type:

ndarray

Returns:

Predictions of shape (B, H, num_targets).

Raises:

ValueError – If no horizon hint was provided and fit() was not called, or if the sequence length is too short.

set_fit_request(*, eval_set='$UNCHANGED$', trial='$UNCHANGED$', verbose='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the fit method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

eval_setstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for eval_set parameter in fit.

trialstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for trial parameter in fit.

verbosestr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for verbose parameter in fit.

Returns#

selfobject

The updated object.

set_predict_request(*, x='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the predict method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to predict.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

xstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for x parameter in predict.

Returns#

selfobject

The updated object.

set_score_request(*, sample_weight='$UNCHANGED$')#

Configure whether metadata should be requested to be passed to the score method.

Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with enable_metadata_routing=True (see sklearn.set_config()). Please check the User Guide on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to score if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to score.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version 1.3.

Parameters#

sample_weightstr, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED

Metadata routing for sample_weight parameter in score.

Returns#

selfobject

The updated object.

update(trial)#

Support HPO for embedding_dim.

Return type:

None

Model Comparison#

Model

Config Class

Hyperparameters

Adapts to window

Typical use

NAIVEModel

NAIVEConfig

strategy (categorical)

Only "window_last"

Universal persistence benchmark

SEASONALNAIVEModel

SEASONALNAIVEConfig

period (categorical)

Yes (per-window seasonal lookup)

Signals with strong daily/weekly periodicity

WINDOWAVERAGEModel

WINDOWAVERAGEConfig

window_size (categorical)

Yes (local mean)

Stationary signals with variable level

DRIFTModel

DRIFTConfig

None

Yes (per-window slope)

Trending signals, ramp events

CONTEXTPARROTModel

CONTEXTPARROTConfig

embedding_dim (categorical)

Yes (motif or simplex)

Recurring patterns, cycles

Usage Example#

The following example shows the complete baseline workflow: train multiple baselines alongside an ML model, evaluate all of them, then compute skill scores.

import pandas as pd

from twiga.core.config import DataPipelineConfig, ExperimentConfig
from twiga.forecaster.core import TwigaForecaster
from twiga.models.baseline.naive_model import NAIVEConfig
from twiga.models.baseline.seasonal_naive_model import SEASONALNAIVEConfig
from twiga.models.baseline.window_average_model import WINDOWAVERAGEConfig
from twiga.models.baseline.drift_model import DRIFTConfig
from twiga.models.ml.lightgbm_model import LIGHTGBMConfig

# --- Data pipeline ---
data_config = DataPipelineConfig(
    target_feature="load_mw",
    period="1h",
    lookback_window_size=168,   # 7 days at hourly resolution
    forecast_horizon=24,
    calendar_features=["hour", "dayofweek", "month"],
    input_scaler="standard",
)

# --- Baseline configs ---
naive_config = NAIVEConfig(strategy="window_last")
seasonal_daily = SEASONALNAIVEConfig(period="1D", freq="1h")
seasonal_weekly = SEASONALNAIVEConfig(period="7D", freq="1h")
window_avg_config = WINDOWAVERAGEConfig(window_size=24)
drift_config = DRIFTConfig()

# --- ML model for comparison ---
lgbm_config = LIGHTGBMConfig(boosting_type="gbdt")

# --- Training orchestration ---
train_config = ExperimentConfig(
    split_freq="months",
    train_size=6,
    test_size=1,
    window="expanding",
    project_name="BaselineBenchmark",
    seed=42,
)

# --- Build forecaster with all models ---
forecaster = TwigaForecaster(
    data_params=data_config,
    model_params=[
        naive_config,
        seasonal_daily,
        seasonal_weekly,
        window_avg_config,
        drift_config,
        lgbm_config,
    ],
    cv_params=train_config,
)

data = pd.read_parquet("data/load.parquet")
train_df = data[data.timestamp <= "2024-06-01"]
test_df  = data[data.timestamp > "2024-06-01"]

forecaster.fit(train_df=train_df)
results_df, metrics_df = forecaster.evaluate_point_forecast(test_df=test_df)

print(metrics_df)

Tuning Baseline Hyperparameters#

Baselines with search spaces (NAIVEModel, SEASONALNAIVEModel, WINDOWAVERAGEModel) support Optuna-based HPO via tune(). This is useful for selecting the best seasonal period or window size without manual trial-and-error.

# Tune the window size for WINDOWAVERAGEModel
window_forecaster = TwigaForecaster(
    data_params=data_config,
    model_params=[WINDOWAVERAGEConfig()],
    cv_params=train_config,
)
window_forecaster.fit(train_df=train_df)
window_forecaster.tune(train_df=train_df, val_df=test_df, num_trials=7)

# Retrain with best window_size
window_forecaster.fit(train_df=train_df)
results_df, metrics_df = window_forecaster.evaluate_point_forecast(test_df=test_df)

Since WINDOWAVERAGEConfig’s default search space has 7 candidates, a full grid search requires only 7 trials.

Skill Score Section#

A skill score measures the relative improvement of a learned model over a naive reference. The standard formulation is:

SS = 1 - (error_model / error_baseline)

Where error is any positively-oriented error metric (MAE, RMSE, MAPE, etc.). A skill score of 0 means the model performs identically to the baseline; 1.0 is a perfect forecast; negative values indicate the model is worse than the baseline.

Computing Skill Scores from Twiga Metrics#

import pandas as pd

# Evaluate all models
results_df, metrics_df = forecaster.evaluate_point_forecast(test_df=test_df)

# metrics_df has index=model_name, columns include "mae", "rmse", etc.
baseline_mae = metrics_df.loc["naive", "mae"]

# Skill score relative to naive persistence
metrics_df["skill_score_mae"] = 1 - (metrics_df["mae"] / baseline_mae)

print(metrics_df[["mae", "skill_score_mae"]].sort_values("skill_score_mae", ascending=False))

Choosing the Reference Baseline#

Signal type

Recommended reference

Load (AC or heating)

SEASONALNAIVEModel(period="1D") or period="7D"

Solar PV / irradiance

NAIVEModel(strategy="window_last") (persistence)

Wind power

NAIVEModel(strategy="window_last")

Price (day-ahead)

SEASONALNAIVEModel(period="1D")

Differenced / centred series

NAIVEModel(strategy="zero")

Reporting skill scores

When publishing or reporting forecast results, always state which baseline was used as the reference and over which evaluation period the skill score was computed. A skill score computed against a weak baseline (e.g. "zero") can be misleadingly high; prefer the strongest relevant naive model as the denominator.

See Also#