class_docstr: Class to build a Prophet Model
      
class_docstr_tok: ["Class","to","build","a","Prophet","Model"]
docstr: Fits the model to the data
  
  :param ts_df The time series data to be used for fitting the model
  :type ts_df pd.DataFrame
  
  :param target_col The column name of the target time series that needs to be modeled.
  All other columns will be considered as exogenous variables (if applicable to method)
  :type target_col str
  
  :param cv: Number of folds to use for cross validation.
  Number of observations in the Validation set for each fold = forecast period
  If None, a single fold is used
  :type cv Optional[int]
  
  :param time_col: Name of the time column in the dataset (needed by Prophet)
  Time column can also be the index, in which case, this would be the name of the index
  :type time_col str
  
  :rtype object
docstr_tok: ["Fits","the","model","to","the","data",":","param","ts_df","The","time","series","data","to","be","used","for","fitting","the","model",":","type","ts_df","pd.DataFrame",":","param","target_col","The","column","name","of","the","target","time","series","that","needs","to","be","modeled",".","All","other","columns","will","be","considered","as","exogenous","variables","(","if","applicable","to","method",")",":","type","target_col","str",":","param","cv",":","Number","of","folds","to","use","for","cross","validation",".","Number","of","observations","in","the","Validation","set","for","each","fold","=","forecast","period","If","None",",","a","single","fold","is","used",":","type","cv","Optional","[","int","]",":","param","time_col",":","Name","of","the","time","column","in","the","dataset","(","needed","by","Prophet",")","Time","column","can","also","be","the","index",",","in","which","case",",","this","would","be","the","name","of","the","index",":","type","time_col","str",":","rtype","object"]
code: def fit(
      self,
      ts_df: pd.DataFrame,
      target_col: str,
      cv: Optional[int],
      time_col: str,
  ):
      """
      Fits the model to the data
  
      :param ts_df The time series data to be used for fitting the model
      :type ts_df pd.DataFrame
  
      :param target_col The column name of the target time series that needs to be modeled.
      All other columns will be considered as exogenous variables (if applicable to method)
      :type target_col str
  
      :param cv: Number of folds to use for cross validation.
      Number of observations in the Validation set for each fold = forecast period
      If None, a single fold is used
      :type cv Optional[int]
  
      :param time_col: Name of the time column in the dataset (needed by Prophet)
      Time column can also be the index, in which case, this would be the name of the index
      :type time_col str
  
      :rtype object
      """
      # use all available threads/cores
  
      self.time_col = time_col
      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
  
      # print(f"Prophet Is Univariate: {self.univariate}")
  
      ts_df = copy.deepcopy(ts_df)
  
      ##### if you are going to use matplotlib with prophet data, it gives an error unless you do this.
      pd.plotting.register_matplotlib_converters()
  
      #### You have to import Prophet if you are going to build a Prophet model #############
      actual = "y"
      timecol = "ds"
  
      data = self.prep_col_names_for_prophet(ts_df=ts_df, test=False)
  
      if self.univariate:
          dft = data[[timecol, actual]]
      else:
          dft = data[[timecol, actual] + self.original_preds]
  
      ##### For most Financial time series data, 80 percent conf interval is enough...
      if self.verbose >= 1:
          print(
              "    Fit-Predict data (shape=%s) with Confidence Interval = %0.2f..."
              % (dft.shape, self.conf_int)
          )
      ### Make Sure you lower your desired interval width from the normal 95% to a more realistic 80%
      start_time = time.time()
  
      if self.univariate is False:
          for name in self.original_preds:
              self.model.add_regressor(name)
  
      print("  Starting Prophet Fit")
  
      if self.seasonality:
          (
              prophet_seasonality,
              prophet_period,
              fourier_order,
              prior_scale,
          ) = get_prophet_seasonality(
              self.time_interval, self.seasonal_period
          )
          self.model.add_seasonality(
              name=prophet_seasonality,
              period=prophet_period,
              fourier_order=fourier_order,
              prior_scale=prior_scale,
          )
          print(
              "       Adding %s seasonality to Prophet with period=%d, fourier_order=%d and prior_scale=%0.2f"
              % (
                  prophet_seasonality,
                  prophet_period,
                  fourier_order,
                  prior_scale,
              )
          )
      else:
          print(
              "      No seasonality assumed since seasonality flag is set to False"
          )
  
      if type(dft) == dask.dataframe.core.DataFrame:
          num_obs = dft.shape[0].compute()
      else:
          num_obs = dft.shape[0]
  
      ### 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)
  
      #########################################################################################
      # NOTE: This change to the FB recommendation will cause the cv folds from facebook to
      # be incompatible with the folds from the other models (in terms of periods of evaluation
      # as well as number of observations in each period). Hence the final comparison will
      # be biased since it will not compare the same folds.
  
      # The original implementation was giving issues under certain conditions, hence this change
      # to FB recommendation has been made as a temporary (short term) fix.
      # The root cause issue will need to be fixed eventually at a later point.
      #########################################################################################
  
      ### Prophet's Time Interval translates into frequency based on the following pandas date_range alias:
      #  Link: https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases
      ## This is done using the get_prophet_time_interval() function later.
  
      if self.time_interval in self.list_of_valid_time_ints:
          time_int = copy.deepcopy(self.time_interval)
      else:
          time_int = self.get_prophet_time_interval(for_cv=False)
  
      # First  Fold -->
      #   Train Set: 0:initial
      #   Test Set: initial:(initial+horizon)
      # Second Fold -->
      #   Train Set: (period):(initial+period)
      #   Test Set: (initial+period):(initial+horizon+ period)
      # Format: '850 D'
  
      print("  Starting Prophet Cross Validation")
      ################################################################################
      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
          )
      ###########################################################################################
      # cv = GapWalkForward(n_splits=NFOLDS, gap_size=0, test_size=self.forecast_period)
      max_trainsize = len(dft) - 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
          )
      y_preds = pd.DataFrame()
      print(
          "Max. iterations using expanding window cross validation = %d"
          % NFOLDS
      )
      start_time = time.time()
      rmse_folds = []
      norm_rmse_folds = []
      forecast_df_folds = []
  
      concatenated = pd.DataFrame()
      extra_concatenated = pd.DataFrame()
  
      if type(dft) == dask.dataframe.core.DataFrame:
          dft = dft.head(
              len(dft)
          )  ### this converts dask into a pandas dataframe
  
      if cv_in == 0:
          print(
              "Skipping cross validation steps since cross_validation = %s"
              % cv_in
          )
          model = Prophet(growth="linear")
  
          kwargs = (
              self.kwargs
          )  ## this limits iterations and hence speeds up prophet
      else:
          for fold_number, (train_index, test_index) in enumerate(
              cv.split(dft)
          ):
              dftx = dft.head(len(train_index) + len(test_index))
              train_fold = dftx.head(
                  len(train_index)
              )  ## now train will be the first segment of dftx
              test_fold = dftx.tail(
                  len(test_index)
              )  ### now test will be right after train in dftx
  
              horizon = len(test_fold)
              print(
                  f"\nFold Number: {fold_number+1} --> Train Shape: {train_fold.shape[0]} Test Shape: {test_fold.shape[0]}"
              )
  
              #########################################
              #### Define the model with fold data ####
              #########################################
  
              model = Prophet(growth="linear")
  
              ############################################
              #### Fit the model with train_fold data ####
              ############################################
  
              kwargs = {
                  "iter": 100
              }  ## this limits iterations and hence speeds up prophet
              model.fit(train_fold, **kwargs)
  
              #################################################
              #### Predict using model with test_fold data ####
              #################################################
  
              future_period = model.make_future_dataframe(
                  freq=time_int, periods=horizon
              )
              forecast_df = model.predict(future_period)
              ### Now compare the actuals with predictions ######
  
              y_pred = forecast_df["yhat"][-horizon:]
  
              concatenated = pd.DataFrame(
                  np.c_[test_fold[actual].values, y_pred.values],
                  columns=["original", "predicted"],
                  index=test_fold.index,
              )
  
              if fold_number == 0:
                  extra_concatenated = copy.deepcopy(concatenated)
              else:
                  extra_concatenated = extra_concatenated.append(
                      concatenated
                  )
  
              rmse_fold, rmse_norm = print_dynamic_rmse(
                  concatenated["original"].values,
                  concatenated["predicted"].values,
                  concatenated["original"].values,
              )
  
              print(
                  "Cross Validation window: %d completed"
                  % (fold_number + 1,)
              )
              rmse_folds.append(rmse_fold)
              norm_rmse_folds.append(rmse_norm)
  
          ######################################################
          ### This is where you consolidate the CV results #####
          ######################################################
          fig = model.plot(forecast_df)
          # rmse_mean = np.mean(rmse_folds)
          # print('Average CV RMSE over %d windows (macro) = %0.5f' %(fold_number+1,rmse_mean))
  
          # cv_micro = np.sqrt(mean_squared_error(y_trues.values, y_preds.values))
          # print('Average CV RMSE of all predictions (micro) = %0.5f' %cv_micro)
  
          try:
              print_ts_model_stats(
                  extra_concatenated["original"],
                  extra_concatenated["predicted"],
                  "Prophet",
              )
          except:
              print(
                  "Not able to plot Prophet CV results. Continuing..."
              )
  
          forecast_df_folds = extra_concatenated["predicted"].values
          # print("  End of Prophet Cross Validation")
          print(
              "Time Taken = %0.0f seconds"
              % ((time.time() - start_time))
          )
  
      #### Now you need to fit Prophet on the whole train data set ##########
      dftx = dft.head(len(dft))
      model = Prophet(growth="linear")
      self.model = model
      self.model.fit(dftx, **kwargs)
      print("  End of Prophet Fit")
  
      # num_obs_folds = df_cv.groupby('cutoff')['ds'].count()
  
      # https://stackoverflow.com/questions/54405704/check-if-all-values-in-dataframe-column-are-the-same
      # a = num_obs_folds.to_numpy()
      # all_equal = (a[0] == a).all()
  
      # if not all_equal:
      # print("WARNING: All folds did not have the same number of observations in the validation sets.")
      # print("Num Test Obs Per fold")
      # print(num_obs_folds)
  
      # rmse_folds = []
      # norm_rmse_folds = []
      # forecast_df_folds = []
  
      # df_cv_grouped = df_cv.groupby('cutoff')
      # for (_, loop_df) in df_cv_grouped:
      #    rmse, norm_rmse = print_dynamic_rmse(loop_df['y'], loop_df['yhat'], dft['y'])
      #    rmse_folds.append(rmse)
      #    norm_rmse_folds.append(norm_rmse)
      #    forecast_df_folds.append(loop_df)
  
      # print(f"RMSE Folds: {rmse_folds}")
      # print(f"Norm RMSE Folds: {norm_rmse_folds}")
      # print(f"Forecast DF folds: {forecast_df_folds}")
  
      # forecast = self.predict(simple=False, return_train_preds=True)
  
      # ####  We are going to plot Prophet's forecasts differently since it is better
      # dfa = plot_prophet(dft, forecast);
      # # Prophet makes Incredible Predictions Charts!
      # ###  There can't be anything simpler than this to make Forecasts!
      # #self.model.plot(forecast);  # make sure to add semi-colon in the end to avoid plotting twice
      # # Also their Trend, Seasonality Charts are Spot On!
      # try:
      #     self.model.plot_components(forecast)
      # except:
      #     print('Error in FB Prophet components forecast. Continuing...')
  
      # rmse, norm_rmse = print_dynamic_rmse(dfa['y'], dfa['yhat'], dfa['y'])
  
      # return self.model, forecast, rmse, norm_rmse
      return self.model, forecast_df_folds, rmse_folds, norm_rmse_folds
  
imports: from typing import Optional
  import logging
  import copy
  import time
  import numpy
  from sklearn.model_selection import TimeSeriesSplit
  import pandas
  from pandas.core.generic import NDFrame
  import pdb
  import dask
  import dask.dataframe
  import matplotlib.pyplot
  from prophet import Prophet
  from prophet.diagnostics import cross_validation
  from prophet.diagnostics import performance_metrics
  from prophet.plot import plot_cross_validation_metric
  from .build_base import BuildBase
  from ..utils import print_dynamic_rmse, quick_ts_plot, print_ts_model_stats
  from ..utils.logging import SuppressStdoutStderr
  from sklearn.metrics import mean_squared_error
  from prophet import Prophet
  import time
  import pdb
  import copy
  import matplotlib.pyplot
label_desc: Use image node_id 2 for calling the BuildProphet obj's underlying member method code with example usage: obj.fit(ts_df, target_col, cv, time_col) and returns: self,  forecast_df_folds,  rmse_folds,  norm_rmse_folds