code: def _backtesting_sarimax(
      forecaster,
      y: pd.Series,
      steps: int,
      metric: Union[str, Callable, list],
      initial_train_size: int,
      fixed_train_size: bool = True,
      gap: int = 0,
      allow_incomplete_fold: bool = True,
      exog: Optional[Union[pd.Series, pd.DataFrame]] = None,
      refit: Optional[Union[bool, int]] = False,
      alpha: Optional[float] = None,
      interval: Optional[list] = None,
      n_jobs: Optional[Union[int, str]] = "auto",
      suppress_warnings_fit: bool = False,
      verbose: bool = False,
      show_progress: bool = True,
  ) -> Tuple[Union[float, list], pd.DataFrame]:
      """
      Backtesting of ForecasterSarimax.
  
      - If `refit` is `False`, the model will be trained only once using the
      `initial_train_size` first observations.
      - If `refit` is `True`, the model is trained on each iteration, increasing
      the training set.
      - If `refit` is an `integer`, the model will be trained every that number
      of iterations.
  
      A copy of the original forecaster is created so that it is not modified during
      the process.
  
      Parameters
      ----------
      forecaster : ForecasterSarimax
          Forecaster model.
      y : pandas Series
          Training time series.
      steps : int
          Number of steps to predict.
      metric : str, Callable, list
          Metric used to quantify the goodness of fit of the model.
  
          - If `string`: {'mean_squared_error', 'mean_absolute_error',
          'mean_absolute_percentage_error', 'mean_squared_log_error'}
          - If `Callable`: Function with arguments y_true, y_pred that returns
          a float.
          - If `list`: List containing multiple strings and/or Callables.
      initial_train_size : int
          Number of samples in the initial train split. The backtest forecaster is
          trained using the first `initial_train_size` observations.
      fixed_train_size : bool, default `True`
          If True, train size doesn't increase but moves by `steps` in each iteration.
      gap : int, default `0`
          Number of samples to be excluded after the end of each training set and
          before the test set.
      allow_incomplete_fold : bool, default `True`
          Last fold is allowed to have a smaller number of samples than the
          `test_size`. If `False`, the last fold is excluded.
      exog : pandas Series, pandas DataFrame, default `None`
          Exogenous variable/s included as predictor/s. Must have the same
          number of observations as `y` and should be aligned so that y[i] is
          regressed on exog[i].
      refit : bool, int, default `False`
          Whether to re-fit the forecaster in each iteration. If `refit` is an integer,
          the Forecaster will be trained every that number of iterations.
      alpha : float, default `0.05`
          The confidence intervals for the forecasts are (1 - alpha) %.
          If both, `alpha` and `interval` are provided, `alpha` will be used.
      interval : list, default `None`
          Confidence of the prediction interval estimated. The values must be
          symmetric. Sequence of percentiles to compute, which must be between
          0 and 100 inclusive. For example, interval of 95% should be as
          `interval = [2.5, 97.5]`. If both, `alpha` and `interval` are
          provided, `alpha` will be used.
      n_jobs : int, 'auto', default `'auto'`
          The number of jobs to run in parallel. If `-1`, then the number of jobs is
          set to the number of cores. If 'auto', `n_jobs` is set using the function
          skforecast.utils.select_n_jobs_backtesting.
          **New in version 0.9.0**
      verbose : bool, default `False`
          Print number of folds and index of training and validation sets used
          for backtesting.
      suppress_warnings_fit : bool, default `False`
          If `True`, warnings generated during fitting will be ignored.
          **New in version 0.10.0**
      show_progress: bool, default `True`
          Whether to show a progress bar.
  
      Returns
      -------
      metrics_value : float, list
          Value(s) of the metric(s).
      backtest_predictions : pandas DataFrame
          Value of predictions and their estimated interval if `interval` is not `None`.
  
          - column pred: predictions.
          - column lower_bound: lower bound of the interval.
          - column upper_bound: upper bound of the interval.
  
      """
  
      forecaster = deepcopy(forecaster)
  
      if refit == False:
          n_jobs = 1
      else:
          if n_jobs == "auto":
              n_jobs = select_n_jobs_backtesting(
                  forecaster=forecaster, refit=refit
              )
          else:
              n_jobs = n_jobs if n_jobs > 0 else cpu_count()
  
      if not isinstance(metric, list):
          metrics = [
              _get_metric(metric=metric)
              if isinstance(metric, str)
              else metric
          ]
      else:
          metrics = [
              _get_metric(metric=m) if isinstance(m, str) else m
              for m in metric
          ]
  
      # initial_train_size cannot be None because of append method in Sarimax
      # First model training, this is done to allow parallelization when `refit`
      # is `False`. The initial Forecaster fit is outside the auxiliary function.
      exog_train = (
          exog.iloc[:initial_train_size,] if exog is not None else None
      )
      forecaster.fit(
          y=y.iloc[:initial_train_size,],
          exog=exog_train,
          suppress_warnings=suppress_warnings_fit,
      )
      window_size = forecaster.window_size
      externally_fitted = False
  
      folds = _create_backtesting_folds(
          data=y,
          window_size=window_size,
          initial_train_size=initial_train_size,
          test_size=steps,
          externally_fitted=externally_fitted,
          refit=refit,
          fixed_train_size=fixed_train_size,
          gap=gap,
          allow_incomplete_fold=allow_incomplete_fold,
          return_all_indexes=False,
          verbose=verbose,
      )
  
      if refit:
          n_of_fits = int(len(folds) / refit)
          if n_of_fits > 50:
              warnings.warn(
                  (
                      f"The forecaster will be fit {n_of_fits} times. This can take substantial "
                      f"amounts of time. If not feasible, try with `refit = False`.\n"
                  ),
                  LongTrainingWarning,
              )
  
      folds_tqdm = tqdm(folds) if show_progress else folds
  
      def _fit_predict_forecaster(
          y, exog, forecaster, alpha, interval, fold, steps
      ):
          """
          Fit the forecaster and predict `steps` ahead. This is an auxiliary
          function used to parallelize the backtesting_forecaster function.
          """
  
          # In each iteration the model is fitted before making predictions.
          # if fixed_train_size the train size doesn't increase but moves by `steps`
          # in each iteration. if False the train size increases by `steps` in each
          # iteration.
          train_idx_start = fold[0][0]
          train_idx_end = fold[0][1]
          test_idx_start = fold[2][0]
          test_idx_end = fold[2][1]
  
          if refit:
              last_window_start = fold[0][1]  # Same as train_idx_end
              last_window_end = fold[1][1]
          else:
              last_window_end = fold[2][0]  # test_idx_start
              last_window_start = last_window_end - steps
  
          if fold[4] is False:
              # When the model is not fitted, last_window and last_window_exog must
              # be updated to include the data needed to make predictions.
              last_window_y = y.iloc[last_window_start:last_window_end]
              last_window_exog = (
                  exog.iloc[last_window_start:last_window_end]
                  if exog is not None
                  else None
              )
          else:
              # The model is fitted before making predictions. If `fixed_train_size`
              # the train size doesn't increase but moves by `steps` in each iteration.
              # If `False` the train size increases by `steps` in each  iteration.
              y_train = y.iloc[train_idx_start:train_idx_end,]
              exog_train = (
                  exog.iloc[train_idx_start:train_idx_end,]
                  if exog is not None
                  else None
              )
  
              last_window_y = None
              last_window_exog = None
  
              forecaster.fit(
                  y=y_train,
                  exog=exog_train,
                  suppress_warnings=suppress_warnings_fit,
              )
  
          next_window_exog = (
              exog.iloc[test_idx_start:test_idx_end,]
              if exog is not None
              else None
          )
  
          # After the first fit, ARIMA must use the last windows stored in the model
          if fold == folds[0]:
              last_window_y = None
              last_window_exog = None
  
          steps = len(range(test_idx_start, test_idx_end))
          if alpha is None and interval is None:
              pred = forecaster.predict(
                  steps=steps,
                  last_window=last_window_y,
                  last_window_exog=last_window_exog,
                  exog=next_window_exog,
              )
          else:
              pred = forecaster.predict_interval(
                  steps=steps,
                  exog=next_window_exog,
                  alpha=alpha,
                  interval=interval,
                  last_window=last_window_y,
                  last_window_exog=last_window_exog,
              )
  
          pred = pred.iloc[gap:,]
  
          return pred
  
      backtest_predictions = Parallel(n_jobs=n_jobs)(
          delayed(_fit_predict_forecaster)(
              y=y,
              exog=exog,
              forecaster=forecaster,
              alpha=alpha,
              interval=interval,
              fold=fold,
              steps=steps,
          )
          for fold in folds_tqdm
      )
  
      backtest_predictions = pd.concat(backtest_predictions)
      if isinstance(backtest_predictions, pd.Series):
          backtest_predictions = pd.DataFrame(backtest_predictions)
  
      metrics_values = [
          m(
              y_true=y.loc[backtest_predictions.index],
              y_pred=backtest_predictions["pred"],
          )
          for m in metrics
      ]
  
      if not isinstance(metric, list):
          metrics_values = metrics_values[0]
  
      return metrics_values, backtest_predictions
  
file_path: skforecast/skforecast/model_selection_sarimax/model_selection_sarimax.py
imports: from typing import Union, Tuple, Optional, Callable
  import pandas
  import warnings
  import logging
  from copy import deepcopy
  from joblib import Parallel, delayed, cpu_count
  from tqdm.auto import tqdm
  from sklearn.model_selection import ParameterGrid
  from sklearn.model_selection import ParameterSampler
  from ..exceptions import LongTrainingWarning
  from ..model_selection.model_selection import _get_metric
  from ..model_selection.model_selection import _create_backtesting_folds
  from ..utils import check_backtesting_input
  from ..utils import select_n_jobs_backtesting
label_desc: Use image node_id 1 for calling a global function with example usage: _backtesting_sarimax(forecaster, y, steps, metric, initial_train_size, fixed_train_size, gap, allow_incomplete_fold, exog, refit, alpha, interval, n_jobs, suppress_warnings_fit, verbose, show_progress) and returns: metrics_values,  backtest_predictions, pred