code: def _bayesian_search_optuna(
      forecaster,
      y: pd.Series,
      search_space: Callable,
      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,
      lags_grid: Optional[Union[list, dict]] = None,
      refit: Optional[Union[bool, int]] = False,
      n_trials: int = 10,
      random_state: int = 123,
      return_best: bool = True,
      n_jobs: Optional[Union[int, str]] = "auto",
      verbose: bool = True,
      show_progress: bool = True,
      kwargs_create_study: dict = {},
      kwargs_study_optimize: dict = {},
  ) -> Tuple[pd.DataFrame, object]:
      """
      Bayesian optimization for a Forecaster object using time series backtesting
      and optuna library.
  
      Parameters
      ----------
      forecaster : ForecasterAutoreg, ForecasterAutoregCustom, ForecasterAutoregDirect
          Forecaster model.
      y : pandas Series
          Training time series.
      search_space : Callable
          Function with argument `trial` which returns a dictionary with parameters names
          (`str`) as keys and Trial object from optuna (trial.suggest_float,
          trial.suggest_int, trial.suggest_categorical) as values.
      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.
      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].
      lags_grid : list, dict, default `None`
          Lists of lags to try, containing int, lists, numpy ndarray, or range
          objects. If `dict`, the keys are used as labels in the `results`
          DataFrame, and the values are used as the lists of lags to try. Ignored
          if the forecaster is an instance of `ForecasterAutoregCustom` or
          `ForecasterAutoregMultiSeriesCustom`.
      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.
      n_trials : int, default `10`
          Number of parameter settings that are sampled in each lag configuration.
      random_state : int, default `123`
          Sets a seed to the sampling for reproducible output.
      return_best : bool, default `True`
          Refit the `forecaster` using the best found parameters on the whole data.
      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 `True`
          Print number of folds used for cv or backtesting.
      show_progress: bool, default `True`
          Whether to show a progress bar.
      kwargs_create_study : dict, default `{'direction': 'minimize', 'sampler': TPESampler(seed=123)}`
          Keyword arguments (key, value mappings) to pass to optuna.create_study.
      kwargs_study_optimize : dict, default `{}`
          Other keyword arguments (key, value mappings) to pass to study.optimize().
  
      Returns
      -------
      results : pandas DataFrame
          Results for each combination of parameters.
  
          - column lags: lags configuration for each iteration.
          - column params: parameters configuration for each iteration.
          - column metric: metric value estimated for each iteration.
          - additional n columns with param = value.
      results_opt_best : optuna object
          The best optimization result returned as a FrozenTrial optuna object.
  
      """
  
      lags_grid, lags_label = initialize_lags_grid(
          forecaster, lags_grid
      )
  
      lags_list = []
      params_list = []
      results_opt_best = None
      if not isinstance(metric, list):
          metric = [metric]
      metric_dict = {
          (m if isinstance(m, str) else m.__name__): [] for m in metric
      }
  
      if len(metric_dict) != len(metric):
          raise ValueError(
              "When `metric` is a `list`, each metric name must be unique."
          )
  
      # Objective function using backtesting_forecaster
      def _objective(
          trial,
          search_space=search_space,
          forecaster=forecaster,
          y=y,
          exog=exog,
          steps=steps,
          metric=metric,
          initial_train_size=initial_train_size,
          fixed_train_size=fixed_train_size,
          gap=gap,
          allow_incomplete_fold=allow_incomplete_fold,
          refit=refit,
          n_jobs=n_jobs,
          verbose=verbose,
      ) -> float:
          forecaster.set_params(search_space(trial))
  
          metrics, _ = backtesting_forecaster(
              forecaster=forecaster,
              y=y,
              exog=exog,
              steps=steps,
              metric=metric,
              initial_train_size=initial_train_size,
              fixed_train_size=fixed_train_size,
              gap=gap,
              allow_incomplete_fold=allow_incomplete_fold,
              refit=refit,
              n_jobs=n_jobs,
              verbose=verbose,
              show_progress=False,
          )
  
          # Store metrics in the variable `metric_values` defined outside _objective.
          nonlocal metric_values
          metric_values.append(metrics)
  
          return metrics[0]
  
      print(
          f"""Number of models compared: {n_trials*len(lags_grid)},
           {n_trials} bayesian search in each lag configuration."""
      )
  
      if show_progress:
          lags_grid_tqdm = tqdm(
              lags_grid.items(), desc="lags grid", position=0
          )
      else:
          lags_grid_tqdm = lags_grid.items()
  
      for lags_k, lags_v in lags_grid_tqdm:
          # `metric_values` will be modified inside _objective function.
          # It is a trick to extract multiple values from _objective since
          # only the optimized value can be returned.
          metric_values = []
  
          if type(forecaster).__name__ != "ForecasterAutoregCustom":
              forecaster.set_lags(lags_v)
              lags_v = (
                  lags_k
                  if lags_label == "keys"
                  else forecaster.lags.copy()
              )
  
          if "sampler" in kwargs_create_study.keys():
              kwargs_create_study[
                  "sampler"
              ]._rng = np.random.RandomState(random_state)
              kwargs_create_study[
                  "sampler"
              ]._random_sampler = RandomSampler(seed=random_state)
  
          study = optuna.create_study(**kwargs_create_study)
  
          if "sampler" not in kwargs_create_study.keys():
              study.sampler = TPESampler(seed=random_state)
  
          study.optimize(
              _objective, n_trials=n_trials, **kwargs_study_optimize
          )
  
          best_trial = study.best_trial
  
          if (
              search_space(best_trial).keys()
              != best_trial.params.keys()
          ):
              raise ValueError(
                  f"""Some of the key values do not match the search_space key names.
                  Dict keys     : {list(search_space(best_trial).keys())}
                  Trial objects : {list(best_trial.params.keys())}."""
              )
  
          for i, trial in enumerate(study.get_trials()):
              params_list.append(trial.params)
              lags_list.append(lags_v)
              for m, m_values in zip(metric, metric_values[i]):
                  m_name = m if isinstance(m, str) else m.__name__
                  metric_dict[m_name].append(m_values)
  
          if results_opt_best is None:
              results_opt_best = best_trial
          else:
              if best_trial.value < results_opt_best.value:
                  results_opt_best = best_trial
  
      results = pd.DataFrame(
          {"lags": lags_list, "params": params_list, **metric_dict}
      )
  
      results = results.sort_values(
          by=list(metric_dict.keys())[0], ascending=True
      )
      results = pd.concat(
          [results, results["params"].apply(pd.Series)], axis=1
      )
  
      if return_best:
          best_lags = results["lags"].iloc[0]
          best_params = results["params"].iloc[0]
          best_metric = results[list(metric_dict.keys())[0]].iloc[0]
  
          if lags_label == "keys":
              best_lags = lags_grid[best_lags]
  
          if type(forecaster).__name__ != "ForecasterAutoregCustom":
              forecaster.set_lags(best_lags)
              best_lags = forecaster.lags
          else:
              best_lags = "custom_predictors"
          forecaster.set_params(best_params)
  
          forecaster.fit(y=y, exog=exog, store_in_sample_residuals=True)
  
          print(
              f"`Forecaster` refitted using the best-found lags and parameters, "
              f"and the whole data set: \n"
              f"  Lags: {best_lags} \n"
              f"  Parameters: {best_params}\n"
              f"  Backtesting metric: {best_metric}\n"
          )
  
      return results, results_opt_best
  
imports: from typing import Union, Tuple, Optional, Callable
  import numpy
  import pandas
  import warnings
  import logging
  from copy import deepcopy
  from joblib import Parallel, delayed, cpu_count
  from tqdm.auto import tqdm
  from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error, mean_squared_log_error
  from sklearn.model_selection import ParameterGrid
  from sklearn.model_selection import ParameterSampler
  import optuna
  from optuna.samplers import TPESampler, RandomSampler
  from ..exceptions import LongTrainingWarning
  from ..utils import check_backtesting_input
  from ..utils import initialize_lags_grid
  from ..utils import select_n_jobs_backtesting
label_desc: Use image node_id 9 for calling a global function with example usage: _bayesian_search_optuna(forecaster, y, search_space, steps, metric, initial_train_size, fixed_train_size, gap, allow_incomplete_fold, exog, lags_grid, refit, n_trials, random_state, return_best, n_jobs, verbose, show_progress, kwargs_create_study, kwargs_study_optimize) and returns: results,  results_opt_best, metrics