code: def fit_ml_model(
      df,
      model_formula_str=None,
      fit_algorithm="linear",
      fit_algorithm_params=None,
      y_col=None,
      pred_cols=None,
      min_admissible_value=None,
      max_admissible_value=None,
      uncertainty_dict=None,
      normalize_method="zero_to_one",
      regression_weight_col=None,
      remove_intercept=False,
  ):
      """Fits predictive ML (machine learning) models to continuous
      response vector (given in ``y_col``)
      and returns fitted model.
  
      Parameters
      ----------
      df : pd.DataFrame
          A data frame with the response vector (y) and the feature columns
          (``x_mat``).
      model_formula_str : str
          The prediction model formula string e.g. "y~x1+x2+x3*x4".
          This is similar to R formulas.
          See https://patsy.readthedocs.io/en/latest/formulas.html#how-formulas-work.
      fit_algorithm : `str`, optional, default "linear"
          The type of predictive model used in fitting.
  
          See `~greykite.algo.common.ml_models.fit_model_via_design_matrix`
          for available options and their parameters.
      fit_algorithm_params : `dict` or None, optional, default None
          Parameters passed to the requested fit_algorithm.
          If None, uses the defaults in `~greykite.algo.common.ml_models.fit_model_via_design_matrix`.
      y_col : str
          The column name which has the value of interest to be forecasted
          If the model_formula_str is not passed, ``y_col`` e.g. ["y"]
          is used as the response vector column
      pred_cols : List[str]
          The names of the feature columns
          If the ``model_formula_str`` is not passed, ``pred_cols`` e.g.
          ["x1", "x2", "x3"] is used as the design matrix columns
      min_admissible_value : Optional[Union[int, float, double]]
          the minimum admissible value for the ``predict`` function to return
      max_admissible_value : Optional[Union[int, float, double]]
          the maximum admissible value for the ``predict`` function to return
      uncertainty_dict : `dict` or None
          If passed as a dictionary an uncertainty model will be fit.
          The items in the dictionary are:
  
              ``"uncertainty_method"`` : `str`
                  the title of the method
                  as of now only "simple_conditional_residuals" is implemented
                  which calculates CIs by using residuals
              ``"params"`` : `dict`
                  A dictionary of parameters needed for the ``uncertainty_method``
                  requested
  
      normalize_method : `str` or None, default "zero_to_one"
          If a string is provided, it will be used as the normalization method
          in `~greykite.common.features.normalize.normalize_df`, passed via
          the argument ``method``.
          Available options are: "zero_to_one", "statistical", "minus_half_to_half", "zero_at_origin".
          If None, no normalization will be performed.
          See that function for more details.
      regression_weight_col : `str` or None, default None
          The column name for the weights to be used in weighted regression version
          of applicable machine-learning models.
      remove_intercept : `bool`, default False
          Whether to remove explicit and implicit intercepts.
          By default, `patsy` will make the design matrix always full rank.
          It will always include an intercept term unless we specify "-1" or "+0".
          However, if there are categorical variables, even we specify "-1" or "+0",
          it will include an implicit intercept by adding all levels of a categorical
          variable into the design matrix.
          Sometimes we don't want this to happen.
          Setting this parameter to True will remove both explicit and implicit intercepts.
  
      Returns
      -------
      trained_model : `dict`
          Trained model dictionary with keys:
  
              - "y" : response values
              - "x_design_info" : design matrix information
              - "ml_model" : A trained model with predict method
              - "uncertainty_model" : `dict`
                  The returned uncertainty_model dict from
                  `~greykite.algo.uncertainty.conditional.conf_interval.conf_interval`.
              - "ml_model_summary": model summary
              - "y_col" : response columns
              - "x_mat ": design matrix
              - "min_admissible_value" : minimum acceptable value
              - "max_admissible_value" : maximum acceptable value
              - "normalize_df_func" : normalization function
              - "regression_weight_col" : regression weight column
              - "alpha" : the regularization term from the linear / ridge regression.
                  Note that the OLS (ridge) estimator is ``inv(X.T @ X + alpha * np.eye(p)) @ X.T @ Y =: H @ Y``.
              - "p_effective" : effective number of parameters.
                  In linear regressions, it is also equal to ``trace(X @ H)``, where H is defined above.
                  ``X @ H`` is also called the hat matrix.
              - "h_mat" : the H matrix (p by n) in linear regression estimator, as defined above.
                  Note that H is not necessarily of full-rank p even in ridge regression.
                  ``H = inv(X.T @ X + alpha * np.eye(p)) @ X.T``.
              - "sigma_scaler" : theoretical scaler of the estimated sigma.
                  Volatility model estimates sigma by taking the sample standard deviation, and
                  we need to scale it by ``np.sqrt((n_train - 1) / (n_train - p_effective))`` to obtain
                  an unbiased estimator.
              - "x_mean" : column mean of ``x_mat`` as a row vector.
                  This is stored and used in ridge regression to compute the prediction intervals.
                  In other methods, it is set to `None`.
      """
  
      # Builds model matrices.
      res = design_mat_from_formula(
          df=df,
          model_formula_str=model_formula_str,
          y_col=y_col,
          pred_cols=pred_cols,
          remove_intercept=remove_intercept,
      )
  
      y = res["y"]
      y_mean = np.mean(y)
      y_std = np.std(y)
      x_mat = res["x_mat"]
      y_col = res["y_col"]
      x_design_info = res["x_design_info"]
      drop_intercept_col = res["drop_intercept_col"]
  
      normalize_df_func = None
      if normalize_method is not None:
          if "Intercept" in (x_mat.columns):
              cols = [
                  col
                  for col in list(x_mat.columns)
                  if col != "Intercept"
              ]
          else:
              cols = list(x_mat.columns)
          normalize_info = normalize_df(
              df=x_mat[cols],
              method=normalize_method,
              drop_degenerate_cols=False,
              replace_zero_denom=True,
          )
          x_mat[cols] = normalize_info["normalized_df"]
          x_mat = x_mat.fillna(value=0)
          normalize_df_func = normalize_info["normalize_df_func"]
  
      sample_weight = None
      if regression_weight_col is not None:
          if df[regression_weight_col].min() < 0:
              raise ValueError(
                  "Weights can not be negative. "
                  f"The column {regression_weight_col} includes negative values."
              )
          sample_weight = df[regression_weight_col]
  
      # Prediction model generated by using all observed data.
      ml_model = fit_model_via_design_matrix(
          x_train=x_mat,
          y_train=y,
          fit_algorithm=fit_algorithm,
          fit_algorithm_params=fit_algorithm_params,
          sample_weight=sample_weight,
      )
  
      # Obtains `alpha`, `p_effective`, `h_mat` (H), and `sigma_scaler`.
      # See comments below the variables.
      # Read more at https://online.stat.psu.edu/stat508/lesson/5/5.1 or
      # book: “Applied Regression Analysis” by Norman R. Draper, Harry Smith.
      alpha = None
      """The regularization term from the linear / ridge regression.
      Note that the OLS (ridge) estimator is ``inv(X.T @ X + alpha * np.eye(p)) @ X.T @ Y =: H @ Y``.
      """
      p_effective = None
      """Effective number of parameters.
      In linear regressions, it is also equal to ``trace(X @ H)``, where H is defined above.
      ``X @ H`` is also called the hat matrix.
      """
      h_mat = None
      """The H matrix (p by n) in linear regression estimator, as defined above.
      Note that H is not necessarily of full-rank p even in ridge regression.
      ``H = inv(X.T @ X + alpha * np.eye(p)) @ X.T``.
      """
      sigma_scaler = None
      """Theoretical scaler of the estimated sigma.
      Volatility model estimates sigma by taking the sample standard deviation, and
      we need to scale it by ``np.sqrt((n_train - 1) / (n_train - p_effective))`` to obtain
      an unbiased estimator.
      """
      x_mean = None
      """Column mean of ``x_mat`` as a row vector.
      This is stored and used in ridge regression to compute the prediction intervals.
      In other methods, it is set to `None`.
      """
      if fit_algorithm in ["ridge", "linear"]:
          X = np.array(x_mat)
          n_train, p = X.shape
          # Extracts `alpha` from the fitted ML model.
          # In linear regression, the rank of the design matrix is `p_effective`,
          # but `RidgeCV` we need to manually derive it by taking the trace.
          # Note that `RidgeCV` centers `X` and `Y` before fitting, hence we need to center `X` too.
          if fit_algorithm == "ridge":
              alpha = ml_model.alpha_
              x_mean = X.mean(axis=0).reshape(1, -1)
              X = X - x_mean
          else:
              alpha = 0
              p_effective = np.linalg.matrix_rank(X)
          # Computes `h_mat` (H, p x n).
          try:
              h_mat = get_h_mat(x_mat=X, alpha=alpha)
              if fit_algorithm == "ridge":
                  # Computes the effective number of parameters.
                  # Note that `p_effective` is the trace of `X @ h_mat` plus 1 for intercept, however
                  # computing `trace(h_mat @ X)` is more efficient due to much faster matrix multiplication.
                  p_effective = (
                      round(np.trace(h_mat @ X), 6) + 1
                  )  # Avoids floating issues e.g. 1.9999999999999998.
          except np.linalg.LinAlgError as e:
              message = traceback.format_exc()
              warning_msg = (
                  f"Error '{e}' occurred when computing `h_mat`, no variance scaling is done!\n"
                  f"{message}"
              )
              log_message(warning_msg, LoggingLevelEnum.WARNING)
              warnings.warn(warning_msg)
  
          if p_effective is not None and round(p_effective) < n_train:
              # Computes scaler on sigma estimate.
              sigma_scaler = np.sqrt(
                  (n_train - 1) / (n_train - p_effective)
              )
          else:
              warnings.warn(
                  f"Zero degrees of freedom ({n_train}-{p_effective}) or the inverse solver failed. "
                  f"Likely caused by singular `X.T @ X + alpha * np.eye(p)`. "
                  f'Please check "x_mat", "alpha". '
                  f"`sigma_scaler` cannot be computed!"
              )
  
      # Uncertainty model is fitted if `uncertainty_dict` is passed.
      uncertainty_model = None
      if uncertainty_dict is not None:
          uncertainty_method = uncertainty_dict["uncertainty_method"]
          if uncertainty_method == "simple_conditional_residuals":
              # Resets index to match behavior of predict before assignment.
              new_df = df.reset_index(drop=True)
              (new_x_mat,) = patsy.build_design_matrices(
                  [x_design_info], data=new_df, return_type="dataframe"
              )
              if drop_intercept_col is not None:
                  new_x_mat = new_x_mat.drop(columns=drop_intercept_col)
              if normalize_df_func is not None:
                  if "Intercept" in list(x_mat.columns):
                      cols = [
                          col
                          for col in list(x_mat.columns)
                          if col != "Intercept"
                      ]
                  else:
                      cols = list(x_mat.columns)
                  new_x_mat[cols] = normalize_df_func(new_x_mat[cols])
              new_x_mat = new_x_mat.fillna(value=0)
              new_df[f"{y_col}_pred"] = ml_model.predict(new_x_mat)
              new_df[RESIDUAL_COL] = (
                  new_df[y_col] - new_df[f"{y_col}_pred"]
              )
  
              # Re-assigns some param defaults for function `conf_interval`
              # with values best suited to this case.
              conf_interval_params = {
                  "quantiles": [0.025, 0.975],
                  "sample_size_thresh": 10,
              }
  
              if uncertainty_dict["params"] is not None:
                  conf_interval_params.update(
                      uncertainty_dict["params"]
                  )
              uncertainty_model = conf_interval(
                  df=new_df,
                  distribution_col=RESIDUAL_COL,
                  offset_col=y_col,
                  sigma_scaler=sigma_scaler,
                  h_mat=h_mat,
                  x_mean=x_mean,
                  min_admissible_value=min_admissible_value,
                  max_admissible_value=max_admissible_value,
                  **conf_interval_params,
              )
          else:
              raise NotImplementedError(
                  f"uncertainty method: {uncertainty_method} is not implemented"
              )
  
      # We get the model summary for a subset of models
      # where summary is available (`statsmodels` module),
      # or summary can be constructed (a subset of models from `sklearn`).
      ml_model_summary = None
      if "statsmodels" in fit_algorithm:
          ml_model_summary = ml_model.summary()
      elif hasattr(ml_model, "coef_"):
          var_names = list(x_mat.columns)
          coefs = ml_model.coef_
          ml_model_summary = pd.DataFrame(
              {"variable": var_names, "coef": coefs}
          )
  
      trained_model = {
          "y": y,
          "y_mean": y_mean,
          "y_std": y_std,
          "x_design_info": x_design_info,
          "ml_model": ml_model,
          "uncertainty_model": uncertainty_model,
          "ml_model_summary": ml_model_summary,
          "y_col": y_col,
          "x_mat": x_mat,
          "min_admissible_value": min_admissible_value,
          "max_admissible_value": max_admissible_value,
          "normalize_df_func": normalize_df_func,
          "regression_weight_col": regression_weight_col,
          "drop_intercept_col": drop_intercept_col,
          "alpha": alpha,
          "h_mat": h_mat,
          "p_effective": p_effective,
          "sigma_scaler": sigma_scaler,
          "x_mean": x_mean,
      }
  
      if uncertainty_dict is None:
          fitted_df = predict_ml(
              fut_df=df, trained_model=trained_model
          )["fut_df"]
      else:
          fitted_df = predict_ml_with_uncertainty(
              fut_df=df, trained_model=trained_model
          )["fut_df"]
  
      trained_model["fitted_df"] = fitted_df
  
      return trained_model
  
imports: import random
  import re
  import traceback
  import warnings
  from typing import Dict
  from typing import List
  from typing import Optional
  import matplotlib
  import numpy
  import pandas
  import patsy
  import scipy
  import statsmodels.api
  from pandas.plotting import register_matplotlib_converters
  from sklearn.ensemble import GradientBoostingRegressor
  from sklearn.ensemble import RandomForestRegressor
  from sklearn.linear_model import ElasticNetCV
  from sklearn.linear_model import LarsCV
  from sklearn.linear_model import LassoCV
  from sklearn.linear_model import LassoLarsCV
  from sklearn.linear_model import RidgeCV
  from sklearn.linear_model import SGDRegressor
  from greykite.algo.common.l1_quantile_regression import QuantileRegression
  from greykite.algo.uncertainty.conditional.conf_interval import conf_interval
  from greykite.algo.uncertainty.conditional.conf_interval import predict_ci
  from greykite.common.constants import RESIDUAL_COL
  from greykite.common.constants import R2_null_model_score
  from greykite.common.evaluation import calc_pred_err
  from greykite.common.evaluation import r2_null_model_score
  from greykite.common.features.normalize import normalize_df
  from greykite.common.logging import LoggingLevelEnum
  from greykite.common.logging import log_message
  from greykite.common.python_utils import group_strs_with_regex_patterns
  from greykite.common.viz.timeseries_plotting import plot_multivariate
  import matplotlib.pyplot
label_desc: Use image node_id 5 for calling a global function with example usage: fit_ml_model(df, model_formula_str, fit_algorithm, fit_algorithm_params, y_col, pred_cols, min_admissible_value, max_admissible_value, uncertainty_dict, normalize_method, regression_weight_col, remove_intercept) and returns: trained_model