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 data may be sent to the LLM. When False, only metadata (schema, summary stats) is shared with the LLM.

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

Three 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.

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

ask

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

Source code in skforecast_ai/assistant.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
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. Must be in profile.forecaster_candidates.

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
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
284
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
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. Must be in `profile.forecaster_candidates`.
    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:
            raise ValueError(
                f"Forecaster '{forecaster}' is not compatible with this "
                f"profile. Available candidates: "
                f"{profile.forecaster_candidates}."
            )
        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,
    )

    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
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
513
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
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

Forecasting profile, plan, and generated code.

Source code in skforecast_ai/assistant.py
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
703
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
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
        Forecasting profile, plan, and generated code.
    """

    _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

Forecasting profile, plan, generated code, predictions, and evaluation metrics. Metrics are only computed in evaluation mode (test_size is set); in prediction mode result.metrics is None. When prediction intervals are requested, the interval columns are included in result.predictions.

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
 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
 905
 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
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
        Forecasting profile, plan, generated code, predictions, and
        evaluation metrics. Metrics are only computed in evaluation
        mode (`test_size` is set); in prediction mode
        `result.metrics` is None. When prediction intervals are
        requested, the interval columns are included in
        `result.predictions`.

    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
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
1119
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
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

Forecasting profile, plan, and generated backtesting code.

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
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
1316
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
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
        Forecasting profile, plan, and generated backtesting code.

    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 profile, plan, metrics, predictions, code, and explanation.

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
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
1432
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
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 profile, plan, metrics, predictions, code, and
        explanation.

    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
    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,
    }
    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"],
    )

ask

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

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

Operates in four modes:

  • Q&A mode (no data, no profile, no forecast_result, no backtest_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 (forecast_result provided): the LLM explains forecast predictions (including any interval columns) and metrics from a completed forecast() run.
  • Backtest mode (backtest_result provided): the LLM explains backtesting metrics, predictions, and CV configuration from a completed backtest() run.

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
forecast_result ForecastResult

Result from a previous forecast() call. When provided, the LLM receives predictions (including any interval columns) and metrics in context so it can explain the forecast results. Extracts profile and plan from the result unless explicitly provided.

None
backtest_result BacktestResult

Result from a previous backtest() call. When provided, the LLM receives backtesting metrics, predictions, and CV configuration in context. Mutually exclusive with forecast_result.

None
steps int

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

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

Response with optional forecaster profile, plan, generated code, and LLM-generated explanation.

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
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
1602
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
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,
    forecast_result: ForecastResult | None = None,
    backtest_result: BacktestResult | 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 four modes:

    - Q&A mode (no data, no profile, no forecast_result, no
      backtest_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 (forecast_result provided): the LLM explains
      forecast predictions (including any interval columns) and
      metrics from a completed `forecast()` run.
    - Backtest mode (backtest_result provided): the LLM explains
      backtesting metrics, predictions, and CV configuration from a
      completed `backtest()` run.

    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.
    forecast_result : ForecastResult, default None
        Result from a previous `forecast()` call. When provided,
        the LLM receives predictions (including any interval
        columns) and metrics in context so it can explain the
        forecast results. Extracts `profile` and `plan` from the
        result unless explicitly provided.
    backtest_result : BacktestResult, default None
        Result from a previous `backtest()` call. When provided,
        the LLM receives backtesting metrics, predictions, and
        CV configuration in context. Mutually exclusive with
        `forecast_result`.
    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`.
    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
        Response with optional forecaster profile, plan, generated code,
        and LLM-generated explanation.

    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 forecast_result is not None and backtest_result is not None:
        raise ValueError(
            "`forecast_result` and `backtest_result` are mutually "
            "exclusive — provide one or the other, not both."
        )

    if forecast_result is not None and not isinstance(
        forecast_result, ForecastResult
    ):
        raise TypeError(
            f"`forecast_result` must be a `ForecastResult` object, got "
            f"{type(forecast_result).__name__}."
        )

    if backtest_result is not None and not isinstance(
        backtest_result, BacktestResult
    ):
        raise TypeError(
            f"`backtest_result` must be a `BacktestResult` object, got "
            f"{type(backtest_result).__name__}."
        )

    # --- Extract from forecast_result if provided ---
    predictions = None
    metrics = None
    cv_config = None
    if forecast_result is not None:
        profile = profile or forecast_result.profile
        plan = plan or forecast_result.plan
        predictions = forecast_result.predictions
        metrics = forecast_result.metrics
    elif backtest_result is not None:
        profile = profile or backtest_result.profile
        plan = plan or backtest_result.plan
        predictions = backtest_result.predictions
        metrics = backtest_result.metrics
        cv_config = backtest_result.cv_config

    # --- Deterministic stage: compute profile/plan if needed ---
    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)

    # --- Generate deterministic code from plan ---
    if forecast_result is not None:
        generated_code = forecast_result.code
    elif backtest_result is not None:
        generated_code = backtest_result.code
    elif 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

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

    # --- Build user message with context ---
    # In results mode, always send prediction data so the LLM can
    # discuss specific values. Otherwise respect the user setting.
    send_data = (
        True
        if forecast_result is not None or backtest_result is not None
        else self.send_data_to_llm
    )
    context = build_context_message(
        profile, plan,
        predictions=predictions,
        metrics=metrics,
        cv_config=cv_config,
        send_data=send_data,
    )
    user_message = (
        f"{context}\n\n## Question\n\n{prompt}" 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
        )
        resolved_skills = select_skills(
            task_type=task_type,
            question=prompt,
        )

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

    agent = self._resolve_agent()
    deps = AskDeps(
        profile=profile,
        plan=plan,
        question=prompt,
        include_reference=include_reference,
        skills_override=resolved_skills,
    )

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

    try:
        result = _run_agent_sync(
            agent,
            user_message,
            deps=deps,
            model_settings=model_settings,
        )
        explanation = result.output

        # Strip code blocks in Explain/Results mode (validated code exists)
        if generated_code is not None:
            explanation = _strip_code_blocks(explanation)
    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}"

    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
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
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_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
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
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
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
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
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
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
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
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
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
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
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
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
2164
2165
2166
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
@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 32768. 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
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
2261
2262
2263
2264
2265
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 32768. 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
    num_ctx = max(4096, min(estimated_tokens + 2048, 32768))

    if estimated_tokens > 30000:
        warnings.warn(
            f"Estimated prompt size (~{estimated_tokens} tokens) approaches "
            f"the Ollama context limit (32768). 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},
        }
    }