Skip to content

ForecastingAssistant

skforecast_ai.assistant.ForecastingAssistant

ForecastingAssistant(
    llm=None,
    base_url=None,
    api_key=None,
    send_data_to_llm=False,
)

AI-powered forecasting assistant built on skforecast.

Analyses a time series dataset, selects a forecaster and estimator, produces a ready-to-run Python script, and optionally executes it, returning predictions, metrics, and the exact code that generated them.

All modeling decisions are deterministic and reproducible. An optional LLM adds natural-language explanations and Q&A without influencing any recommendation.

Parameters:

Name Type Description Default
llm str

LLM provider string in format 'provider:model_name'. If None, only deterministic methods are available.

None
base_url str

Custom base URL for the LLM provider (used for Ollama or OpenAI-compatible endpoints).

None
api_key str

Explicit API key for the LLM provider. When None, Pydantic AI resolves credentials from environment variables (e.g. OPENAI_API_KEY, GOOGLE_API_KEY). Use this for notebook workflows or multi-tenant scenarios.

None
send_data_to_llm bool

Whether raw values from the dataset supplied to a method (for example via data) may be sent to the LLM. When False, only metadata (schema, summary stats) is shared with the LLM. This governs input data only: a result passed to ask() carries its own predictions, which are always included so the LLM can discuss specific forecast values.

False

Attributes:

Name Type Description
llm (str, None)

LLM provider string or None for deterministic-only mode.

base_url (str, None)

Custom base URL for the LLM provider.

api_key (str, None)

Explicit API key or None (resolve from environment).

send_data_to_llm bool

Whether raw data may be sent to the LLM.

Notes

Four workflows are available:

Fast path: call a single method that handles everything internally:

  • forecast_code() profiles the data, builds a plan, and returns a ready-to-run Python script.
  • forecast() does the same and also executes the forecast, returning predictions and metrics.

Step-by-step path: for full control over each stage:

  • profile() inspects the dataset and selects the recommended forecaster and estimator (with alternative candidates).
  • plan() takes the profile and derives the detailed configuration (lags, metric, preprocessing, intervals, NaN handling).
  • refine_plan() adjusts an existing plan with user overrides, or with LLM guidance when a prompt is provided (if desired).
  • forecast_code() generates a Python script (accepts pre-computed profile and plan for full control).
  • forecast() executes the workflow from a pre-computed profile and plan, returning predictions and metrics.

Backtesting path: evaluate model performance with time series cross-validation:

  • create_cv() produces a TimeSeriesFold with smart defaults (optionally guided by an LLM prompt). Requires a profile and plan.
  • backtest_code() generates a backtesting script without executing it (accepts pre-computed profile and plan).
  • backtest() runs backtesting using the CV strategy and returns metrics, predictions, and reproducible code.

Comparison path: evaluate several configurations side by side:

  • compare() backtests a set of candidate configurations with the same cross-validation strategy and returns a metric-ranked leaderboard plus the winning configuration as a reusable BacktestResult.

ask() is an LLM-powered method (requires llm to be configured) available in any workflow to explain results, answer forecasting questions, or interpret metrics.

Methods:

Name Description
profile

Profile a dataset and select the recommended forecaster and estimator.

plan

Build a detailed ForecastPlan from a ForecastingProfile.

refine_plan

Re-derive a forecast plan applying user overrides or LLM guidance.

forecast_code

Profile, plan, and generate a complete forecasting script.

forecast

Execute a full forecasting workflow end-to-end.

create_cv

Generate a time series cross-validation strategy for backtesting.

backtest_code

Profile, plan, and generate a complete backtesting script.

backtest

Execute backtesting with a pre-configured time series cross-validation

compare

Compare several forecaster configurations on the same data.

ask

Ask a forecasting question or explain a pre-computed plan.

Source code in skforecast_ai/assistant.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def __init__(
    self,
    llm: str | None = None,
    base_url: str | None = None,
    api_key: str | None = None,
    send_data_to_llm: bool = False,
) -> None:

    self.llm              = llm
    self.base_url         = base_url
    self.api_key          = api_key
    self.send_data_to_llm = send_data_to_llm
    self._model           = None
    self._agent           = None
    self._cv_agent        = None
    self._plan_refinement_agent = None

Attributes

llm instance-attribute

llm = llm

base_url instance-attribute

base_url = base_url

api_key instance-attribute

api_key = api_key

send_data_to_llm instance-attribute

send_data_to_llm = send_data_to_llm

_model instance-attribute

_model = None

_agent instance-attribute

_agent = None

_cv_agent instance-attribute

_cv_agent = None

_plan_refinement_agent instance-attribute

_plan_refinement_agent = None

Functions

profile

profile(
    data,
    target=None,
    date_column=None,
    series_id_column=None,
)

Profile a dataset and select the recommended forecaster and estimator.

Assembles the data profile and selects the recommended forecaster, estimator, and their compatible candidates. The returned ForecastingProfile carries the DataProfile plus the coarse modeling decisions.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. When a pandas Series is passed, the target is derived from its name.

required
target str, list of str

Name of the column to forecast. For wide-format multi-series, pass a list of column names where each column is a series. Optional only when data is a pandas Series, in which case the Series name is used (or 'y' when the Series has no name).

None
date_column str

Name of the column containing timestamps.

None
series_id_column str

Name of the column identifying individual series.

None

Returns:

Name Type Description
profile ForecastingProfile

Dataset profile + recommended forecaster + estimator (with alternative candidates) + analysis context.

Source code in skforecast_ai/assistant.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def profile(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
) -> ForecastingProfile:
    """
    Profile a dataset and select the recommended forecaster and estimator.

    Assembles the data profile and selects the recommended forecaster,
    estimator, and their compatible candidates. The returned
    `ForecastingProfile` carries the `DataProfile` plus the coarse
    modeling decisions.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file. When a
        pandas Series is passed, the target is derived from its name.
    target : str, list of str, default None
        Name of the column to forecast. For wide-format multi-series,
        pass a list of column names where each column is a series.
        Optional only when `data` is a pandas Series, in which case the
        Series name is used (or `'y'` when the Series has no name).
    date_column : str, default None
        Name of the column containing timestamps.
    series_id_column : str, default None
        Name of the column identifying individual series.

    Returns
    -------
    profile : ForecastingProfile
        Dataset profile + recommended forecaster + estimator
        (with alternative candidates) + analysis context.
    """

    data_path = str(data) if isinstance(data, (str, Path)) else "data.csv"
    data, target = _resolve_data_and_target(data, target)

    data_profile = create_data_profile(
        data             = data,
        target           = target,
        date_column      = date_column,
        series_id_column = series_id_column,
        data_path        = data_path,
    )

    forecaster, forecaster_candidates = select_forecaster_and_candidates(data_profile)
    task_type = select_task_type_from_forecaster(forecaster)

    series_pacf = compute_series_pacf(data=data, profile=data_profile)
    window_features = select_window_features(
                          task_type      = task_type,
                          n_observations = data_profile.span_index_length,
                          frequency      = data_profile.frequency,
                      )

    calendar_features = select_calendar_features(
                            task_type      = task_type,
                            frequency      = data_profile.frequency,
                            n_observations = data_profile.span_index_length,
                        )

    estimator, estimator_candidates = select_estimator_and_candidates(
        task_type=task_type, n_observations=data_profile.n_total_observations
    )

    explanation = _build_profile_explanation(
        task_type             = task_type,
        forecaster            = forecaster,
        forecaster_candidates = forecaster_candidates,
        estimator             = estimator,
        estimator_candidates  = estimator_candidates,
        data_profile          = data_profile,
    )

    return ForecastingProfile(
        data_profile          = data_profile,
        task_type             = task_type,
        forecaster            = forecaster,
        forecaster_candidates = forecaster_candidates,
        estimator             = estimator,
        estimator_candidates  = estimator_candidates,
        series_pacf           = series_pacf,
        window_features       = window_features,
        calendar_features     = calendar_features,
        explanation           = explanation,
    )

plan

plan(
    profile,
    steps,
    interval=None,
    forecaster=None,
    estimator=None,
    estimator_kwargs=None,
    lags=None,
    window_features=None,
)

Build a detailed ForecastPlan from a ForecastingProfile.

Performs the fine-grained configuration (lags, prediction intervals, NaN handling, exogenous usage, preprocessing steps) without re-evaluating the coarse decisions already encoded in profile.

Parameters:

Name Type Description Default
profile ForecastingProfile

Output of profile().

required
steps int

Forecast horizon (number of steps ahead to predict).

required
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] (e.g. [0.1, 0.9] for 80 % interval). If None, no prediction intervals are computed.

None
forecaster str

Explicit forecaster class name to override the profile recommendation. When it is not in profile.forecaster_candidates but is a supported forecaster, it is used anyway and an UnrecommendedForecasterWarning is issued.

None
estimator str

Explicit estimator class name to override the profile recommendation (e.g. 'HistGradientBoostingRegressor').

None
estimator_kwargs dict

Keyword arguments for the estimator constructor (e.g. {'n_estimators': 200, 'learning_rate': 0.05}). Merged on top of built-in defaults (random_state, silencing flags). User values take precedence.

None
lags int, list of int

Explicit lag configuration. If provided, bypasses the deterministic PACF-based lag selection.

None
window_features list of dict

Explicit window (rolling) features configuration. Each dict must contain the keys 'stats' (a list of rolling statistics) and 'window_size' (a scalar int applied to every stat in that same dict), for example [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], 'window_size': 24}, {'stats': ['mean'], 'window_size': 168}]. To combine several window sizes, add one dict per size. Allowed stats are 'mean', 'std', 'min', 'max', 'sum', 'median', 'ratio_min_max', 'coef_variation', and 'ewm'. If provided, bypasses the deterministic window feature selection. deterministic window feature selection.

None

Returns:

Name Type Description
plan ForecastPlan

Detailed forecasting plan.

Source code in skforecast_ai/assistant.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def plan(
    self,
    profile: ForecastingProfile,
    steps: int,
    interval: list[float] | None = None,
    forecaster: str | None = None,
    estimator: str | None = None,
    estimator_kwargs: dict | None = None,
    lags: int | list[int] | None = None,
    window_features: list[dict[str, list[str] | int]] | None = None,
) -> ForecastPlan:
    """
    Build a detailed `ForecastPlan` from a `ForecastingProfile`.

    Performs the fine-grained configuration (lags, prediction
    intervals, NaN handling, exogenous usage, preprocessing steps)
    without re-evaluating the coarse decisions already encoded in
    `profile`.

    Parameters
    ----------
    profile : ForecastingProfile
        Output of `profile()`.
    steps : int
        Forecast horizon (number of steps ahead to predict).
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` (e.g. `[0.1, 0.9]` for 80 % interval). If
        None, no prediction intervals are computed.
    forecaster : str, default None
        Explicit forecaster class name to override the profile
        recommendation. When it is not in
        `profile.forecaster_candidates` but is a supported
        forecaster, it is used anyway and an
        `UnrecommendedForecasterWarning` is issued.
    estimator : str, default None
        Explicit estimator class name to override the profile
        recommendation (e.g. `'HistGradientBoostingRegressor'`).
    estimator_kwargs : dict, default None
        Keyword arguments for the estimator constructor (e.g.
        `{'n_estimators': 200, 'learning_rate': 0.05}`). Merged
        on top of built-in defaults (`random_state`, silencing
        flags). User values take precedence.
    lags : int, list of int, default None
        Explicit lag configuration. If provided, bypasses the
        deterministic PACF-based lag selection.
    window_features : list of dict, default None
        Explicit window (rolling) features configuration. Each dict
        must contain the keys `'stats'` (a list of rolling statistics)
        and `'window_size'` (a scalar int applied to every stat in
        that same dict), for example `[{'stats': ['mean', 'std'],
        'window_size': 3}, {'stats': ['mean'], 'window_size': 24},
        {'stats': ['mean'], 'window_size': 168}]`. To combine several
        window sizes, add one dict per size. Allowed stats are
        `'mean'`, `'std'`, `'min'`, `'max'`, `'sum'`, `'median'`,
        `'ratio_min_max'`, `'coef_variation'`, and `'ewm'`. If
        provided, bypasses the deterministic window feature selection.
        deterministic window feature selection.

    Returns
    -------
    plan : ForecastPlan
        Detailed forecasting plan.
    """

    data_profile = profile.data_profile

    fc = profile.forecaster
    if forecaster is not None:
        if forecaster not in profile.forecaster_candidates:
            if forecaster not in FORECASTER_TASK_TYPES:
                raise ValueError(
                    f"Forecaster '{forecaster}' is not compatible with this "
                    f"profile. Available candidates: "
                    f"{profile.forecaster_candidates}."
                )
            warnings.warn(
                f"Forecaster '{forecaster}' is not among the recommended "
                f"candidates for this profile "
                f"({profile.forecaster_candidates}), but it is used as "
                f"requested. It may be slow or perform poorly on this data.",
                UnrecommendedForecasterWarning,
            )
        fc = forecaster

    task_type = select_task_type_from_forecaster(fc)

    # Reject inputs incompatible with the resolved task type
    # (single-series tasks with multi-series input; multivariate with
    # series of different lengths).
    _validate_task_input(data_profile, task_type)

    n_obs_total = data_profile.n_total_observations

    # Recompute the estimator only when the task type changed.
    if task_type != profile.task_type:
        est, _ = select_estimator_and_candidates(
            task_type      = task_type,
            n_observations = n_obs_total,
        )
    else:
        est = profile.estimator

    if estimator is not None:
        est = estimator

    if task_type in ("statistical", "foundation"):
        final_lags = None
        final_window_features = None
        transformer_series = None
        transformer_exog = None
        dropna_from_series = None
        calendar_features = None
    else:
        # Explicit lag/window overrides (manual or LLM-supplied) bypass the
        # deterministic PACF selection and its budget guard, so validate
        # them against the data budget before building the forecaster.
        if window_features is not None:
            _validate_window_features(window_features)

        if lags is not None or window_features is not None:
            _validate_max_window_size(
                lags              = lags,
                window_features   = window_features,
                span_index_length = data_profile.span_index_length
            )

        if lags is not None:
            final_lags = lags
        else:
            # Direct forecasters lose `steps - 1` extra rows beyond the
            # `window_size` (the last-step regressor needs the target at
            # t + steps), so reserve them from the lag budget. Recursive
            # forecasters reserve nothing.
            n_reserved_rows = steps - 1 if "Direct" in fc else 0
            final_lags = finalize_lags(
                series_pacf     = profile.series_pacf,
                task_type       = task_type,
                n_observations  = data_profile.span_index_length,
                frequency       = data_profile.frequency,
                n_reserved_rows = n_reserved_rows,
            )

        if window_features is not None:
            final_window_features = window_features
        else:
            final_window_features = profile.window_features

        if profile.calendar_features:
            calendar_features = {
                "features": profile.calendar_features,
                "encoding": select_calendar_encoding(est, task_type),
            }
        else:
            calendar_features = None

        transformer_series = select_transformer_series(est, task_type)

        transformer_exog = select_transformer_exog(
            estimator        = est,
            task_type        = task_type,
            exog_columns     = data_profile.exog_columns,
            categorical_exog = data_profile.categorical_exog,
        )

        dropna_from_series = select_dropna_from_series(
            estimator        = est,
            missing_target   = data_profile.missing_target,
            missing_exog     = data_profile.missing_exog,
            task_type        = task_type,
        )

    forecaster_kwargs = build_forecaster_kwargs(
        forecaster         = fc,
        task_type          = task_type,
        steps              = steps,
        lags               = final_lags,
        window_features    = final_window_features,
        calendar_features  = calendar_features,
        transformer_series = transformer_series,
        transformer_exog   = transformer_exog,
        dropna_from_series = dropna_from_series
    )

    interval_method = None
    if interval is not None:
        if task_type in {"statistical", "foundation"}:
            interval_method = "native"
        else:
            interval_method = "bootstrapping"

    use_exog = check_exog_usage(data_profile.exog_columns)

    preprocessing_steps = derive_preprocessing_steps(data_profile, fc)

    metric, metric_explanation, metrics_to_compute = select_metric(
        data_profile = data_profile,
    )

    explanation = build_plan_explanation(
        forecaster         = fc,
        estimator          = est,
        lags               = final_lags,
        window_features    = final_window_features,
        interval_method    = interval_method,
        dropna_from_series = dropna_from_series,
        use_exog           = use_exog,
        metric_explanation = metric_explanation,
        calendar_features  = calendar_features,
        task_type          = task_type,
    )

    return ForecastPlan(
        task_type           = task_type,
        forecaster          = fc,
        forecaster_kwargs   = forecaster_kwargs,
        estimator           = est,
        estimator_kwargs    = estimator_kwargs or {},
        steps               = steps,
        frequency           = data_profile.frequency,
        interval            = interval,
        interval_method     = interval_method,
        metric              = metric,
        metrics_to_compute  = metrics_to_compute,
        use_exog            = use_exog,
        preprocessing_steps = preprocessing_steps,
        explanation         = explanation,
    )

refine_plan

refine_plan(profile, plan, prompt=None, **overrides)

Re-derive a forecast plan applying user overrides or LLM guidance.

Operates in two modes:

  • Deterministic mode (prompt=None): takes an existing plan and a set of overrides, then calls plan() with the merged parameters. Only the overridden fields change; everything else is re-derived deterministically from the original profile.
  • LLM mode (prompt provided): a specialized agent interprets the natural-language domain knowledge and suggests lags and window_features, which are merged on top of the deterministic plan. The agent's reasoning is appended to the returned plan's explanation.

Supported overrides: forecaster, estimator, estimator_kwargs, steps, interval, lags, window_features.

Note that lags and window_features default to the values already stored in plan.forecaster_kwargs, so refining an unrelated field (e.g. steps) preserves the existing features rather than re-running the PACF-based selection.

In LLM mode, explicit lags/window_features overrides take precedence over the LLM suggestion (a UserWarning is emitted for each shadowed field, and a note recording the overridden field(s) is appended to the explanation). When both are supplied explicitly, the LLM has nothing left to decide and is not called. LLM mode does not apply to task_type in ('statistical', 'foundation'), which do not use lags or window features; the prompt is ignored with a UserWarning. When the agent omits a field, or the LLM call fails or its suggestion cannot satisfy the data budget, that field keeps the plan's existing value (a UserWarning is emitted on failure).

Parameters:

Name Type Description Default
profile ForecastingProfile

Original profile that produced the plan.

required
plan ForecastPlan

Existing plan to refine.

required
prompt str

Natural-language domain knowledge used to guide LLM refinement of lags and window_features. When None, only the explicit overrides are applied. Requires an LLM to be configured.

None
**overrides

Keyword arguments to override. Accepted keys: forecaster, estimator, estimator_kwargs, steps, interval, lags, window_features.

{}

Returns:

Name Type Description
plan ForecastPlan

Updated plan with overrides (and any LLM refinement) applied. In LLM mode, the agent's reasoning is appended to plan.explanation.

Source code in skforecast_ai/assistant.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
def refine_plan(
    self,
    profile: ForecastingProfile,
    plan: ForecastPlan,
    prompt: str | None = None,
    **overrides,
) -> ForecastPlan:
    """
    Re-derive a forecast plan applying user overrides or LLM guidance.

    Operates in two modes:

    - Deterministic mode (`prompt=None`): takes an existing plan and a
      set of overrides, then calls `plan()` with the merged parameters.
      Only the overridden fields change; everything else is re-derived
      deterministically from the original profile.
    - LLM mode (`prompt` provided): a specialized agent interprets the
      natural-language domain knowledge and suggests `lags` and
      `window_features`, which are merged on top of the deterministic
      plan. The agent's reasoning is appended to the returned plan's
      `explanation`.

    Supported overrides: `forecaster`, `estimator`, `estimator_kwargs`, 
    `steps`, `interval`, `lags`, `window_features`.

    Note that `lags` and `window_features` default to the values
    already stored in `plan.forecaster_kwargs`, so refining an
    unrelated field (e.g. `steps`) preserves the existing features
    rather than re-running the PACF-based selection.

    In LLM mode, explicit `lags`/`window_features` overrides take
    precedence over the LLM suggestion (a `UserWarning` is emitted for
    each shadowed field, and a note recording the overridden field(s) is
    appended to the explanation). When both are supplied explicitly, the
    LLM has nothing left to decide and is not called. LLM mode does not
    apply to `task_type` in `('statistical', 'foundation')`, which do not
    use lags or window features; the prompt is ignored with a
    `UserWarning`. When the agent omits a field, or the LLM call fails or
    its suggestion cannot satisfy the data budget, that field keeps the
    plan's existing value (a `UserWarning` is emitted on failure).

    Parameters
    ----------
    profile : ForecastingProfile
        Original profile that produced the plan.
    plan : ForecastPlan
        Existing plan to refine.
    prompt : str, default None
        Natural-language domain knowledge used to guide LLM refinement of
        `lags` and `window_features`. When None, only the explicit
        overrides are applied. Requires an LLM to be configured.
    **overrides
        Keyword arguments to override. Accepted keys:
        `forecaster`, `estimator`, `estimator_kwargs`, `steps`,
        `interval`, `lags`, `window_features`.

    Returns
    -------
    plan : ForecastPlan
        Updated plan with overrides (and any LLM refinement) applied. In
        LLM mode, the agent's reasoning is appended to `plan.explanation`.
    """

    allowed_keys = {
        "forecaster", "estimator", "estimator_kwargs", "steps", "interval", 
        "lags", "window_features"
    }
    invalid_keys = set(overrides) - allowed_keys
    if invalid_keys:
        raise ValueError(
            f"Invalid override keys: {sorted(invalid_keys)}. "
            f"Allowed keys: {sorted(allowed_keys)}."
        )

    reasoning = None
    shadowed_fields: list[str] = []
    llm_applied_fields: list[str] = []
    if prompt is not None:
        if self.llm is None:
            raise LLMRequiredError("refine_plan")

        if plan.task_type in ("statistical", "foundation"):
            warnings.warn(
                f"LLM plan refinement does not apply to task_type "
                f"'{plan.task_type}' (no lags/window_features to refine). "
                f"Ignoring prompt.",
                UserWarning,
                stacklevel=2,
            )
        elif "lags" in overrides and "window_features" in overrides:
            warnings.warn(
                "Prompt ignored: both lags and window_features were set "
                "explicitly, leaving nothing for the LLM to decide.",
                UserWarning,
                stacklevel=2,
            )
        else:
            # Fail fast when an explicit lags/window_features override
            # already exceeds the data budget, so an LLM call is not spent
            # only for self.plan() to reject the explicit value afterwards.
            explicit_lags = overrides.get("lags")
            explicit_window_features = overrides.get("window_features")
            if explicit_window_features is not None:
                _validate_window_features(explicit_window_features)
            if explicit_lags is not None or explicit_window_features is not None:
                _validate_max_window_size(
                    lags              = explicit_lags,
                    window_features   = explicit_window_features,
                    span_index_length = profile.data_profile.span_index_length,
                )

            llm_lags, llm_window_features, reasoning = (
                self._refine_features_with_llm(
                    profile = profile,
                    plan    = plan,
                    prompt  = prompt,
                )
            )
            # Category-A precedence: an explicit lags/window_features
            # override wins over the LLM suggestion (warn and record each
            # shadowed field). Otherwise inject the LLM value only when the
            # agent actually suggested one; a field the agent omitted
            # (None), or a failed call (reasoning=None), preserves the
            # plan's existing value rather than re-running the
            # deterministic selection.
            if reasoning is not None:
                llm_features = {
                    "lags": llm_lags,
                    "window_features": llm_window_features,
                }
                for field, value in llm_features.items():
                    if value is None:
                        continue
                    if field in overrides:
                        shadowed_fields.append(field)
                        warnings.warn(
                            f"Explicit {field} override shadowed the LLM "
                            f"suggestion.",
                            UserWarning,
                            stacklevel=2,
                        )
                    else:
                        overrides[field] = value
                        llm_applied_fields.append(field)

    steps = overrides.get("steps", plan.steps)
    forecaster = overrides.get("forecaster", plan.forecaster)
    estimator = overrides.get("estimator", plan.estimator)
    estimator_kwargs = overrides.get("estimator_kwargs", plan.estimator_kwargs or None)
    interval = overrides.get("interval", plan.interval)
    lags = overrides.get("lags", plan.forecaster_kwargs.get("lags"))
    window_features = overrides.get("window_features", plan.forecaster_kwargs.get("window_features"))

    refined_plan = self.plan(
        profile          = profile,
        steps            = steps,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        interval         = interval,
        lags             = lags,
        window_features  = window_features,
    )

    if reasoning is not None:
        refined_plan.explanation += (
            f"\n\nLLM Refinement Reasoning:\n{reasoning}"
        )
        refined_plan.llm_refined_fields = llm_applied_fields
        if shadowed_fields:
            # The LLM's narrative may describe a field that an explicit
            # override replaced. Record which fields actually took
            # precedence so the persisted explanation is not misleading.
            refined_plan.explanation += (
                f"\n\nNote: explicit override(s) took precedence over the "
                f"LLM suggestion for: {', '.join(shadowed_fields)}."
            )
        if llm_applied_fields:
            # LLM-suggested features are hypotheses, not measured
            # improvements. Make the lack of validation explicit so the
            # user does not read the plan as a proven accuracy gain.
            refined_plan.explanation += (
                "\n\nNote: the LLM-suggested "
                f"{' and '.join(llm_applied_fields)} are hypotheses, not "
                "validated improvements. Confirm any expected accuracy "
                "gain before relying on them."
            )

    return refined_plan

forecast_code

forecast_code(
    data=None,
    steps=None,
    target=None,
    date_column=None,
    series_id_column=None,
    exog=None,
    interval=None,
    test_size=None,
    forecaster=None,
    estimator=None,
    estimator_kwargs=None,
    lags=None,
    window_features=None,
    profile=None,
    plan=None,
)

Profile, plan, and generate a complete forecasting script.

Convenience wrapper that chains profile(), plan(), and code generation in a single call. Pre-computed profile and/or plan can be passed to skip those stages (e.g. after modifying the plan with refine_plan()).

The method operates in one of two modes depending on test_size:

  • Evaluation mode (test_size is set): the data is split into train and test sets, the forecaster is trained on the training set, predictions are made for the test set and metrics are computed against the held-out observations.
  • Prediction mode (test_size is None, the default): the forecaster is trained on all available data and forecasts the future. No metrics are returned because there is no ground truth to compare against. When the data contains exogenous variables, future values must be supplied through exog.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. Required when profile is not provided. When a pandas Series is passed, the target is derived from its name.

None
steps int

Forecast horizon (number of steps ahead to predict). Required when plan is not provided.

None
target str, list of str

Name of the column to forecast. Required when profile is not provided, unless data is a pandas Series (the Series name is used instead). For wide-format multi-series, pass a list of column names where each column is a series.

None
date_column str

Name of the column containing timestamps. When None, the index of data is assumed to be a DatetimeIndex.

None
series_id_column str

Name of the column identifying individual series (long-format multi-series input). When None, the data is treated as single-series or wide-format multi-series.

None
exog pandas DataFrame

Future exogenous variables covering the forecast horizon. Mirrors forecast() for signature consistency. Because this method only generates code (the rendered prediction-mode script loads the future values from 'exog_future.csv' at run time), exog is optional here and is used only to validate the inputs: it must not be combined with test_size, and it must not be supplied when the data has no exogenous columns.

None
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] (e.g. [0.1, 0.9] for 80 % interval). When None, no prediction intervals are computed.

None
test_size int, float, str, pandas Timestamp

Size or start of the test set, selecting the evaluation or prediction mode described above.

  • int: the last test_size observations form the test set.
  • float in (0, 1): the last fraction test_size of the observations form the test set.
  • str or pandas Timestamp: the first timestamp of the test set (the split boundary).

When None (default), the method runs in prediction mode.

None
forecaster str

Explicit forecaster class name to use instead of the recommended one (e.g. 'ForecasterRecursive', 'ForecasterDirect', 'ForecasterRecursiveMultiSeries'). When None, the most suitable forecaster is selected automatically from the characteristics of the data.

None
estimator str

Explicit regressor class name to use instead of the recommended one (e.g. 'HistGradientBoostingRegressor', 'LGBMRegressor'). When None, a suitable estimator is selected automatically based on the dataset size.

None
estimator_kwargs dict

Keyword arguments for the estimator constructor (e.g. {'n_estimators': 200, 'learning_rate': 0.05}). Merged on top of built-in defaults (random_state and silencing flags), with user values taking precedence. When None, only the built-in defaults are used.

None
lags int, list of int

Explicit lag configuration. An integer uses lags 1 to lags; a list uses the specified lags. When None, lags are selected automatically from the partial autocorrelation of the series.

None
window_features list of dict

Explicit window (rolling) features configuration. Each dict must contain the keys 'stats' (a list of rolling statistics) and 'window_size' (a scalar int applied to every stat in that same dict), for example [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], 'window_size': 24}, {'stats': ['mean'], 'window_size': 168}]. To combine several window sizes, add one dict per size. Allowed stats are 'mean', 'std', 'min', 'max', 'sum', 'median', 'ratio_min_max', 'coef_variation', and 'ewm'. When None, window features are selected automatically from the characteristics of the data.

None
profile ForecastingProfile

Pre-computed profile to skip profiling. If None, profiling is performed from data.

None
plan ForecastPlan

Pre-computed plan to skip planning. If None, a plan is generated from the profile. Requires profile to also be provided.

None

Returns:

Name Type Description
result CodeGenerationResult

Generated forecasting script and the decisions behind it. Contains the following attributes:

  • profile: profile of the input dataset and high-level modeling decisions.
  • plan: detailed forecasting plan.
  • code: generated Python script.
Source code in skforecast_ai/assistant.py
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
def forecast_code(
    self,
    data: pd.Series | pd.DataFrame | str | Path | None = None,
    steps: int | None = None,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    exog: pd.DataFrame | None = None,
    interval: list[float] | None = None,
    test_size: int | float | str | pd.Timestamp | None = None,
    forecaster: str | None = None,
    estimator: str | None = None,
    estimator_kwargs: dict | None = None,
    lags: int | list[int] | None = None,
    window_features: list[dict[str, list[str] | int]] | None = None,
    profile: ForecastingProfile | None = None,
    plan: ForecastPlan | None = None,
) -> CodeGenerationResult:
    """
    Profile, plan, and generate a complete forecasting script.

    Convenience wrapper that chains `profile()`, `plan()`,
    and code generation in a single call. Pre-computed `profile`
    and/or `plan` can be passed to skip those stages (e.g. after
    modifying the plan with `refine_plan()`).

    The method operates in one of two modes depending on `test_size`:

    - Evaluation mode (`test_size` is set): the data is split into
    train and test sets, the forecaster is trained on the training
    set, predictions are made for the test set and metrics are
    computed against the held-out observations.
    - Prediction mode (`test_size` is None, the default): the
    forecaster is trained on all available data and forecasts the
    future. No metrics are returned because there is no ground
    truth to compare against. When the data contains exogenous
    variables, future values must be supplied through `exog`.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path, default None
        Input dataset, a single series, or path to a CSV file. Required
        when `profile` is not provided. When a pandas Series is passed,
        the target is derived from its name.
    steps : int, default None
        Forecast horizon (number of steps ahead to predict).
        Required when `plan` is not provided.
    target : str, list of str, default None
        Name of the column to forecast. Required when `profile`
        is not provided, unless `data` is a pandas Series (the Series
        name is used instead). For wide-format multi-series, pass a
        list of column names where each column is a series.
    date_column : str, default None
        Name of the column containing timestamps. When None, the
        index of `data` is assumed to be a DatetimeIndex.
    series_id_column : str, default None
        Name of the column identifying individual series (long-format
        multi-series input). When None, the data is treated as
        single-series or wide-format multi-series.
    exog : pandas DataFrame, default None
        Future exogenous variables covering the forecast horizon.
        Mirrors `forecast()` for signature consistency. Because this
        method only generates code (the rendered prediction-mode
        script loads the future values from `'exog_future.csv'` at run
        time), `exog` is optional here and is used only to validate
        the inputs: it must not be combined with `test_size`, and it
        must not be supplied when the data has no exogenous columns.
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` (e.g. `[0.1, 0.9]` for 80 % interval). When
        None, no prediction intervals are computed.
    test_size : int, float, str, pandas Timestamp, default None
        Size or start of the test set, selecting the evaluation or
        prediction mode described above.

        - int: the last `test_size` observations form the test set.
        - float in `(0, 1)`: the last fraction `test_size` of the
        observations form the test set.
        - str or pandas Timestamp: the first timestamp of the test
        set (the split boundary).

        When None (default), the method runs in prediction mode.
    forecaster : str, default None
        Explicit forecaster class name to use instead of the
        recommended one (e.g. `'ForecasterRecursive'`,
        `'ForecasterDirect'`, `'ForecasterRecursiveMultiSeries'`).
        When None, the most suitable forecaster is selected
        automatically from the characteristics of the data.
    estimator : str, default None
        Explicit regressor class name to use instead of the
        recommended one (e.g. `'HistGradientBoostingRegressor'`,
        `'LGBMRegressor'`). When None, a suitable estimator is
        selected automatically based on the dataset size.
    estimator_kwargs : dict, default None
        Keyword arguments for the estimator constructor (e.g.
        `{'n_estimators': 200, 'learning_rate': 0.05}`). Merged on
        top of built-in defaults (`random_state` and silencing
        flags), with user values taking precedence. When None, only
        the built-in defaults are used.
    lags : int, list of int, default None
        Explicit lag configuration. An integer uses lags 1 to `lags`;
        a list uses the specified lags. When None, lags are selected
        automatically from the partial autocorrelation of the series.
    window_features : list of dict, default None
        Explicit window (rolling) features configuration. Each dict
        must contain the keys `'stats'` (a list of rolling statistics)
        and `'window_size'` (a scalar int applied to every stat in
        that same dict), for example `[{'stats': ['mean', 'std'],
        'window_size': 3}, {'stats': ['mean'], 'window_size': 24},
        {'stats': ['mean'], 'window_size': 168}]`. To combine several
        window sizes, add one dict per size. Allowed stats are
        `'mean'`, `'std'`, `'min'`, `'max'`, `'sum'`, `'median'`,
        `'ratio_min_max'`, `'coef_variation'`, and `'ewm'`. When None,
        window features are selected automatically from the
        characteristics of the data.
    profile : ForecastingProfile, default None
        Pre-computed profile to skip profiling. If None, profiling
        is performed from `data`.
    plan : ForecastPlan, default None
        Pre-computed plan to skip planning. If None, a plan is
        generated from the profile. Requires `profile` to also be
        provided.

    Returns
    -------
    result : CodeGenerationResult
        Generated forecasting script and the decisions behind it.
        Contains the following attributes:

        - profile: profile of the input dataset and high-level
        modeling decisions.
        - plan: detailed forecasting plan.
        - code: generated Python script.
    """

    _warn_if_plan_overrides_ignored(
        plan             = plan,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        interval         = interval,
    )

    if profile is None:
        profile = self.profile(
            data             = data,
            target           = target,
            date_column      = date_column,
            series_id_column = series_id_column,
        )

    has_exog = bool(profile.data_profile.exog_columns)
    # Evaluation mode is driven by `test_size`, or by a pre-built plan
    # that already carries an `end_train` split boundary. Everything
    # else is prediction mode (forecast the future).
    evaluate = test_size is not None or (
        plan is not None and plan.end_train is not None
    )
    effective_steps = steps if steps is not None else (
        plan.steps if plan is not None else 0
    )
    _validate_forecast_mode(
        evaluate     = evaluate,
        exog         = exog,
        has_exog     = has_exog,
        steps        = effective_steps,
        require_exog = False,
    )

    if plan is None:
        plan = self.plan(
            profile          = profile,
            steps            = steps,
            forecaster       = forecaster,
            estimator        = estimator,
            estimator_kwargs = estimator_kwargs,
            interval         = interval,
            lags             = lags,
            window_features  = window_features,
        )

    # Resolve the forecast-only split boundary here (see `forecast()`),
    # stamping it onto the plan whether it was built or supplied.
    if test_size is not None:
        end_train = resolve_end_train(
            start_date     = profile.data_profile.start_date,
            frequency      = profile.data_profile.frequency,
            n_observations = profile.data_profile.span_index_length,
            test_size      = test_size,
        )
        plan = plan.model_copy(update={"end_train": end_train})

    code = render_forecast_script(
        profile=profile.data_profile, plan=plan
    ).full_script

    return CodeGenerationResult(
        profile = profile,
        plan    = plan,
        code    = code,
    )

forecast

forecast(
    data,
    steps,
    target=None,
    date_column=None,
    series_id_column=None,
    exog=None,
    interval=None,
    test_size=None,
    forecaster=None,
    estimator=None,
    estimator_kwargs=None,
    lags=None,
    window_features=None,
    profile=None,
    plan=None,
)

Execute a full forecasting workflow end-to-end.

Convenience wrapper that chains profile(), plan(), validation and programmatic execution. Pre-computed profile and/or plan can be passed to skip those stages (e.g. after modifying the plan with refine_plan()).

The method operates in one of two modes depending on test_size:

  • Evaluation mode (test_size is set): the data is split into train and test sets, the forecaster is trained on the training set, predictions are made for the test set and metrics are computed against the held-out observations.
  • Prediction mode (test_size is None, the default): the forecaster is trained on all available data and forecasts the future. No metrics are returned because there is no ground truth to compare against. When the data contains exogenous variables, future values must be supplied through exog.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. When a pandas Series is passed, the target is derived from its name.

required
steps int

Forecast horizon (number of steps ahead to predict).

required
target str, list of str

Name of the column to forecast. Optional only when data is a pandas Series (the Series name is used instead). For wide-format multi-series, pass a list of column names where each column is a series.

None
date_column str

Name of the column containing timestamps. When None, the index of data is assumed to be a DatetimeIndex.

None
series_id_column str

Name of the column identifying individual series (long-format multi-series input). When None, the data is treated as single-series or wide-format multi-series.

None
exog pandas DataFrame

Future exogenous variables covering the forecast horizon (at least steps rows). Used only in prediction mode (test_size=None) and required there when the data contains exogenous variables. Must not be combined with test_size: in evaluation mode the test-set exogenous values are taken from the split.

None
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] (e.g. [0.1, 0.9] for 80 % interval). When None, no prediction intervals are computed.

None
test_size int, float, str, pandas Timestamp

Size or start of the test set, selecting the evaluation or prediction mode described above.

  • int: the last test_size observations form the test set.
  • float in (0, 1): the last fraction test_size of the observations form the test set.
  • str or pandas Timestamp: the first timestamp of the test set (the split boundary).

When None (default), the method runs in prediction mode.

None
forecaster str

Explicit forecaster class name to use instead of the recommended one (e.g. 'ForecasterRecursive', 'ForecasterDirect', 'ForecasterRecursiveMultiSeries'). When None, the most suitable forecaster is selected automatically from the characteristics of the data.

None
estimator str

Explicit regressor class name to use instead of the recommended one (e.g. 'HistGradientBoostingRegressor', 'LGBMRegressor'). When None, a suitable estimator is selected automatically based on the dataset size.

None
estimator_kwargs dict

Keyword arguments for the estimator constructor (e.g. {'n_estimators': 200, 'learning_rate': 0.05}). Merged on top of built-in defaults (random_state and silencing flags), with user values taking precedence. When None, only the built-in defaults are used.

None
lags int, list of int

Explicit lag configuration. An integer uses lags 1 to lags; a list uses the specified lags. When None, lags are selected automatically from the partial autocorrelation of the series.

None
window_features list of dict

Explicit window (rolling) features configuration. Each dict must contain the keys 'stats' (a list of rolling statistics) and 'window_size' (a scalar int applied to every stat in that same dict), for example [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], 'window_size': 24}, {'stats': ['mean'], 'window_size': 168}]. To combine several window sizes, add one dict per size. Allowed stats are 'mean', 'std', 'min', 'max', 'sum', 'median', 'ratio_min_max', 'coef_variation', and 'ewm'. When None, window features are selected automatically from the characteristics of the data.

None
profile ForecastingProfile

Pre-computed profile to skip profiling. If None, profiling is performed from data.

None
plan ForecastPlan

Pre-computed plan to skip planning. If None, a plan is generated from the profile. Requires profile to also be provided.

None

Returns:

Name Type Description
result ForecastResult

Executed forecast and the decisions behind it. Contains the following attributes:

  • profile: profile of the input dataset and high-level modeling decisions.
  • plan: detailed forecasting plan that was executed.
  • code: generated Python script equivalent to the execution.
  • predictions: forecasted values for the requested steps, including the interval columns when interval is set.
  • metrics: evaluation metrics, one row per series. None in prediction mode (test_size=None), where there is no ground truth to evaluate against.
Notes

This method executes the same code that forecast_code() produces, ensuring perfect fidelity between the inspectable script (ForecastResult.code) and the actual execution.

Source code in skforecast_ai/assistant.py
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
def forecast(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    steps: int,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    exog: pd.DataFrame | None = None,
    interval: list[float] | None = None,
    test_size: int | float | str | pd.Timestamp | None = None,
    forecaster: str | None = None,
    estimator: str | None = None,
    estimator_kwargs: dict | None = None,
    lags: int | list[int] | None = None,
    window_features: list[dict[str, list[str] | int]] | None = None,
    profile: ForecastingProfile | None = None,
    plan: ForecastPlan | None = None,
) -> ForecastResult:
    """
    Execute a full forecasting workflow end-to-end.

    Convenience wrapper that chains `profile()`, `plan()`,
    validation and programmatic execution. Pre-computed `profile`
    and/or `plan` can be passed to skip those stages (e.g. after
    modifying the plan with `refine_plan()`).

    The method operates in one of two modes depending on `test_size`:

    - Evaluation mode (`test_size` is set): the data is split into
    train and test sets, the forecaster is trained on the training
    set, predictions are made for the test set and metrics are
    computed against the held-out observations.
    - Prediction mode (`test_size` is None, the default): the
    forecaster is trained on all available data and forecasts the
    future. No metrics are returned because there is no ground
    truth to compare against. When the data contains exogenous
    variables, future values must be supplied through `exog`.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file. When a
        pandas Series is passed, the target is derived from its name.
    steps : int
        Forecast horizon (number of steps ahead to predict).
    target : str, list of str, default None
        Name of the column to forecast. Optional only when `data` is a
        pandas Series (the Series name is used instead). For
        wide-format multi-series, pass a list of column names where
        each column is a series.
    date_column : str, default None
        Name of the column containing timestamps. When None, the
        index of `data` is assumed to be a DatetimeIndex.
    series_id_column : str, default None
        Name of the column identifying individual series (long-format
        multi-series input). When None, the data is treated as
        single-series or wide-format multi-series.
    exog : pandas DataFrame, default None
        Future exogenous variables covering the forecast horizon
        (at least `steps` rows). Used only in prediction mode
        (`test_size=None`) and required there when the data contains
        exogenous variables. Must not be combined with `test_size`:
        in evaluation mode the test-set exogenous values are taken
        from the split.
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` (e.g. `[0.1, 0.9]` for 80 % interval). When
        None, no prediction intervals are computed.
    test_size : int, float, str, pandas Timestamp, default None
        Size or start of the test set, selecting the evaluation or
        prediction mode described above.

        - int: the last `test_size` observations form the test set.
        - float in `(0, 1)`: the last fraction `test_size` of the
        observations form the test set.
        - str or pandas Timestamp: the first timestamp of the test
        set (the split boundary).

        When None (default), the method runs in prediction mode.
    forecaster : str, default None
        Explicit forecaster class name to use instead of the
        recommended one (e.g. `'ForecasterRecursive'`,
        `'ForecasterDirect'`, `'ForecasterRecursiveMultiSeries'`).
        When None, the most suitable forecaster is selected
        automatically from the characteristics of the data.
    estimator : str, default None
        Explicit regressor class name to use instead of the
        recommended one (e.g. `'HistGradientBoostingRegressor'`,
        `'LGBMRegressor'`). When None, a suitable estimator is
        selected automatically based on the dataset size.
    estimator_kwargs : dict, default None
        Keyword arguments for the estimator constructor (e.g.
        `{'n_estimators': 200, 'learning_rate': 0.05}`). Merged on
        top of built-in defaults (`random_state` and silencing
        flags), with user values taking precedence. When None, only
        the built-in defaults are used.
    lags : int, list of int, default None
        Explicit lag configuration. An integer uses lags 1 to `lags`;
        a list uses the specified lags. When None, lags are selected
        automatically from the partial autocorrelation of the series.
    window_features : list of dict, default None
        Explicit window (rolling) features configuration. Each dict
        must contain the keys `'stats'` (a list of rolling statistics)
        and `'window_size'` (a scalar int applied to every stat in
        that same dict), for example `[{'stats': ['mean', 'std'],
        'window_size': 3}, {'stats': ['mean'], 'window_size': 24},
        {'stats': ['mean'], 'window_size': 168}]`. To combine several
        window sizes, add one dict per size. Allowed stats are
        `'mean'`, `'std'`, `'min'`, `'max'`, `'sum'`, `'median'`,
        `'ratio_min_max'`, `'coef_variation'`, and `'ewm'`. When None,
        window features are selected automatically from the
        characteristics of the data.
    profile : ForecastingProfile, default None
        Pre-computed profile to skip profiling. If None, profiling
        is performed from `data`.
    plan : ForecastPlan, default None
        Pre-computed plan to skip planning. If None, a plan is
        generated from the profile. Requires `profile` to also be
        provided.

    Returns
    -------
    result : ForecastResult
        Executed forecast and the decisions behind it. Contains the
        following attributes:

        - profile: profile of the input dataset and high-level
        modeling decisions.
        - plan: detailed forecasting plan that was executed.
        - code: generated Python script equivalent to the execution.
        - predictions: forecasted values for the requested steps,
        including the interval columns when `interval` is set.
        - metrics: evaluation metrics, one row per series. None in
        prediction mode (`test_size=None`), where there is no ground
        truth to evaluate against.

    Notes
    -----
    This method executes the same code that `forecast_code()`
    produces, ensuring perfect fidelity between the inspectable
    script (`ForecastResult.code`) and the actual execution.
    """

    _warn_if_plan_overrides_ignored(
        plan             = plan,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        interval         = interval,
    )

    data_df, target = _resolve_data_and_target(data, target)

    if profile is None:
        profile = self.profile(
            data             = data_df,
            target           = target,
            date_column      = date_column,
            series_id_column = series_id_column,
        )

    has_exog = bool(profile.data_profile.exog_columns)
    # Evaluation mode is driven by `test_size`, or by a pre-built plan
    # that already carries an `end_train` split boundary. Everything
    # else is prediction mode (forecast the future).
    evaluate = test_size is not None or (
        plan is not None and plan.end_train is not None
    )
    _validate_forecast_mode(
        evaluate = evaluate,
        exog     = exog,
        has_exog = has_exog,
        steps    = steps,
    )

    if plan is None:
        plan = self.plan(
            profile          = profile,
            steps            = steps,
            forecaster       = forecaster,
            estimator        = estimator,
            estimator_kwargs = estimator_kwargs,
            interval         = interval,
            lags             = lags,
            window_features  = window_features,
        )

    # `test_size` is a forecast-only concept, so the split boundary is
    # resolved here rather than in the shared `plan()` method. It is
    # stamped onto the plan whether it was freshly built or supplied.
    if test_size is not None:
        end_train = resolve_end_train(
            start_date     = profile.data_profile.start_date,
            frequency      = profile.data_profile.frequency,
            n_observations = profile.data_profile.span_index_length,
            test_size      = test_size,
        )
        plan = plan.model_copy(update={"end_train": end_train})

    result = run_forecast(
        data    = data_df,
        profile = profile.data_profile,
        plan    = plan,
        exog    = exog,
    )

    return ForecastResult(
        profile     = profile,
        plan        = plan,
        code        = result["rendered_code"].full_script,
        metrics     = result["metrics"],
        predictions = result["predictions"],
    )

create_cv

create_cv(
    profile,
    plan,
    prompt=None,
    initial_train_size=None,
    fold_stride=None,
    refit=None,
    fixed_train_size=None,
    gap=None,
    skip_folds=None,
    allow_incomplete_fold=None,
)

Generate a time series cross-validation strategy for backtesting.

Produces a TimeSeriesFold [1]_ configured with smart defaults derived from the profile and plan.

Explicit keyword arguments override defaults. If None, they are automatically determined based on the profile and plan characteristics.

Parameters:

Name Type Description Default
profile ForecastingProfile

Output of profile().

required
plan ForecastPlan

Output of plan().

required
prompt str

Natural language description of the evaluation scenario. Requires an LLM to be configured. If None or no LLM is available, deterministic defaults are used.

None
initial_train_size int, str, pandas Timestamp

Number of observations used for initial training.

  • If None, initial training size is automatically determined based on the profile and plan.
  • If an integer, the number of observations used for initial training.
  • If a date string or pandas Timestamp, it is the last date included in the initial training set.
None
fold_stride int

Number of observations that the start of the test set advances between consecutive folds.

  • If None, it defaults to the same value as steps, meaning that folds are placed back-to-back without overlap.
  • If fold_stride < steps, test sets overlap and multiple forecasts will be generated for the same observations.
  • If fold_stride > steps, gaps are left between consecutive test sets. New in version 0.18.0
None
refit (bool, int)

Whether to refit the forecaster in each fold.

  • If None, refit behavior is automatically determined based on the profile and plan.
  • If True, the forecaster is refitted in each fold.
  • If False, the forecaster is trained only in the first fold.
  • If an integer, the forecaster is trained in the first fold and then refitted every refit folds.
None
fixed_train_size bool

Whether the training size is fixed or increases in each fold.

None
gap int

Number of observations between the end of the training set and the start of the test set.

None
skip_folds (int, list)

Number of folds to skip.

  • If an integer, every 'skip_folds'-th is returned.
  • If a list, the indexes of the folds to skip.

For example, if skip_folds=3 and there are 10 folds, the returned folds are 0, 3, 6, and 9. If skip_folds=[1, 2, 3], the returned folds are 0, 4, 5, 6, 7, 8, and 9.

None
allow_incomplete_fold bool

Whether to allow the last fold to include fewer observations than steps. If False, the last fold is excluded if it is incomplete.

None

Returns:

Name Type Description
cv TimeSeriesFold

Configured cross-validation fold splitter.

cv_explanation str

Human-readable explanation of the chosen configuration.

References

[1] Skforecast TimeSeriesFold API Reference: https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

Source code in skforecast_ai/assistant.py
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
def create_cv(
    self,
    profile: ForecastingProfile,
    plan: ForecastPlan,
    prompt: str | None = None,
    initial_train_size: int | str | pd.Timestamp | None = None,
    fold_stride: int | None = None,
    refit: bool | int | None = None,
    fixed_train_size: bool | None = None,
    gap: int | None = None,
    skip_folds: int | list[int] | None = None,
    allow_incomplete_fold: bool | None = None,
) -> tuple[TimeSeriesFold, str]:
    """
    Generate a time series cross-validation strategy for backtesting.

    Produces a `TimeSeriesFold` [1]_ configured with smart defaults
    derived from the profile and plan. 

    Explicit keyword arguments override defaults. If None, they are 
    automatically determined based on the profile and plan characteristics.

    Parameters
    ----------
    profile : ForecastingProfile
        Output of `profile()`.
    plan : ForecastPlan
        Output of `plan()`.
    prompt : str, default None
        Natural language description of the evaluation scenario.
        Requires an LLM to be configured. If None or no LLM is
        available, deterministic defaults are used.
    initial_train_size : int, str, pandas Timestamp, default None
        Number of observations used for initial training. 

        - If `None`, initial training size is automatically determined based 
        on the profile and plan.
        - If an integer, the number of observations used for initial training.
        - If a date string or pandas Timestamp, it is the last date included in 
        the initial training set.
    fold_stride : int, default None
        Number of observations that the start of the test set advances between
        consecutive folds.

        - If `None`, it defaults to the same value as `steps`, meaning that folds
        are placed back-to-back without overlap.
        - If `fold_stride < steps`, test sets overlap and multiple forecasts will
        be generated for the same observations.
        - If `fold_stride > steps`, gaps are left between consecutive test sets.
        **New in version 0.18.0**
    refit : bool, int, default None
        Whether to refit the forecaster in each fold.

        - If `None`, refit behavior is automatically determined based on the 
        profile and plan.
        - If `True`, the forecaster is refitted in each fold.
        - If `False`, the forecaster is trained only in the first fold.
        - If an integer, the forecaster is trained in the first fold and then refitted
        every `refit` folds.
    fixed_train_size : bool, default None
        Whether the training size is fixed or increases in each fold.
    gap : int, default None
        Number of observations between the end of the training set and the start of the
        test set.
    skip_folds : int, list, default None
        Number of folds to skip.

        - If an integer, every 'skip_folds'-th is returned.
        - If a list, the indexes of the folds to skip.

        For example, if `skip_folds=3` and there are 10 folds, the returned folds are
        0, 3, 6, and 9. If `skip_folds=[1, 2, 3]`, the returned folds are 0, 4, 5, 6, 7,
        8, and 9.
    allow_incomplete_fold : bool, default None
        Whether to allow the last fold to include fewer observations than `steps`.
        If `False`, the last fold is excluded if it is incomplete.

    Returns
    -------
    cv : TimeSeriesFold
        Configured cross-validation fold splitter.
    cv_explanation : str
        Human-readable explanation of the chosen configuration.

    References
    ----------
    [1] Skforecast `TimeSeriesFold` API Reference:
        https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

    """

    span_index_length = profile.data_profile.span_index_length

    # -----------------------------------------------------------------
    # LLM path: when prompt is provided, use LLM for CV configuration
    # -----------------------------------------------------------------
    if prompt is not None and self.llm is None:
        raise LLMRequiredError("create_cv")

    # All CV parameters the LLM would otherwise decide. When every one is
    # set explicitly, the LLM has nothing left to decide, so skip the call.
    llm_decidable = (
        initial_train_size,
        refit,
        fixed_train_size,
        gap,
        fold_stride,
        skip_folds,
        allow_incomplete_fold,
    )
    use_llm = prompt is not None
    if use_llm and all(param is not None for param in llm_decidable):
        warnings.warn(
            "Prompt ignored: all CV parameters were set explicitly, "
            "leaving nothing for the LLM to decide.",
            UserWarning,
            stacklevel=2,
        )
        use_llm = False

    if use_llm:
        defaults = self._configure_cv_with_llm(
                       profile        = profile,
                       plan           = plan,
                       prompt         = prompt,
                       n_observations = span_index_length,
                   )
    else:
        # Compute deterministic defaults
        defaults = derive_cv_defaults(profile=profile, plan=plan)

    # Apply explicit overrides
    overrides = {
        "initial_train_size": initial_train_size,
        "refit": refit,
        "fixed_train_size": fixed_train_size,
        "gap": gap,
        "fold_stride": fold_stride,
        "skip_folds": skip_folds,
        "allow_incomplete_fold": allow_incomplete_fold,
    }
    for key, value in overrides.items():
        if value is not None:
            defaults[key] = value

    # Handle initial_train_size type conversion
    its = defaults["initial_train_size"]
    if isinstance(its, float):
        if not (0 < its < 1):
            raise ValueError(
                f"initial_train_size as float must satisfy "
                f"0 < value < 1, got {its}."
            )
        defaults["initial_train_size"] = int(its * span_index_length)

    # Instantiate TimeSeriesFold
    cv = TimeSeriesFold(
        steps                 = defaults["steps"],
        initial_train_size    = defaults["initial_train_size"],
        refit                 = defaults["refit"],
        fixed_train_size      = defaults["fixed_train_size"],
        gap                   = defaults["gap"],
        fold_stride           = defaults["fold_stride"],
        skip_folds            = defaults["skip_folds"],
        allow_incomplete_fold = defaults["allow_incomplete_fold"],
        differentiation       = defaults.get("differentiation"),
        verbose               = False,
    )

    # Validate fold count. A date-based initial_train_size needs a
    # DatetimeIndex so `cv.split` can locate the split date; integer or
    # fractional sizes are validated against a plain RangeIndex.
    n_folds = _count_cv_folds(
                  cv             = cv,
                  n_observations = span_index_length,
                  start_date     = profile.data_profile.start_date,
                  frequency      = profile.data_profile.frequency,
              )
    if n_folds < 2:
        raise ValueError(
            f"The resolved CV configuration produces only "
            f"{n_folds} fold(s). At least 2 are required. "
            f"Resolved parameters: {defaults}."
        )

    # Build explanation
    reasoning = defaults.pop("_reasoning", None)
    cv_explanation = build_cv_explanation(
                         cv_params      = defaults,
                         n_observations = span_index_length,
                         n_folds        = n_folds,
                     )
    if reasoning:
        cv_explanation = f"{reasoning} {cv_explanation}"

    return cv, cv_explanation

backtest_code

backtest_code(
    data,
    cv,
    target=None,
    date_column=None,
    series_id_column=None,
    interval=None,
    forecaster=None,
    estimator=None,
    estimator_kwargs=None,
    profile=None,
    plan=None,
)

Profile, plan, and generate a complete backtesting script.

Convenience wrapper that chains profile(), plan(), and backtesting code generation in a single call. Pre-computed profile and/or plan can be passed to skip those stages.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. When a pandas Series is passed, the target is derived from its name.

required
cv TimeSeriesFold

Time series cross-validation fold splitter (output of create_cv() or user-constructed) [1]_.

required
target str, list of str

Name of the column(s) to forecast. Optional only when data is a pandas Series (the Series name is used instead). For wide-format multi-series, pass a list of column names where each column is a series.

None
date_column str

Name of the column containing timestamps. When None, the index of data is assumed to be a DatetimeIndex.

None
series_id_column str

Name of the column identifying individual series (long-format multi-series input). When None, the data is treated as single-series or wide-format multi-series.

None
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] (e.g. [0.1, 0.9] for 80 % interval). When None, no prediction intervals are computed.

None
forecaster str

Explicit forecaster class name to use instead of the recommended one (e.g. 'ForecasterRecursive', 'ForecasterDirect', 'ForecasterRecursiveMultiSeries'). When None, the most suitable forecaster is selected automatically from the characteristics of the data.

None
estimator str

Explicit regressor class name to use instead of the recommended one (e.g. 'HistGradientBoostingRegressor', 'LGBMRegressor'). When None, a suitable estimator is selected automatically based on the dataset size.

None
estimator_kwargs dict

Keyword arguments for the estimator constructor (e.g. {'n_estimators': 200, 'learning_rate': 0.05}). Merged on top of built-in defaults (random_state and silencing flags), with user values taking precedence. When None, only the built-in defaults are used.

None
profile ForecastingProfile

Pre-computed profile to skip profiling.

None
plan ForecastPlan

Pre-computed plan to skip planning.

None

Returns:

Name Type Description
result CodeGenerationResult

Generated backtesting script and the decisions behind it. Contains the following attributes:

  • profile: profile of the input dataset and high-level modeling decisions.
  • plan: detailed forecasting plan.
  • code: generated Python backtesting script.
Notes

To customize lags or window_features, build the plan with plan() (or refine_plan()) and pass it via plan, then build a matching cv with create_cv(). This keeps the plan and the cross-validation configuration consistent.

References

[1] Skforecast TimeSeriesFold API Reference: https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

Source code in skforecast_ai/assistant.py
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
def backtest_code(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    cv: TimeSeriesFold,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    interval: list[float] | None = None,
    forecaster: str | None = None,
    estimator: str | None = None,
    estimator_kwargs: dict | None = None,
    profile: ForecastingProfile | None = None,
    plan: ForecastPlan | None = None,
) -> CodeGenerationResult:
    """
    Profile, plan, and generate a complete backtesting script.

    Convenience wrapper that chains `profile()`, `plan()`, and
    backtesting code generation in a single call. Pre-computed
    `profile` and/or `plan` can be passed to skip those stages.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file. When a
        pandas Series is passed, the target is derived from its name.
    cv : TimeSeriesFold
        Time series cross-validation fold splitter (output of
        `create_cv()` or user-constructed) [1]_.
    target : str, list of str, default None
        Name of the column(s) to forecast. Optional only when `data`
        is a pandas Series (the Series name is used instead). For
        wide-format multi-series, pass a list of column names where
        each column is a series.
    date_column : str, default None
        Name of the column containing timestamps. When None, the
        index of `data` is assumed to be a DatetimeIndex.
    series_id_column : str, default None
        Name of the column identifying individual series (long-format
        multi-series input). When None, the data is treated as
        single-series or wide-format multi-series.
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` (e.g. `[0.1, 0.9]` for 80 % interval). When
        None, no prediction intervals are computed.
    forecaster : str, default None
        Explicit forecaster class name to use instead of the
        recommended one (e.g. `'ForecasterRecursive'`,
        `'ForecasterDirect'`, `'ForecasterRecursiveMultiSeries'`).
        When None, the most suitable forecaster is selected
        automatically from the characteristics of the data.
    estimator : str, default None
        Explicit regressor class name to use instead of the
        recommended one (e.g. `'HistGradientBoostingRegressor'`,
        `'LGBMRegressor'`). When None, a suitable estimator is
        selected automatically based on the dataset size.
    estimator_kwargs : dict, default None
        Keyword arguments for the estimator constructor (e.g.
        `{'n_estimators': 200, 'learning_rate': 0.05}`). Merged on
        top of built-in defaults (`random_state` and silencing
        flags), with user values taking precedence. When None, only
        the built-in defaults are used.
    profile : ForecastingProfile, default None
        Pre-computed profile to skip profiling.
    plan : ForecastPlan, default None
        Pre-computed plan to skip planning.

    Returns
    -------
    result : CodeGenerationResult
        Generated backtesting script and the decisions behind it.
        Contains the following attributes:

        - profile: profile of the input dataset and high-level
        modeling decisions.
        - plan: detailed forecasting plan.
        - code: generated Python backtesting script.

    Notes
    -----
    To customize `lags` or `window_features`, build the plan with
    `plan()` (or `refine_plan()`) and pass it via `plan`, then build a
    matching `cv` with `create_cv()`. This keeps the plan and the
    cross-validation configuration consistent.

    References
    ----------
    [1] Skforecast `TimeSeriesFold` API Reference:
        https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

    """

    profile, plan = self._prepare_backtest(
        data             = data,
        target           = target,
        cv               = cv,
        date_column      = date_column,
        series_id_column = series_id_column,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        interval         = interval,
        profile          = profile,
        plan             = plan,
    )

    code = render_backtesting_script(
        profile=profile.data_profile, plan=plan, cv=cv
    ).full_script

    return CodeGenerationResult(
        profile = profile,
        plan    = plan,
        code    = code,
    )

backtest

backtest(
    data,
    cv,
    target=None,
    date_column=None,
    series_id_column=None,
    interval=None,
    forecaster=None,
    estimator=None,
    estimator_kwargs=None,
    profile=None,
    plan=None,
    show_progress=True,
)

Execute backtesting with a pre-configured time series cross-validation strategy (TimeSeriesFold [1]_).

Chains profile(), plan(), and backtesting execution using the provided TimeSeriesFold. The steps parameter is inferred from cv.steps.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. When a pandas Series is passed, the target is derived from its name.

required
cv TimeSeriesFold

Time series cross-validation fold splitter (output of create_cv() or user-constructed) [1]_.

required
target str, list of str

Name of the column(s) to forecast. Optional only when data is a pandas Series (the Series name is used instead). For wide-format multi-series, pass a list of column names where each column is a series.

None
date_column str

Name of the column containing timestamps. When None, the index of data is assumed to be a DatetimeIndex.

None
series_id_column str

Name of the column identifying individual series (long-format multi-series input). When None, the data is treated as single-series or wide-format multi-series.

None
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] (e.g. [0.1, 0.9] for 80 % interval). When None, no prediction intervals are computed.

None
forecaster str

Explicit forecaster class name to use instead of the recommended one (e.g. 'ForecasterRecursive', 'ForecasterDirect', 'ForecasterRecursiveMultiSeries'). When None, the most suitable forecaster is selected automatically from the characteristics of the data.

None
estimator str

Explicit regressor class name to use instead of the recommended one (e.g. 'HistGradientBoostingRegressor', 'LGBMRegressor'). When None, a suitable estimator is selected automatically based on the dataset size.

None
estimator_kwargs dict

Keyword arguments for the estimator constructor (e.g. {'n_estimators': 200, 'learning_rate': 0.05}). Merged on top of built-in defaults (random_state and silencing flags), with user values taking precedence. When None, only the built-in defaults are used.

None
profile ForecastingProfile

Pre-computed profile to skip profiling.

None
plan ForecastPlan

Pre-computed plan to skip planning.

None
show_progress bool

Whether to display a progress bar during backtesting.

True

Returns:

Name Type Description
result BacktestResult

Backtesting outcome and the decisions behind it. Contains the following attributes:

  • profile: profile of the input dataset and high-level modeling decisions.
  • plan: detailed forecasting plan that was executed.
  • cv_config: resolved TimeSeriesFold parameters plus the resulting n_folds.
  • metrics: backtesting metric values returned by skforecast.
  • predictions: full backtest predictions across all folds.
  • code: generated Python script reproducing the workflow.
  • explanation: human-readable summary of the configuration and results.
Notes

The data DataFrame must include exogenous columns if the plan uses them. Exogenous variables are extracted automatically from profile.data_profile.exog_columns.

To customize lags or window_features, build the plan with plan() (or refine_plan()) and pass it via plan, then build a matching cv with create_cv(). This keeps the plan and the cross-validation configuration consistent.

References

[1] Skforecast TimeSeriesFold API Reference: https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

Source code in skforecast_ai/assistant.py
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
def backtest(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    cv: TimeSeriesFold,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    interval: list[float] | None = None,
    forecaster: str | None = None,
    estimator: str | None = None,
    estimator_kwargs: dict | None = None,
    profile: ForecastingProfile | None = None,
    plan: ForecastPlan | None = None,
    show_progress: bool = True,
) -> BacktestResult:
    """
    Execute backtesting with a pre-configured time series cross-validation 
    strategy (`TimeSeriesFold` [1]_).

    Chains `profile()`, `plan()`, and backtesting execution
    using the provided `TimeSeriesFold`. The `steps` parameter is
    inferred from `cv.steps`.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file. When a
        pandas Series is passed, the target is derived from its name.
    cv : TimeSeriesFold
        Time series cross-validation fold splitter (output of `create_cv()`
        or user-constructed) [1]_.
    target : str, list of str, default None
        Name of the column(s) to forecast. Optional only when `data`
        is a pandas Series (the Series name is used instead). For
        wide-format multi-series, pass a list of column names where
        each column is a series.
    date_column : str, default None
        Name of the column containing timestamps. When None, the
        index of `data` is assumed to be a DatetimeIndex.
    series_id_column : str, default None
        Name of the column identifying individual series (long-format
        multi-series input). When None, the data is treated as
        single-series or wide-format multi-series.
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` (e.g. `[0.1, 0.9]` for 80 % interval). When
        None, no prediction intervals are computed.
    forecaster : str, default None
        Explicit forecaster class name to use instead of the
        recommended one (e.g. `'ForecasterRecursive'`,
        `'ForecasterDirect'`, `'ForecasterRecursiveMultiSeries'`).
        When None, the most suitable forecaster is selected
        automatically from the characteristics of the data.
    estimator : str, default None
        Explicit regressor class name to use instead of the
        recommended one (e.g. `'HistGradientBoostingRegressor'`,
        `'LGBMRegressor'`). When None, a suitable estimator is
        selected automatically based on the dataset size.
    estimator_kwargs : dict, default None
        Keyword arguments for the estimator constructor (e.g.
        `{'n_estimators': 200, 'learning_rate': 0.05}`). Merged on
        top of built-in defaults (`random_state` and silencing
        flags), with user values taking precedence. When None, only
        the built-in defaults are used.
    profile : ForecastingProfile, default None
        Pre-computed profile to skip profiling.
    plan : ForecastPlan, default None
        Pre-computed plan to skip planning.
    show_progress : bool, default True
        Whether to display a progress bar during backtesting.

    Returns
    -------
    result : BacktestResult
        Backtesting outcome and the decisions behind it. Contains the
        following attributes:

        - profile: profile of the input dataset and high-level
        modeling decisions.
        - plan: detailed forecasting plan that was executed.
        - cv_config: resolved `TimeSeriesFold` parameters plus the
        resulting `n_folds`.
        - metrics: backtesting metric values returned by skforecast.
        - predictions: full backtest predictions across all folds.
        - code: generated Python script reproducing the workflow.
        - explanation: human-readable summary of the configuration
        and results.

    Notes
    -----
    The `data` DataFrame must include exogenous columns if the plan
    uses them. Exogenous variables are extracted automatically from
    `profile.data_profile.exog_columns`.

    To customize `lags` or `window_features`, build the plan with
    `plan()` (or `refine_plan()`) and pass it via `plan`, then build a
    matching `cv` with `create_cv()`. This keeps the plan and the
    cross-validation configuration consistent.

    References
    ----------
    [1] Skforecast `TimeSeriesFold` API Reference:
        https://skforecast.org/latest/api/model_selection#skforecast.model_selection._split.TimeSeriesFold

    """

    data_df, target = _resolve_data_and_target(data, target)

    profile, plan = self._prepare_backtest(
        data             = data_df,
        target           = target,
        cv               = cv,
        date_column      = date_column,
        series_id_column = series_id_column,
        interval         = interval,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        profile          = profile,
        plan             = plan,
    )

    # Build human-readable CV explanation
    span_index_length = profile.data_profile.span_index_length
    n_folds = _count_cv_folds(
                  cv             = cv,
                  n_observations = span_index_length,
                  start_date     = profile.data_profile.start_date,
                  frequency      = profile.data_profile.frequency,
              )

    # Extract cv_config after split. `n_folds` is stored alongside the
    # `TimeSeriesFold` parameters because it is the fact a reader (or the
    # LLM) actually needs; leaving it out forced it to be re-derived from
    # the prediction row count.
    cv_config = {
        "steps": cv.steps,
        "initial_train_size": cv.initial_train_size,
        "refit": cv.refit,
        "fixed_train_size": cv.fixed_train_size,
        "gap": cv.gap,
        "fold_stride": cv.fold_stride,
        "differentiation": cv.differentiation,
        "n_folds": n_folds,
    }
    cv_explanation = build_cv_explanation(
                         cv_params      = cv_config,
                         n_observations = span_index_length,
                         n_folds        = n_folds,
                     )

    result = run_backtest(
        data           = data_df,
        profile        = profile.data_profile,
        plan           = plan,
        cv             = cv,
        cv_explanation = cv_explanation,
        show_progress  = show_progress,
    )

    return BacktestResult(
        profile     = profile,
        plan        = plan,
        cv_config   = cv_config,
        metrics     = result["metrics"],
        predictions = result["predictions"],
        code        = result["rendered_code"].full_script,
        explanation = result["explanation"],
    )

compare

compare(
    data,
    cv,
    target=None,
    date_column=None,
    series_id_column=None,
    candidates=None,
    metric=None,
    interval=None,
    profile=None,
    show_progress=True,
)

Compare several forecaster configurations on the same data.

Backtests each candidate configuration with the same cross-validation strategy and returns a metric-ranked leaderboard plus the winning configuration as a reusable BacktestResult.

The dataset is profiled once and the profile is shared across all candidates; only the plan varies per candidate. Each candidate is evaluated through the same plan() + backtest() path, so every row carries a reproducible code. Ranking is a pure ascending sort of the metric column (all default metrics are error metrics where lower is better); the LLM never influences the outcome.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file. When a pandas Series is passed, the target is derived from its name.

required
cv TimeSeriesFold

Cross-validation strategy applied identically to every candidate. The steps value is inferred from cv.steps.

required
target str, list of str

Name of the column(s) to forecast. Optional only when data is a pandas Series (the Series name is used instead). For wide-format multi-series, pass a list of column names.

None
date_column str

Name of the column containing timestamps. When None, the index of data is assumed to be a DatetimeIndex.

None
series_id_column str

Name of the column identifying individual series (long-format multi-series input).

None
candidates list of tuple of (str, dict)

Configurations to compare. Each entry is a (name, config) tuple, where name labels the row in the results table and config holds the forecaster/estimator settings. The config dict accepts the same override keys understood by plan(): 'forecaster', 'estimator', 'estimator_kwargs', 'lags', and 'window_features'. Names must be unique. When None, the set is built automatically from profile.forecaster_candidates.

None
metric str, list of str

Metric(s) computed per candidate. When a list is passed, the first metric is used to rank the table. When None, the plan default metric (and its metric panel) is used.

None
interval list of float

Prediction interval quantiles as a two-element list [lower, upper] computed for every candidate. When None, no prediction intervals are computed.

None
profile ForecastingProfile

Pre-computed profile to skip profiling and guarantee a shared profile across candidates.

None
show_progress bool

Whether to display a progress bar across candidates.

True

Returns:

Name Type Description
result ComparisonResult

Ranked comparison of the candidate configurations. Contains the following attributes:

  • profile: shared profile used for every candidate.
  • cv_config: resolved TimeSeriesFold parameters plus the resulting n_folds, applied identically to every candidate.
  • results: ranked comparison table, one row per candidate sorted best to worst by ranking_metric.
  • candidates: mapping of candidate name to the full BacktestResult of every candidate that ran successfully, ordered best to worst.
  • failures: mapping of candidate name to a CandidateFailure describing why it failed. Empty when all candidates succeed.
  • ranking_metric: name of the metric used to sort results.
  • explanation: human-readable summary of the comparison.
  • best_name: name of the top-ranked candidate.
  • best_candidate: top-ranked candidate as a BacktestResult.

Raises:

Type Description
AllCandidatesFailedError

If every candidate fails to run. The individual failures are available on the failures attribute of the raised error.

ValueError

If metric is an empty list, or if candidates is empty, contains a malformed entry, or repeats a name.

Warns:

Type Description
CandidateFailedWarning

Once per failed candidate.

Notes

If a candidate fails to run, a CandidateFailedWarning is issued, its row records the error and is sorted last, and a CandidateFailure carrying the full traceback and the generated code is kept in failures. One bad configuration therefore never aborts the whole comparison. Ties keep input order, so the table is deterministic.

The winning configuration, best_candidate, is a full BacktestResult carrying both a profile and a plan, which can be fed directly into forecast(), backtest(), or forecast_code().

Source code in skforecast_ai/assistant.py
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
def compare(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    cv: TimeSeriesFold,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    candidates: list[tuple[str, dict]] | None = None,
    metric: str | list[str] | None = None,
    interval: list[float] | None = None,
    profile: ForecastingProfile | None = None,
    show_progress: bool = True,
) -> ComparisonResult:
    """
    Compare several forecaster configurations on the same data.

    Backtests each candidate configuration with the **same**
    cross-validation strategy and returns a metric-ranked leaderboard
    plus the winning configuration as a reusable `BacktestResult`.

    The dataset is profiled once and the profile is shared across all
    candidates; only the plan varies per candidate. Each candidate is
    evaluated through the same `plan()` + `backtest()` path, so every
    row carries a reproducible `code`. Ranking is a pure ascending sort
    of the metric column (all default metrics are error metrics where
    lower is better); the LLM never influences the outcome.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file. When a
        pandas Series is passed, the target is derived from its name.
    cv : TimeSeriesFold
        Cross-validation strategy applied identically to every
        candidate. The `steps` value is inferred from `cv.steps`.
    target : str, list of str, default None
        Name of the column(s) to forecast. Optional only when `data`
        is a pandas Series (the Series name is used instead). For
        wide-format multi-series, pass a list of column names.
    date_column : str, default None
        Name of the column containing timestamps. When None, the index
        of `data` is assumed to be a DatetimeIndex.
    series_id_column : str, default None
        Name of the column identifying individual series (long-format
        multi-series input).
    candidates : list of tuple of (str, dict), default None
        Configurations to compare. Each entry is a `(name, config)`
        tuple, where `name` labels the row in the results table and
        `config` holds the forecaster/estimator settings. The `config`
        dict accepts the same override keys understood by `plan()`:
        `'forecaster'`, `'estimator'`, `'estimator_kwargs'`, `'lags'`,
        and `'window_features'`. Names must be unique. When None, the
        set is built automatically from
        `profile.forecaster_candidates`.
    metric : str, list of str, default None
        Metric(s) computed per candidate. When a list is passed, the
        first metric is used to rank the table. When None, the plan
        default metric (and its metric panel) is used.
    interval : list of float, default None
        Prediction interval quantiles as a two-element list
        `[lower, upper]` computed for every candidate. When None, no
        prediction intervals are computed.
    profile : ForecastingProfile, default None
        Pre-computed profile to skip profiling and guarantee a shared
        profile across candidates.
    show_progress : bool, default True
        Whether to display a progress bar across candidates.

    Returns
    -------
    result : ComparisonResult
        Ranked comparison of the candidate configurations. Contains
        the following attributes:

        - profile: shared profile used for every candidate.
        - cv_config: resolved `TimeSeriesFold` parameters plus the
        resulting `n_folds`, applied
        identically to every candidate.
        - results: ranked comparison table, one row per candidate
        sorted best to worst by `ranking_metric`.
        - candidates: mapping of candidate name to the full
        `BacktestResult` of every candidate that ran successfully,
        ordered best to worst.
        - failures: mapping of candidate name to a `CandidateFailure`
        describing why it failed. Empty when all candidates succeed.
        - ranking_metric: name of the metric used to sort `results`.
        - explanation: human-readable summary of the comparison.
        - best_name: name of the top-ranked candidate.
        - best_candidate: top-ranked candidate as a `BacktestResult`.

    Raises
    ------
    AllCandidatesFailedError
        If every candidate fails to run. The individual failures are
        available on the `failures` attribute of the raised error.
    ValueError
        If `metric` is an empty list, or if `candidates` is empty,
        contains a malformed entry, or repeats a name.

    Warns
    -----
    CandidateFailedWarning
        Once per failed candidate.

    Notes
    -----
    If a candidate fails to run, a `CandidateFailedWarning` is issued,
    its row records the error and is sorted last, and a
    `CandidateFailure` carrying the full traceback and the generated
    code is kept in `failures`. One bad configuration therefore never
    aborts the whole comparison. Ties keep input order, so the table
    is deterministic.

    The winning configuration, `best_candidate`, is a full
    `BacktestResult` carrying both a `profile` and a `plan`, which can
    be fed directly into `forecast()`, `backtest()`, or
    `forecast_code()`.
    """

    data_df, target = _resolve_data_and_target(data, target)

    if profile is None:
        profile = self.profile(
            data             = data_df,
            target           = target,
            date_column      = date_column,
            series_id_column = series_id_column,
        )

    candidate_configs = self._resolve_compare_candidates(candidates, profile)

    # Resolve the ranking metric and the metric columns once, so the
    # table is consistent across candidates regardless of which ones
    # succeed. When `metric` is None the deterministic plan default is
    # used; otherwise the requested metrics override the plan panel.
    if metric is None:
        metric_override = None
        ranking_metric, _, metric_columns = select_metric(
            profile.data_profile
        )
    else:
        metric_override = [metric] if isinstance(metric, str) else list(metric)
        if not metric_override:
            raise ValueError("`metric` must not be an empty list.")
        ranking_metric = metric_override[0]
        metric_columns = metric_override

    steps = cv.steps

    # Human-readable description of the shared CV strategy. The folds
    # are counted before any candidate runs, on the untouched `cv`.
    span_index_length = profile.data_profile.span_index_length
    n_folds = _count_cv_folds(
                  cv             = cv,
                  n_observations = span_index_length,
                  start_date     = profile.data_profile.start_date,
                  frequency      = profile.data_profile.frequency,
              )
    cv_config = {
        "steps": cv.steps,
        "initial_train_size": cv.initial_train_size,
        "refit": cv.refit,
        "fixed_train_size": cv.fixed_train_size,
        "gap": cv.gap,
        "fold_stride": cv.fold_stride,
        "differentiation": cv.differentiation,
        "n_folds": n_folds,
    }
    cv_explanation = build_cv_explanation(
                         cv_params      = cv_config,
                         n_observations = span_index_length,
                         n_folds        = n_folds,
                     )

    iterator: Any = candidate_configs
    if show_progress:
        from tqdm.auto import tqdm

        iterator = tqdm(candidate_configs, desc="Comparing forecasters")

    rows: list[tuple[dict, float]] = []
    ranked: list[tuple[str, BacktestResult, float]] = []
    failures: dict[str, CandidateFailure] = {}

    for name, config in iterator:
        row: dict[str, Any] = {
            "name": name,
            "forecaster": config.get("forecaster") or profile.forecaster,
            "estimator": config.get("estimator"),
            "error": None,
        }
        for col in metric_columns:
            row[col] = float("nan")
        ranking_value = float("nan")

        try:
            cand_plan = self.plan(
                profile          = profile,
                steps            = steps,
                forecaster       = config.get("forecaster"),
                estimator        = config.get("estimator"),
                estimator_kwargs = config.get("estimator_kwargs"),
                lags             = config.get("lags"),
                window_features  = config.get("window_features"),
                interval         = interval,
            )
            if metric_override is not None:
                cand_plan.metrics_to_compute = list(metric_override)
                cand_plan.metric = metric_override[0]

            row["forecaster"] = cand_plan.forecaster
            row["estimator"] = cand_plan.estimator

            bt = self.backtest(
                data             = data_df,
                cv               = cv,
                target           = target,
                date_column      = date_column,
                series_id_column = series_id_column,
                profile          = profile,
                plan             = cand_plan,
                show_progress    = False,
            )
            agg = self._aggregate_metrics(bt.metrics)
            for col in metric_columns:
                row[col] = agg.get(col, float("nan"))
            ranking_value = agg.get(ranking_metric, float("nan"))
            ranked.append((name, bt, ranking_value))
        except Exception as exc:
            failure = CandidateFailure.from_exception(exc)
            failures[name] = failure
            row["error"] = failure.summary()
            warnings.warn(
                f"Candidate '{name}' failed and is ranked last: "
                f"{failure.summary()}. The full traceback is available in "
                f"`ComparisonResult.failures['{name}'].traceback`.",
                CandidateFailedWarning,
                stacklevel=2,
            )

        rows.append((row, ranking_value))

    results = self._build_comparison_table(
        rows           = rows,
        metric_columns = metric_columns,
        any_error      = bool(failures),
    )

    # Order the successful candidates using the same
    # ascending-with-NaN-last, stable ordering as the results table, so
    # the mapping iterates best to worst and its first entry is the
    # winner reported by `best_name` / `best_candidate`.
    ranked_sorted = sorted(ranked, key=self._compare_sort_key)
    if not ranked_sorted:
        raise AllCandidatesFailedError(failures)
    candidate_results = {name: bt for name, bt, _ in ranked_sorted}

    explanation = self._build_comparison_explanation(
        n_candidates   = len(candidate_configs),
        ranked         = ranked_sorted,
        ranking_metric = ranking_metric,
        any_error      = bool(failures),
        cv_explanation = cv_explanation,
    )

    return ComparisonResult(
        profile        = profile,
        cv_config      = cv_config,
        results        = results,
        candidates     = candidate_results,
        failures       = failures,
        ranking_metric = ranking_metric,
        explanation    = explanation,
    )

ask

ask(
    prompt,
    data=None,
    target=None,
    date_column=None,
    series_id_column=None,
    profile=None,
    plan=None,
    result=None,
    steps=None,
    skills=None,
    include_reference=False,
)

Ask a forecasting question or explain a pre-computed plan.

Operates in three mutually exclusive modes:

  • Q&A mode (no data, no profile, no result): the LLM answers general forecasting or skforecast questions using its skills.
  • Explain mode (data or profile provided): deterministic profiling runs first, then the LLM explains the result.
  • Results mode (result provided): the LLM explains a completed workflow result (for example a ForecastResult, BacktestResult, or ComparisonResult). Each result renders its own context block, so the LLM receives what is relevant to that kind of result.

Results mode takes precedence: when result is provided it is the single source of truth, so data, target, date_column, series_id_column, profile, plan, and steps are ignored with an IgnoredArgumentWarning.

Parameters:

Name Type Description Default
prompt str

Natural-language question or instruction.

required
data pandas Series, pandas DataFrame, str, Path

Optional dataset, a single series, or path to a CSV file. When provided (without a pre-computed profile), triggers deterministic profiling + plan generation before the LLM call. When a pandas Series is passed, the target is derived from its name.

None
target str, list of str

Name of the target column(s). Required when data is provided and profile is None, unless data is a pandas Series (the Series name is used instead).

None
date_column str

Name of the column containing timestamps.

None
series_id_column str

Name of the column identifying individual series.

None
profile ForecastingProfile

Pre-computed profile. If provided, profiling is skipped.

None
plan ForecastPlan

Pre-computed plan. If provided, plan generation is skipped.

None
result ExplainableResult

Result from a previous workflow call (for example forecast(), backtest(), or compare()). When provided, the result renders its own context block for the LLM: a single run contributes its predictions, metrics, and any cross-validation configuration, while a ComparisonResult contributes its leaderboard, shared cross-validation strategy, and the winning candidate's plan. The returned profile, plan, and code are the result's own; for a ComparisonResult these are the shared profile and the winning candidate's plan and code. A result's own predicted values are always sent to the LLM, regardless of send_data_to_llm, since a question about a result cannot be answered from summary statistics alone. The input data is not sent: a result holds only the model's output, never the data it was fitted on.

None
steps int

Forecast horizon used when generating a plan from data. Required when data or profile is provided without a pre-computed plan. Ignored when result is provided.

None
skills list of str

List of skill names to include in the agent system prompt. If None, skills are selected automatically based on the task type and question content. See skforecast_ai.ALL_SKILLS for valid names.

None
include_reference bool

Whether to include the skforecast API reference in the prompt.

False

Returns:

Name Type Description
result AskResult

LLM response and any deterministic artifacts computed first. Contains the following attributes:

  • profile: profile of the input dataset, when data was provided.
  • plan: detailed forecasting plan, when one was produced.
  • code: generated Python script, when one was produced.
  • explanation: LLM-generated explanation or response.

Raises:

Type Description
LLMRequiredError

If no LLM was configured at init time.

TypeError

If result is not an ExplainableResult.

ValueError

If data or profile is provided without a pre-computed plan and steps is None.

Warns:

Type Description
IgnoredArgumentWarning

If result is provided together with any deterministic input it supersedes.

DataSentToLLMWarning

If result is provided while send_data_to_llm is False. Results mode always sends the result's predicted values.

Notes

An LLM must be configured at init time. When llm is None, this method cannot operate and raises LLMRequiredError.

Source code in skforecast_ai/assistant.py
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
def ask(
    self,
    prompt: str,
    data: pd.Series | pd.DataFrame | str | Path | None = None,
    target: str | list[str] | None = None,
    date_column: str | None = None,
    series_id_column: str | None = None,
    profile: ForecastingProfile | None = None,
    plan: ForecastPlan | None = None,
    result: ExplainableResult | None = None,
    steps: int | None = None,
    skills: list[str] | None = None,
    include_reference: bool = False,
) -> AskResult:
    """
    Ask a forecasting question or explain a pre-computed plan.

    Operates in three mutually exclusive modes:

    - Q&A mode (no data, no profile, no result): the LLM answers
      general forecasting or skforecast questions using its skills.
    - Explain mode (data or profile provided): deterministic
      profiling runs first, then the LLM explains the result.
    - Results mode (`result` provided): the LLM explains a completed
      workflow result (for example a `ForecastResult`,
      `BacktestResult`, or `ComparisonResult`). Each result renders
      its own context block, so the LLM receives what is relevant to
      that kind of result.

    Results mode takes precedence: when `result` is provided it is the
    single source of truth, so `data`, `target`, `date_column`,
    `series_id_column`, `profile`, `plan`, and `steps` are ignored
    with an `IgnoredArgumentWarning`.

    Parameters
    ----------
    prompt : str
        Natural-language question or instruction.
    data : pandas Series, pandas DataFrame, str, Path, default None
        Optional dataset, a single series, or path to a CSV file. When
        provided (without a pre-computed profile), triggers
        deterministic profiling + plan generation before the LLM call.
        When a pandas Series is passed, the target is derived from its
        name.
    target : str, list of str, default None
        Name of the target column(s). Required when `data` is
        provided and `profile` is None, unless `data` is a pandas
        Series (the Series name is used instead).
    date_column : str, default None
        Name of the column containing timestamps.
    series_id_column : str, default None
        Name of the column identifying individual series.
    profile : ForecastingProfile, default None
        Pre-computed profile. If provided, profiling is skipped.
    plan : ForecastPlan, default None
        Pre-computed plan. If provided, plan generation is skipped.
    result : ExplainableResult, default None
        Result from a previous workflow call (for example
        `forecast()`, `backtest()`, or `compare()`). When provided,
        the result renders its own context block for the LLM:
        a single run contributes its predictions, metrics, and any
        cross-validation configuration, while a `ComparisonResult`
        contributes its leaderboard, shared cross-validation
        strategy, and the winning candidate's plan. The returned
        `profile`, `plan`, and `code` are the result's own; for a
        `ComparisonResult` these are the shared profile and the
        winning candidate's plan and code. A result's own predicted
        values are always sent to the LLM, regardless of
        `send_data_to_llm`, since a question about a result cannot be
        answered from summary statistics alone. The input data is not
        sent: a result holds only the model's output, never the data
        it was fitted on.
    steps : int, default None
        Forecast horizon used when generating a plan from data.
        Required when `data` or `profile` is provided
        without a pre-computed `plan`. Ignored when `result` is
        provided.
    skills : list of str, default None
        List of skill names to include in the agent system prompt.
        If None, skills are selected automatically based on the
        task type and question content. See `skforecast_ai.ALL_SKILLS`
        for valid names.
    include_reference : bool, default False
        Whether to include the skforecast API reference in the
        prompt.

    Returns
    -------
    result : AskResult
        LLM response and any deterministic artifacts computed first.
        Contains the following attributes:

        - profile: profile of the input dataset, when data was
        provided.
        - plan: detailed forecasting plan, when one was produced.
        - code: generated Python script, when one was produced.
        - explanation: LLM-generated explanation or response.

    Raises
    ------
    LLMRequiredError
        If no LLM was configured at init time.
    TypeError
        If `result` is not an `ExplainableResult`.
    ValueError
        If `data` or `profile` is provided without a pre-computed
        `plan` and `steps` is None.

    Warns
    -----
    IgnoredArgumentWarning
        If `result` is provided together with any deterministic input
        it supersedes.
    DataSentToLLMWarning
        If `result` is provided while `send_data_to_llm` is False.
        Results mode always sends the result's predicted values.

    Notes
    -----
    An LLM must be configured at init time. When `llm` is None,
    this method cannot operate and raises `LLMRequiredError`.
    """

    if self.llm is None:
        raise LLMRequiredError("ask")

    if result is not None and not isinstance(result, ExplainableResult):
        raise TypeError(
            f"`result` must be an `ExplainableResult` (for example "
            f"`ForecastResult`, `BacktestResult`, or `ComparisonResult`), "
            f"got {type(result).__name__}."
        )

    # --- Deterministic stage ---
    # Results mode and explain mode are exclusive branches, so the
    # profile, plan, code, and context always describe one single
    # state. A result is the sole source of truth: it already carries
    # the profile and plan it was produced with, and it renders its
    # own context block, so `ask()` never needs to know the shape of a
    # particular result.
    if result is not None:
        _warn_if_result_inputs_ignored(
            data             = data,
            target           = target,
            date_column      = date_column,
            series_id_column = series_id_column,
            profile          = profile,
            plan             = plan,
            steps            = steps,
        )
        # In results mode, raw data is always sent so the LLM can
        # discuss specific values. This overrides `send_data_to_llm`,
        # so say so: a user who set it to False for privacy reasons
        # would otherwise ship predicted values without being told.
        if not self.send_data_to_llm:
            warnings.warn(
                "`send_data_to_llm=False` does not apply to `result`: the "
                "predicted values it carries are sent to the LLM, because "
                "a question about a result cannot be answered from "
                "summary statistics alone. Your input data is not sent: a "
                "result holds only the model's output, never the data it "
                "was fitted on. To keep predictions local, ask without "
                "`result`.",
                DataSentToLLMWarning,
                stacklevel=2,
            )
        result_context = result.to_llm_context(send_data=True)
        profile        = result_context.profile
        plan           = result_context.plan
        generated_code = result_context.code
        context        = result_context.text
    else:
        if data is not None and profile is None:
            profile = self.profile(
                data             = data,
                target           = target,
                date_column      = date_column,
                series_id_column = series_id_column,
            )
        if profile is not None and plan is None:
            if steps is None:
                raise ValueError(
                    "`steps` is required when `data` or "
                    "`profile` is provided without a "
                    "pre-computed `plan`."
                )
            plan = self.plan(profile, steps=steps)

        if plan is not None and profile is not None:
            generated_code = render_forecast_script(
                profile=profile.data_profile, plan=plan
            ).full_script
        else:
            generated_code = None

        context = build_context_message(
            profile, plan, send_data=self.send_data_to_llm
        )

    # --- Pre-flight check for Ollama ---
    if self.llm.startswith("ollama:"):
        ensure_ollama_reachable(self.base_url)

    # --- Build user message with context ---
    # The question is delimited so it cannot be mistaken for part of
    # the deterministic context block that precedes it.
    user_message = (
        f"{context}\n\n<question>\n{prompt}\n</question>"
        if context
        else prompt
    )

    # --- Dynamic skill selection when not explicitly provided ---
    resolved_skills = skills
    if resolved_skills is None:
        task_type = (
            profile.task_type
            if profile is not None
            else None
        )
        # Budget the skills against the space the context block has
        # already taken. Only local models expose a context window
        # known up front; hosted windows are provider specific and
        # generally far larger, so no budget is imposed for them and
        # the full selection is sent.
        token_budget = None
        if self.llm.startswith("ollama:"):
            token_budget = compute_skill_token_budget(
                max_context_tokens = OLLAMA_MAX_CONTEXT_TOKENS,
                context_tokens     = estimate_context_tokens(user_message),
                include_reference  = include_reference,
            )
        resolved_skills = select_skills(
            task_type    = task_type,
            question     = prompt,
            token_budget = token_budget,
        )

    # --- LLM call ---
    from .llm import AskDeps

    agent = self._resolve_agent()
    deps = AskDeps(
        profile           = profile,
        plan              = plan,
        skills            = resolved_skills,
        include_reference = include_reference,
    )

    estimated_tokens = estimate_prompt_tokens(
        resolved_skills, include_reference
    )
    model_settings = self._build_ollama_settings(
        estimated_tokens, user_message
    )

    try:
        agent_result = _run_agent_sync(
            agent,
            user_message,
            deps=deps,
            model_settings=model_settings,
        )
        explanation = agent_result.output
    except Exception as exc:
        warnings.warn(
            f"LLM call failed ({exc}), returning deterministic result.",
            UserWarning,
            stacklevel=2,
        )
        if plan is not None:
            explanation = f"[LLM unavailable] {plan.explanation}"
        else:
            explanation = f"[LLM unavailable] {exc}"
    else:
        # Strip code blocks in Explain/Results mode (validated code
        # exists). Kept out of the `try` so a post-processing failure
        # is not reported as a failed LLM call.
        if generated_code is not None:
            explanation = _strip_code_blocks(explanation)

    return AskResult(
        profile     = profile,
        plan        = plan,
        code        = generated_code,
        explanation = explanation,
    )

_prepare_backtest

_prepare_backtest(
    data,
    cv,
    target,
    date_column,
    series_id_column,
    interval,
    forecaster,
    estimator,
    estimator_kwargs,
    profile,
    plan,
)

Resolve profile and plan for backtesting workflows.

Shared preparation logic used by both backtest_code() and backtest(). Coerces data, auto-generates profile/plan when not provided, and validates that cv.steps matches plan.steps when a plan is explicitly passed.

Parameters:

Name Type Description Default
data pandas Series, pandas DataFrame, str, Path

Input dataset, a single series, or path to a CSV file.

required
cv TimeSeriesFold

Cross-validation fold splitter.

required
target str, list of str, None

Name of the column(s) to forecast. Optional only when data is a pandas Series (the Series name is used instead).

required
date_column (str, None)

Name of the column containing timestamps.

required
series_id_column (str, None)

Name of the column identifying individual series.

required
interval list of float, None

Prediction interval quantiles.

required
forecaster (str, None)

Explicit forecaster class name override.

required
estimator (str, None)

Explicit estimator class name override.

required
estimator_kwargs (dict, None)

Keyword arguments for the estimator constructor.

required
profile (ForecastingProfile, None)

Pre-computed profile.

required
plan (ForecastPlan, None)

Pre-computed plan.

required

Returns:

Name Type Description
profile ForecastingProfile

Resolved profile.

plan ForecastPlan

Resolved plan.

Source code in skforecast_ai/assistant.py
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
def _prepare_backtest(
    self,
    data: pd.Series | pd.DataFrame | str | Path,
    cv: TimeSeriesFold,
    target: str | list[str] | None,
    date_column: str | None,
    series_id_column: str | None,
    interval: list[float] | None,
    forecaster: str | None,
    estimator: str | None,
    estimator_kwargs: dict | None,
    profile: ForecastingProfile | None,
    plan: ForecastPlan | None,
) -> tuple[ForecastingProfile, ForecastPlan]:
    """
    Resolve profile and plan for backtesting workflows.

    Shared preparation logic used by both `backtest_code()` and
    `backtest()`. Coerces data, auto-generates profile/plan when
    not provided, and validates that `cv.steps` matches `plan.steps`
    when a plan is explicitly passed.

    Parameters
    ----------
    data : pandas Series, pandas DataFrame, str, Path
        Input dataset, a single series, or path to a CSV file.
    cv : TimeSeriesFold
        Cross-validation fold splitter.
    target : str, list of str, None
        Name of the column(s) to forecast. Optional only when `data`
        is a pandas Series (the Series name is used instead).
    date_column : str, None
        Name of the column containing timestamps.
    series_id_column : str, None
        Name of the column identifying individual series.
    interval : list of float, None
        Prediction interval quantiles.
    forecaster : str, None
        Explicit forecaster class name override.
    estimator : str, None
        Explicit estimator class name override.
    estimator_kwargs : dict, None
        Keyword arguments for the estimator constructor.
    profile : ForecastingProfile, None
        Pre-computed profile.
    plan : ForecastPlan, None
        Pre-computed plan.

    Returns
    -------
    profile : ForecastingProfile
        Resolved profile.
    plan : ForecastPlan
        Resolved plan.
    """

    _warn_if_plan_overrides_ignored(
        plan             = plan,
        forecaster       = forecaster,
        estimator        = estimator,
        estimator_kwargs = estimator_kwargs,
        interval         = interval,
    )

    data_df, target = _resolve_data_and_target(data, target)
    steps = cv.steps

    if profile is None:
        profile = self.profile(
            data             = data_df,
            target           = target,
            date_column      = date_column,
            series_id_column = series_id_column,
        )

    if plan is None:
        plan = self.plan(
            profile          = profile,
            steps            = steps,
            forecaster       = forecaster,
            estimator        = estimator,
            estimator_kwargs = estimator_kwargs,
            interval         = interval,
        )
    else:
        if cv.steps != plan.steps:
            raise ValueError(
                f"cv.steps ({cv.steps}) does not match plan.steps "
                f"({plan.steps}). These must be equal — "
                f"ForecasterDirect and ForecasterDirectMultiVariate "
                f"model architectures depend on steps."
            )

    return profile, plan

_resolve_compare_candidates staticmethod

_resolve_compare_candidates(candidates, profile)

Resolve the candidate configurations for compare().

When candidates is None, the candidates are derived from profile.forecaster_candidates, each labelled by its forecaster class name. Otherwise the user-supplied (name, config) tuples are validated. Names must be unique in both cases, since they key the candidates and failures mappings of the result.

Parameters:

Name Type Description Default
candidates list of tuple of (str, dict), None

User-supplied configurations, or None to auto-build.

required
profile ForecastingProfile

Shared profile used to derive the auto candidates.

required

Returns:

Name Type Description
resolved list of tuple of (str, dict)

Validated (name, config) tuples.

Source code in skforecast_ai/assistant.py
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
@staticmethod
def _resolve_compare_candidates(
    candidates: list[tuple[str, dict]] | None,
    profile: ForecastingProfile,
) -> list[tuple[str, dict]]:
    """
    Resolve the candidate configurations for `compare()`.

    When `candidates` is None, the candidates are derived from
    `profile.forecaster_candidates`, each labelled by its forecaster
    class name. Otherwise the user-supplied `(name, config)` tuples
    are validated. Names must be unique in both cases, since they key
    the `candidates` and `failures` mappings of the result.

    Parameters
    ----------
    candidates : list of tuple of (str, dict), None
        User-supplied configurations, or None to auto-build.
    profile : ForecastingProfile
        Shared profile used to derive the auto candidates.

    Returns
    -------
    resolved : list of tuple of (str, dict)
        Validated `(name, config)` tuples.
    """

    allowed_keys = {
        "forecaster",
        "estimator",
        "estimator_kwargs",
        "lags",
        "window_features",
    }

    resolved: list[tuple[str, dict]] = []
    if candidates is None:
        if not profile.forecaster_candidates:
            raise ValueError(
                "Profile has no forecaster candidates to compare. "
                "Pass an explicit `candidates` list."
            )
        resolved = [
            (fc, {"forecaster": fc})
            for fc in profile.forecaster_candidates
        ]
    else:
        if not candidates:
            raise ValueError("`candidates` must not be an empty list.")

        for entry in candidates:
            if not isinstance(entry, (tuple, list)) or len(entry) != 2:
                raise ValueError(
                    "Each entry in `candidates` must be a (name, config) "
                    f"tuple, got {entry!r}."
                )
            name, config = entry
            if not isinstance(config, dict):
                raise TypeError(
                    f"Configuration for '{name}' must be a dict, got "
                    f"{type(config).__name__}."
                )
            invalid_keys = set(config) - allowed_keys
            if invalid_keys:
                raise ValueError(
                    f"Invalid config keys for '{name}': "
                    f"{sorted(invalid_keys)}. Allowed keys: "
                    f"{sorted(allowed_keys)}."
                )
            resolved.append((str(name), config))

    names = [name for name, _ in resolved]
    duplicates = sorted({name for name in names if names.count(name) > 1})
    if duplicates:
        raise ValueError(
            f"Candidate names must be unique, found duplicates: "
            f"{duplicates}."
        )

    return resolved

_aggregate_metrics staticmethod

_aggregate_metrics(metrics)

Reduce a backtest metrics DataFrame to one scalar per metric.

For single-series tasks the single row is used directly. For multi-series tasks the skforecast 'average' aggregate row is used, falling back to the first row when it is absent.

Parameters:

Name Type Description Default
metrics pandas DataFrame, None

Backtest metrics returned by skforecast.

required

Returns:

Name Type Description
aggregated dict

Mapping of metric name to a scalar value.

Source code in skforecast_ai/assistant.py
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
@staticmethod
def _aggregate_metrics(metrics: pd.DataFrame | None) -> dict[str, Any]:
    """
    Reduce a backtest metrics DataFrame to one scalar per metric.

    For single-series tasks the single row is used directly. For
    multi-series tasks the skforecast `'average'` aggregate row is
    used, falling back to the first row when it is absent.

    Parameters
    ----------
    metrics : pandas DataFrame, None
        Backtest metrics returned by skforecast.

    Returns
    -------
    aggregated : dict
        Mapping of metric name to a scalar value.
    """

    if metrics is None or len(metrics) == 0:
        return {}
    if "levels" in metrics.columns:
        average = metrics[metrics["levels"] == "average"]
        row = average.iloc[0] if not average.empty else metrics.iloc[0]
        return {c: row[c] for c in metrics.columns if c != "levels"}
    row = metrics.iloc[0]
    return {c: row[c] for c in metrics.columns}

_compare_sort_key staticmethod

_compare_sort_key(item)

Sort key placing NaN ranking values last while keeping order.

Parameters:

Name Type Description Default
item tuple

A (name, backtest, ranking_value) tuple whose third element is the ranking value.

required

Returns:

Name Type Description
key tuple of (bool, float)

(is_nan, value) so NaN entries sort last and finite values sort ascending.

Source code in skforecast_ai/assistant.py
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
@staticmethod
def _compare_sort_key(item: tuple[Any, Any, Any]) -> tuple[bool, float]:
    """
    Sort key placing NaN ranking values last while keeping order.

    Parameters
    ----------
    item : tuple
        A `(name, backtest, ranking_value)` tuple whose third element
        is the ranking value.

    Returns
    -------
    key : tuple of (bool, float)
        `(is_nan, value)` so NaN entries sort last and finite values
        sort ascending.
    """

    value = item[2]
    is_nan = bool(pd.isna(value))
    return (is_nan, 0.0 if is_nan else float(value))

_build_comparison_table

_build_comparison_table(rows, metric_columns, any_error)

Assemble and rank the compare() results table.

Parameters:

Name Type Description Default
rows list of tuple of (dict, float)

Per-candidate (row, ranking_value) pairs.

required
metric_columns list of str

Metric column names, in display order.

required
any_error bool

Whether at least one candidate failed (controls the 'error' column).

required

Returns:

Name Type Description
results pandas DataFrame

Ranked table sorted best to worst by the ranking value, with a leading 'rank' column.

Source code in skforecast_ai/assistant.py
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
def _build_comparison_table(
    self,
    rows: list[tuple[dict, float]],
    metric_columns: list[str],
    any_error: bool,
) -> pd.DataFrame:
    """
    Assemble and rank the `compare()` results table.

    Parameters
    ----------
    rows : list of tuple of (dict, float)
        Per-candidate `(row, ranking_value)` pairs.
    metric_columns : list of str
        Metric column names, in display order.
    any_error : bool
        Whether at least one candidate failed (controls the `'error'`
        column).

    Returns
    -------
    results : pandas DataFrame
        Ranked table sorted best to worst by the ranking value, with a
        leading `'rank'` column.
    """

    results = pd.DataFrame([row for row, _ in rows])
    results["_rank_value"] = [value for _, value in rows]
    results = results.sort_values(
        "_rank_value",
        ascending    = True,
        na_position  = "last",
        kind         = "stable",
    ).reset_index(drop=True)
    results.insert(0, "rank", range(1, len(results) + 1))

    ordered = ["rank", "name", "forecaster", "estimator", *metric_columns]
    if any_error:
        ordered.append("error")
    return results[ordered]

_build_comparison_explanation staticmethod

_build_comparison_explanation(
    n_candidates,
    ranked,
    ranking_metric,
    any_error,
    cv_explanation,
)

Build the deterministic compare() summary explanation.

Parameters:

Name Type Description Default
n_candidates int

Total number of candidates evaluated.

required
ranked list of tuple of (str, BacktestResult, float)

Successful candidates ordered best to worst. Never empty: a comparison with no successful candidate raises instead.

required
ranking_metric str

Metric used to rank the table.

required
any_error bool

Whether at least one candidate failed.

required
cv_explanation str

Description of the shared cross-validation strategy.

required

Returns:

Name Type Description
explanation str

Human-readable summary of the comparison.

Source code in skforecast_ai/assistant.py
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
@staticmethod
def _build_comparison_explanation(
    n_candidates: int,
    ranked: list[tuple[str, BacktestResult, float]],
    ranking_metric: str,
    any_error: bool,
    cv_explanation: str,
) -> str:
    """
    Build the deterministic `compare()` summary explanation.

    Parameters
    ----------
    n_candidates : int
        Total number of candidates evaluated.
    ranked : list of tuple of (str, BacktestResult, float)
        Successful candidates ordered best to worst. Never empty: a
        comparison with no successful candidate raises instead.
    ranking_metric : str
        Metric used to rank the table.
    any_error : bool
        Whether at least one candidate failed.
    cv_explanation : str
        Description of the shared cross-validation strategy.

    Returns
    -------
    explanation : str
        Human-readable summary of the comparison.
    """

    best_name, best_result, best_value = ranked[0]
    label = best_result.plan.forecaster
    if best_result.plan.estimator:
        label += f" / {best_result.plan.estimator}"

    metrics = best_result.metrics
    pooled = (
        metrics is not None
        and "levels" in metrics.columns
        and bool((metrics["levels"] == "average").any())
    )
    metric_desc = ranking_metric
    if pooled:
        metric_desc += " pooled across series"

    noun = "configuration" if n_candidates == 1 else "configurations"
    best_sentence = f"Best: '{best_name}' ({label}) = {best_value:.4f}"
    if len(ranked) > 1:
        # The runner-up is only quoted when its value is usable: the
        # table sorts NaN last, so a non-finite runner-up carries no
        # information about the margin.
        runner_name, _, runner_value = ranked[1]
        if np.isfinite(runner_value):
            if np.isfinite(best_value) and runner_value != 0:
                margin = 100 * (runner_value - best_value) / abs(runner_value)
                best_sentence += (
                    f", {margin:.1f}% ahead of '{runner_name}' "
                    f"({runner_value:.4f})"
                )
            else:
                best_sentence += (
                    f", ahead of '{runner_name}' ({runner_value:.4f})"
                )
    best_sentence += "."

    parts = [
        f"Compared {n_candidates} {noun}, ranked ascending by "
        f"{metric_desc}.",
        f"Shared cross-validation strategy: {cv_explanation}",
        best_sentence,
    ]
    if any_error:
        n_failed = n_candidates - len(ranked)
        if n_failed == 1:
            parts.append("1 configuration failed to run and is ranked last.")
        else:
            parts.append(
                f"{n_failed} configurations failed to run and are ranked "
                f"last."
            )
    return " ".join(parts)

_resolve_model

_resolve_model()

Resolve the LLM model from the provider string.

The model is created on the first call and cached for subsequent invocations.

Returns:

Name Type Description
model (str, OllamaModel)

Resolved Pydantic AI model instance.

Source code in skforecast_ai/assistant.py
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
def _resolve_model(self):
    """
    Resolve the LLM model from the provider string.

    The model is created on the first call and cached for
    subsequent invocations.

    Returns
    -------
    model : str, OllamaModel
        Resolved Pydantic AI model instance.
    """

    if self._model is None:
        self._model = create_model(
            llm=self.llm, base_url=self.base_url, api_key=self.api_key
        )

    return self._model

_resolve_agent

_resolve_agent()

Create and cache the pydantic-ai Agent instance.

The agent is created once per assistant and reused across calls. Dynamic behavior (skill selection, reference inclusion) is handled via AskDeps passed at run time.

Returns:

Name Type Description
agent Agent[AskDeps, str]

Cached agent instance.

Source code in skforecast_ai/assistant.py
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
def _resolve_agent(self):
    """
    Create and cache the pydantic-ai Agent instance.

    The agent is created once per assistant and reused across calls.
    Dynamic behavior (skill selection, reference inclusion) is
    handled via `AskDeps` passed at run time.

    Returns
    -------
    agent : Agent[AskDeps, str]
        Cached agent instance.
    """
    if self._agent is None:
        from .llm.agent import create_forecasting_agent

        model = self._resolve_model()
        self._agent = create_forecasting_agent(model)

    return self._agent

_resolve_cv_agent

_resolve_cv_agent()

Create and cache the CV configuration agent.

Returns:

Name Type Description
agent Agent[CVDeps, CVParams]

Cached CV configuration agent instance.

Source code in skforecast_ai/assistant.py
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
def _resolve_cv_agent(self):
    """
    Create and cache the CV configuration agent.

    Returns
    -------
    agent : Agent[CVDeps, CVParams]
        Cached CV configuration agent instance.
    """
    if self._cv_agent is None:
        from .llm.agent import create_cv_agent

        model = self._resolve_model()
        self._cv_agent = create_cv_agent(model)

    return self._cv_agent

_resolve_plan_refinement_agent

_resolve_plan_refinement_agent()

Create and cache the plan refinement agent.

Returns:

Name Type Description
agent Agent[PlanRefinementDeps, PlanOverrides]

Cached plan refinement agent instance.

Source code in skforecast_ai/assistant.py
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
def _resolve_plan_refinement_agent(self):
    """
    Create and cache the plan refinement agent.

    Returns
    -------
    agent : Agent[PlanRefinementDeps, PlanOverrides]
        Cached plan refinement agent instance.
    """
    if self._plan_refinement_agent is None:
        from .llm.agent import create_plan_refinement_agent

        model = self._resolve_model()
        self._plan_refinement_agent = create_plan_refinement_agent(model)

    return self._plan_refinement_agent

_refine_features_with_llm

_refine_features_with_llm(profile, plan, prompt)

Use the LLM to suggest lags and window features from domain knowledge.

Retries up to 2 times when the suggested lags/window_features exceed the data budget enforced by plan(), feeding the concrete violation back each time. On a transient/model failure, or after retries are exhausted, a UserWarning is emitted and (None, None, None) is returned so the caller falls back to the deterministic plan.

Parameters:

Name Type Description Default
profile ForecastingProfile

The profiled dataset and modeling decisions.

required
plan ForecastPlan

The current forecasting plan.

required
prompt str

The user's domain knowledge description.

required

Returns:

Name Type Description
lags int, list of int, None

The LLM-suggested lags, or None on failure.

window_features list of dict, None

The LLM-suggested window features as plain dicts, or None on failure. Each dict contains the keys 'stats' (a list of rolling statistics) and 'window_size' (a scalar int applied to every stat in that same dict), for example [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], 'window_size': 24}]. Allowed stats are 'mean', 'std', 'min', 'max', 'sum', 'median', 'ratio_min_max', 'coef_variation', and 'ewm'.

reasoning (str, None)

The LLM's explanation on success, or None on failure.

Source code in skforecast_ai/assistant.py
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
def _refine_features_with_llm(
    self,
    profile: ForecastingProfile,
    plan: ForecastPlan,
    prompt: str,
) -> tuple[int | list[int] | None, list[dict] | None, str | None]:
    """
    Use the LLM to suggest lags and window features from domain knowledge.

    Retries up to 2 times when the suggested lags/window_features exceed
    the data budget enforced by `plan()`, feeding the concrete violation
    back each time. On a transient/model failure, or after retries are
    exhausted, a `UserWarning` is emitted and `(None, None, None)` is
    returned so the caller falls back to the deterministic plan.

    Parameters
    ----------
    profile : ForecastingProfile
        The profiled dataset and modeling decisions.
    plan : ForecastPlan
        The current forecasting plan.
    prompt : str
        The user's domain knowledge description.

    Returns
    -------
    lags : int, list of int, None
        The LLM-suggested lags, or None on failure.
    window_features : list of dict, None
        The LLM-suggested window features as plain dicts, or None on
        failure. Each dict contains the keys `'stats'` (a list of
        rolling statistics) and `'window_size'` (a scalar int applied
        to every stat in that same dict), for example `[{'stats':
        ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'],
        'window_size': 24}]`. Allowed stats are `'mean'`, `'std'`,
        `'min'`, `'max'`, `'sum'`, `'median'`, `'ratio_min_max'`,
        `'coef_variation'`, and `'ewm'`.
    reasoning : str, None
        The LLM's explanation on success, or None on failure.
    """
    from .llm.agent import PlanRefinementDeps

    agent = self._resolve_plan_refinement_agent()
    deps = PlanRefinementDeps(
        profile=profile,
        plan=plan,
        prompt=prompt,
    )

    span_index_length = profile.data_profile.span_index_length
    max_allowed = int(span_index_length * MAX_FEATURE_FRACTION)

    max_retries = 2
    last_error = None

    for attempt in range(1 + max_retries):
        if attempt == 0:
            user_message = prompt
        else:
            user_message = (
                f"{prompt}\n\n"
                f"[RETRY {attempt}/{max_retries}] Your previous "
                f"lags/window_features were infeasible: {last_error} "
                f"The dataset has {span_index_length} observations, so "
                f"the largest lag or window size must not exceed "
                f"{max_allowed} ({int(MAX_FEATURE_FRACTION * 100)}%). "
                f"Shrink the largest value and try again."
            )

        # A transient/model failure (network, or malformed structured
        # output after pydantic-ai's own internal retries) is terminal
        # here — a budget hint would not fix it, so it is not retried.
        try:
            result = _run_agent_sync(agent, user_message, deps=deps)
        except Exception as exc:
            warnings.warn(
                f"LLM plan refinement failed ({exc}). Returning "
                f"deterministic plan.",
                UserWarning,
                stacklevel=3,
            )
            return None, None, None

        llm_overrides = result.output

        # Materialise the typed WindowFeature models back into plain dicts,
        # the format the deterministic plan pipeline stores and renders.
        window_features = (
            [wf.model_dump() for wf in llm_overrides.window_features]
            if llm_overrides.window_features is not None
            else None
        )

        # Pre-validate against the same data budget `plan()` enforces, so
        # an infeasible suggestion drives a retry with concrete feedback
        # instead of silently falling back to the deterministic plan.
        max_span = _max_window_size(llm_overrides.lags, window_features)
        if max_span > max_allowed:
            last_error = (
                f"lags/window_features span up to {max_span} "
                f"observations, exceeding the maximum of {max_allowed}."
            )
            if attempt < max_retries:
                continue
            warnings.warn(
                f"LLM plan refinement failed after {1 + max_retries} "
                f"attempts (last error: {last_error}). Returning "
                f"deterministic plan.",
                UserWarning,
                stacklevel=3,
            )
            return None, None, None

        return llm_overrides.lags, window_features, llm_overrides.reasoning

    # Unreachable: the loop above always returns on its last iteration.
    return None, None, None  # pragma: no cover

_configure_cv_with_llm

_configure_cv_with_llm(
    profile, plan, prompt, n_observations
)

Use the LLM to derive CV parameters from a natural-language prompt.

Retries up to 2 times on validation failure, then falls back to deterministic defaults with a warning.

Parameters:

Name Type Description Default
profile ForecastingProfile

Profiled dataset.

required
plan ForecastPlan

Forecast plan.

required
prompt str

User's deployment scenario description.

required
n_observations int

Total number of observations.

required

Returns:

Name Type Description
defaults dict

Resolved CV parameters dict (same format as derive_cv_defaults).

Source code in skforecast_ai/assistant.py
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
def _configure_cv_with_llm(
    self,
    profile: ForecastingProfile,
    plan: ForecastPlan,
    prompt: str,
    n_observations: int,
) -> dict:
    """
    Use the LLM to derive CV parameters from a natural-language prompt.

    Retries up to 2 times on validation failure, then falls back to
    deterministic defaults with a warning.

    Parameters
    ----------
    profile : ForecastingProfile
        Profiled dataset.
    plan : ForecastPlan
        Forecast plan.
    prompt : str
        User's deployment scenario description.
    n_observations : int
        Total number of observations.

    Returns
    -------
    defaults : dict
        Resolved CV parameters dict (same format as
        `derive_cv_defaults`).
    """
    from .llm.agent import CVDeps

    agent = self._resolve_cv_agent()
    lags = plan.forecaster_kwargs.get("lags")

    deps = CVDeps(
               n_observations = n_observations,
               frequency      = profile.data_profile.frequency,
               steps          = plan.steps,
               task_type      = plan.task_type,
               lags           = lags,
           )

    max_retries = 2
    last_error = None

    for attempt in range(1 + max_retries):
        try:
            if attempt == 0:
                user_message = prompt
            else:
                user_message = (
                    f"{prompt}\n\n"
                    f"[RETRY {attempt}/{max_retries}] Your previous "
                    f"configuration failed validation: {last_error}. "
                    f"The dataset has {n_observations} observations "
                    f"and steps={plan.steps}. Fix the parameters."
                )

            result = _run_agent_sync(agent, user_message, deps=deps)
            cv_params = result.output

            # Convert CVParams to defaults dict
            defaults = {
                "steps": plan.steps,
                "initial_train_size": cv_params.initial_train_size,
                "refit": cv_params.refit,
                "fixed_train_size": cv_params.fixed_train_size,
                "gap": cv_params.gap,
                "fold_stride": cv_params.fold_stride,
                "skip_folds": cv_params.skip_folds,
                "allow_incomplete_fold": cv_params.allow_incomplete_fold,
                "differentiation": plan.forecaster_kwargs.get(
                    "differentiation"
                ),
                "_reasoning": cv_params.reasoning,
            }

            # Validate: check that we can produce ≥2 folds
            self._validate_cv_defaults(defaults, n_observations)

            return defaults

        except Exception as exc:
            last_error = str(exc)
            if attempt < max_retries:
                continue
            # All retries exhausted — fall back to deterministic
            warnings.warn(
                f"LLM CV configuration failed after "
                f"{1 + max_retries} attempts "
                f"(last error: {last_error}). "
                f"Falling back to deterministic defaults.",
                UserWarning,
                stacklevel=3,
            )
            defaults = derive_cv_defaults(profile=profile, plan=plan)
            return defaults

    # Should never reach here, but satisfy type checker
    return derive_cv_defaults(profile=profile, plan=plan)  # pragma: no cover

_validate_cv_defaults staticmethod

_validate_cv_defaults(defaults, n_observations)

Validate that CV defaults can produce at least 2 folds.

A ValueError is raised when the configuration cannot produce at least 2 folds.

Parameters:

Name Type Description Default
defaults dict

Resolved CV parameters dict with keys 'steps', 'initial_train_size', 'refit', 'fixed_train_size', 'gap', 'fold_stride', 'skip_folds', 'allow_incomplete_fold', and 'differentiation'.

required
n_observations int

Total number of observations in the dataset.

required

Returns:

Type Description
None
Source code in skforecast_ai/assistant.py
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
@staticmethod
def _validate_cv_defaults(defaults: dict, n_observations: int) -> None:
    """
    Validate that CV defaults can produce at least 2 folds.

    A `ValueError` is raised when the configuration cannot produce
    at least 2 folds.

    Parameters
    ----------
    defaults : dict
        Resolved CV parameters dict with keys `'steps'`,
        `'initial_train_size'`, `'refit'`, `'fixed_train_size'`,
        `'gap'`, `'fold_stride'`, `'skip_folds'`,
        `'allow_incomplete_fold'`, and `'differentiation'`.
    n_observations : int
        Total number of observations in the dataset.

    Returns
    -------
    None
    """

    its = defaults["initial_train_size"]
    if isinstance(its, str):
        # Cannot validate date-based initial_train_size without data
        return

    if isinstance(its, float):
        if not (0 < its < 1):
            raise ValueError(
                f"initial_train_size as float must be in (0, 1), got {its}."
            )
        its = int(its * n_observations)
        defaults["initial_train_size"] = its

    cv = TimeSeriesFold(
        steps=defaults["steps"],
        initial_train_size=its,
        refit=defaults["refit"],
        fixed_train_size=defaults["fixed_train_size"],
        gap=defaults["gap"],
        fold_stride=defaults.get("fold_stride"),
        skip_folds=defaults.get("skip_folds"),
        allow_incomplete_fold=defaults.get("allow_incomplete_fold", True),
        differentiation=defaults.get("differentiation"),
        verbose=False,
    )

    n_folds = _count_cv_folds(cv=cv, n_observations=n_observations)
    if n_folds < 2:
        raise ValueError(
            f"Configuration produces only {n_folds} fold(s). "
            f"At least 2 required. Parameters: {defaults}."
        )

_build_ollama_settings

_build_ollama_settings(
    estimated_prompt_tokens, user_message
)

Build Ollama-specific model settings with dynamic context sizing.

Uses the pre-computed token estimate for system prompt content plus the user message length to determine the appropriate num_ctx. Clamps between 4096 and OLLAMA_MAX_CONTEXT_TOKENS. Warns when the prompt approaches the hard maximum. Returns None for non-Ollama providers.

Parameters:

Name Type Description Default
estimated_prompt_tokens int

Estimated tokens for the system prompt (skills + reference).

required
user_message str

The user message to send.

required

Returns:

Name Type Description
settings (dict, None)

Model settings dict or None for cloud providers.

Source code in skforecast_ai/assistant.py
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
def _build_ollama_settings(
    self, estimated_prompt_tokens: int, user_message: str
) -> dict | None:
    """
    Build Ollama-specific model settings with dynamic context sizing.

    Uses the pre-computed token estimate for system prompt content
    plus the user message length to determine the appropriate
    `num_ctx`. Clamps between 4096 and `OLLAMA_MAX_CONTEXT_TOKENS`.
    Warns when the prompt approaches the hard maximum. Returns None
    for non-Ollama providers.

    Parameters
    ----------
    estimated_prompt_tokens : int
        Estimated tokens for the system prompt (skills + reference).
    user_message : str
        The user message to send.

    Returns
    -------
    settings : dict, None
        Model settings dict or None for cloud providers.
    """
    if self.llm is None or not self.llm.startswith("ollama:"):
        return None

    user_tokens = len(user_message) // 4
    estimated_tokens = estimated_prompt_tokens + user_tokens
    requested_ctx = estimated_tokens + RESERVED_RESPONSE_TOKENS
    num_ctx = max(4096, min(requested_ctx, OLLAMA_MAX_CONTEXT_TOKENS))

    # The clamp is what causes truncation: the prompt plus the space
    # reserved for the answer no longer fits the window.
    if requested_ctx > OLLAMA_MAX_CONTEXT_TOKENS:
        warnings.warn(
            f"Estimated prompt size (~{estimated_tokens} tokens) exceeds "
            f"the Ollama context limit ({OLLAMA_MAX_CONTEXT_TOKENS}) once "
            f"room for the answer is reserved. Output may be truncated. "
            f"Consider using `skills=[]` or `include_reference=False`.",
            UserWarning,
            stacklevel=3,
        )

    return {
        "extra_body": {
            "keep_alive": "10m",
            "options": {"num_ctx": num_ctx},
        }
    }