docstr: Build a Time Series Model using SARIMAX from statsmodels.
docstr_tok: ["Build","a","Time","Series","Model","using","SARIMAX","from","statsmodels","."]
code: def fit(
      self,
      ts_df: pd.DataFrame,
      target_col: str,
      cv: Optional[int] = None,
  ):
      """
      Build a Time Series Model using SARIMAX from statsmodels.
      """
  
      self.original_target_col = target_col
      self.original_preds = [
          x for x in list(ts_df) if x not in [self.original_target_col]
      ]
  
      if len(self.original_preds) == 0:
          self.univariate = True
      else:
          self.univariate = False
  
      ##########################################
      #### Find best pdq and PDQ parameters ####
      ##########################################
  
      # NOTE: We use the entire dataset to compute the pdq and PDQ parameters.
      # Then we use the selected "best" parameters to check how well it
      # generalizes across the various folds (which may even be 1)
  
      # ## Added temporarily
      # ts_train = ts_df.iloc[:-self.forecast_period]
      # self.find_best_parameters(data = ts_train)
  
      if self.seasonal_period <= 1:
          self.seasonal_period = (
              2  ### Sarimax cannot have seasonal period 1 or below.
          )
  
      if self.verbose >= 1:
          print(f"\n\nBest Parameters:")
          print(f"p: {self.best_p}, d: {self.best_d}, q: {self.best_q}")
          print(f"P: {self.best_P}, D: {self.best_D}, Q: {self.best_Q}")
          print(
              f"Seasonality: {self.seasonality}\nSeasonal Period: {self.seasonal_period}"
          )
  
      #######################################
      #### Cross Validation across Folds ####
      #######################################
  
      rmse_folds = []
      norm_rmse_folds = []
      forecast_df_folds = []
  
      ### Creating a new way to skip cross validation when trying to run auto-ts multiple times. ###
      if cv == 0:
          cv_in = 0
      else:
          cv_in = copy.deepcopy(cv)
      NFOLDS = self.get_num_folds_from_cv(cv)
  
      #########################################################################
      if type(ts_df) == dask.dataframe.core.DataFrame:
          num_obs = ts_df.shape[0].compute()
      else:
          num_obs = ts_df.shape[0]
  
      if self.forecast_period <= 5:
          #### Set a minimum of 5 for the number of rows in test!
          self.forecast_period = 5
      ### In case the number of forecast_period is too high, just reduce it so it can fit into num_obs
      if NFOLDS * self.forecast_period > num_obs:
          self.forecast_period = int(num_obs / (NFOLDS + 1))
          print(
              "Lowering forecast period to %d to enable cross_validation"
              % self.forecast_period
          )
      #########################################################################
      extra_concatenated = pd.DataFrame()
      concatenated = pd.DataFrame()
      norm_rmse_folds2 = []
  
      max_trainsize = len(ts_df) - self.forecast_period
      try:
          cv = TimeSeriesSplit(
              n_splits=NFOLDS, test_size=self.forecast_period
          )  ### this works only sklearn v 0.0.24]
      except:
          cv = TimeSeriesSplit(
              n_splits=NFOLDS, max_train_size=max_trainsize
          )
  
      if type(ts_df) == dask.dataframe.core.DataFrame:
          ts_df = dft.head(
              len(ts_df)
          )  ### this converts dask into a pandas dataframe
  
      if cv_in == 0:
          print(
              "Skipping cross validation steps since cross_validation = %s"
              % cv_in
          )
      else:
          for fold_number, (train_index, test_index) in enumerate(
              cv.split(ts_df)
          ):
              dftx = ts_df.head(len(train_index) + len(test_index))
              ts_train = dftx.head(
                  len(train_index)
              )  ## now train will be the first segment of dftx
              ts_test = dftx.tail(
                  len(test_index)
              )  ### now test will be right after train in dftx
  
              if self.verbose >= 1:
                  print(
                      f"\nFold Number: {fold_number+1} --> Train Shape: {ts_train.shape[0]} Test Shape: {ts_test.shape[0]}"
                  )
  
              ### this is needed for static forecasts ####################
              # TODO: Check if this needs to be fixed to pick usimg self.original_target_col
              y_truth = ts_train[
                  :
              ]  #  TODO: Note that this is only univariate analysis
  
              if len(self.original_preds) == 0:
                  exog = None
              elif len(self.original_preds) == 1:
                  exog = ts_test[self.original_preds[0]].values.reshape(
                      -1, 1
                  )
              else:
                  exog = ts_test[self.original_preds].values
  
              auto_arima_model = self.find_best_parameters(
                  data=ts_train
              )
              self.model = auto_arima_model
              y_forecasted = self.model.predict(ts_test.shape[0], exog)
  
              if fold_number == 0:
                  concatenated = pd.DataFrame(
                      np.c_[
                          ts_test[self.original_target_col].values,
                          y_forecasted,
                      ],
                      columns=["original", "predicted"],
                      index=ts_test.index,
                  )
                  extra_concatenated = copy.deepcopy(concatenated)
              else:
                  concatenated = pd.DataFrame(
                      np.c_[
                          ts_test[self.original_target_col].values,
                          y_forecasted,
                      ],
                      columns=["original", "predicted"],
                      index=ts_test.index,
                  )
                  extra_concatenated = extra_concatenated.append(
                      concatenated
                  )
  
              ### for SARIMAX and Auto_ARIMA, you don't have to restore differences since it predicts like actuals.###
              y_true = concatenated["original"]
              y_pred = concatenated["predicted"]
  
              if self.verbose >= 1:
                  print("Static Forecasts:")
                  # Since you are differencing the data, some original data points will not be available
                  # Hence taking from first available value.
                  print_static_rmse(
                      y_true.values, y_pred.values, verbose=self.verbose
                  )
                  # quick_ts_plot(y_true, y_pred)
  
              # Extract the dynamic predicted and true values of our time series
              forecast_df = copy.deepcopy(y_forecasted)
              forecast_df_folds.append(forecast_df)
  
              rmse, norm_rmse = print_static_rmse(
                  y_true.values, y_pred.values, verbose=0
              )  ## don't print this time
              rmse_folds.append(rmse)
              norm_rmse_folds.append(norm_rmse)
  
              # TODO: Convert rmse_folds, rmse_norm_folds, forecasts_folds into base class attributes
              # TODO: Add gettes and seters for these class attributes.
              # This will ensure consistency across various model build types.
  
          # This is taking the std of entire dataset and using that to normalize
          # vs. other approach that was using std of individual folds to standardize.
          # Technically this is not correct, but in order to do Apples:Aples compatison with ML
          # (sklearn) based cross_val_score, we need to do this since we dont get individual folds
          # back for cross_val_score. If at a later point in time, we can get this, then,
          # we can revert back to dividing by individual fold std values.
          norm_rmse_folds2 = (
              rmse_folds / ts_df[self.original_target_col].values.std()
          )  # Same as what was there in print_dynamic_rmse()
  
          print(
              f"\nSARIMAX RMSE (all folds): {np.mean(rmse_folds):.4f}"
          )
          print(
              f"SARIMAX Norm RMSE (all folds): {(np.mean(norm_rmse_folds2)*100):.0f}%\n"
          )
          try:
              print_ts_model_stats(
                  extra_concatenated["original"],
                  extra_concatenated["predicted"],
                  "auto_SARIMAX",
              )
          except:
              print("Unable to print model stats. Continuing...")
  
      ###############################################
      #### Refit the model on the entire dataset ####
      ###############################################
      auto_arima_model = self.find_best_parameters(data=ts_df)
      self.model = auto_arima_model
      self.refit(ts_df=ts_df)
  
      print(self.model.summary())
  
      # return self.model, forecast_df_folds, rmse_folds, norm_rmse_folds
      return self.model, forecast_df_folds, rmse_folds, norm_rmse_folds2
  
imports: from typing import Optional
  import warnings
  from abc import abstractmethod
  import copy
  import pdb
  import numpy
  import pandas
  from pandas.core.generic import NDFrame
  import dask
  import dask.dataframe
  import matplotlib.pyplot
  from sklearn.model_selection import TimeSeriesSplit
  from statsmodels.tsa.statespace.sarimax import SARIMAX
  from ..build_base import BuildBase
  from ...utils import colorful, print_static_rmse, print_dynamic_rmse, print_ts_model_stats
  from ...models.ar_based.param_finder import find_best_pdq_or_PDQ
label_desc: Use image node_id 2 for calling the BuildArimaBase obj's underlying member method code with example usage: obj.fit(ts_df, target_col, cv) and returns: self,  forecast_df_folds,  rmse_folds,  norm_rmse_folds2