code: def flexible_grouping_evaluation(
      df,
      map_func_dict=None,
      groupby_col=None,
      agg_kwargs=None,
      extend_col_names=True,
      unpack_list=True,
      list_names_dict=None,
  ):
      """Flexible aggregation. Generates additional columns for evaluation via
      ``map_func_dict``, groups by ``groupby_col``, then aggregates according
      to ``agg_kwargs``.
  
      This function calls `pandas.DataFrame.apply` and
      `pandas.core.groupby.DataFrameGroupBy.agg` internally.
  
      Parameters
      ----------
      df : `pandas.DataFrame`
          DataFrame to transform / aggregate
      map_func_dict : `dict` [`str`, `callable`] or None, default None
          Row-wise transformation functions to create new columns.
          If None, no new columns are added.
  
          key: new column name
          value: row-wise function to apply to ``df`` to generate the column value.
                 Signature (row: `pandas.DataFrame`) -> transformed value: `float`.
  
          For example::
  
              map_func_dict = {
                  "residual": lambda row: row["predicted"] - row["actual"],
                  "squared_error": lambda row: (row["predicted"] - row["actual"])**2
              }
  
      groupby_col : `str` or None, default None
          Which column to group by.
          Can be in ``df`` or generated by ``map_func_dict``.
          If None, no grouping or aggregation is done.
      agg_kwargs : `dict` or None, default None
          Passed as keyword args to `pandas.core.groupby.DataFrameGroupBy.aggregate` after creating
          new columns and grouping by ``groupby_col``. Must be provided if ``groupby_col is not None``.
          To fully customize output column names, pass a dictionary as shown below.
  
          For example::
  
              # Example 1, named aggregation to explicitly name output columns.
              # Assume ``df`` contains ``abs_percent_err``, ``abs_err`` columns.
              # Output columns are "MedAPE", "MAPE", "MAE", etc. in a single level index.
              from functools import partial
              agg_kwargs = {
                  # output column name: (column to aggregate, aggregation function)
                  "MedAPE": pd.NamedAgg(column="abs_percent_err", aggfunc=np.nanmedian),
                  "MAPE": pd.NamedAgg(column="abs_percent_err", aggfunc=np.nanmean),
                  "MAE": pd.NamedAgg(column="abs_err", aggfunc=np.nanmean),
                  "q95_abs_err": pd.NamedAgg(column="abs_err", aggfunc=partial(np.nanquantile, q=0.95)),
                  "q05_abs_err": pd.NamedAgg(column="abs_err", aggfunc=partial(np.nanquantile, q=0.05)),
              }
  
              # Example 2, multi-level aggregation using `func` parameter
              # to `pandas.core.groupby.DataFrameGroupBy.aggregate`.
              # Assume ``df`` contains ``y1``, ``y2`` columns.
              agg_kwargs = {
                  "func": {
                      "y1": [np.nanmedian, np.nanmean],
                      "y2": [np.nanmedian, np.nanmax],
                  }
              }
              # `extend_col_names` controls the output column names
              extend_col_names = True  # output columns are "y1_nanmean", "y1_nanmedian", "y2_nanmean", "y2_nanmax"
              extend_col_names = False  # output columns are "nanmean", "nanmedian", "nanmean", "nanmax"
  
      extend_col_names : `bool` or None, default True
          How to flatten index after aggregation.
          In some cases, the column index after aggregation is a multi-index.
          This parameter controls how to flatten an index with 2 levels to 1 level.
  
              - If None, the index is not flattened.
              - If True, column name is a composite: ``{index0}_{index1}``
                Use this option if index1 is not unique.
              - If False, column name is simply ``{index1}``
  
          Ignored if the ColumnIndex after aggregation has only one level (e.g.
          if named aggregation is used in ``agg_kwargs``).
  
      unpack_list : `bool`, default True
          Whether to unpack (flatten) columns that contain list/tuple after aggregation,
          to create one column per element of the list/tuple.
          If True, ``list_names_dict`` can be used to rename the unpacked columns.
  
      list_names_dict : `dict` [`str`, `list` [`str`]] or None, default None
          If ``unpack_list`` is True, this dictionary can optionally be
          used to rename the unpacked columns.
  
              - Key = column name after aggregation, before upacking.
                E.g. ``{index0}_{index1}`` or ``{index1}`` depending on ``extend_col_names``.
              - Value = list of names to use for the unpacked columns. Length must match
                the length of the lists contained in the column.
  
          If a particular list/tuple column is not found in this dictionary, appends
          0, 1, 2, ..., n-1 to the original column name, where n = list length.
  
          For example, if the column contains a tuple of length 4 corresponding to
          quantiles 0.1, 0.25, 0.75, 0.9, then the following would be appropriate::
  
              aggfunc = lambda grp: partial(np.nanquantile, q=[0.1, 0.25, 0.75, 0.9])(grp).tolist()
              agg_kwargs = {
                  "value_Q": pd.NamedAgg(column="value", aggfunc=aggfunc)
              }
              list_names_dict = {
                  # the key is the name of the unpacked column
                  "value_Q" : ["Q0.10", "Q0.25", "Q0.75", "Q0.90"]
              }
              # Output columns are "Q0.10", "Q0.25", "Q0.75", "Q0.90"
  
              # In this example, if list_names_dict=None, the default output column names
              # would be: "value_Q0", "value_Q1", "value_Q2", "value_Q3"
  
      Returns
      -------
      df_transformed : `pandas.DataFrame`
          df after transformation and optional aggregation.
  
          If ``groupby_col`` is None, returns ``df`` with additional columns as the keys in ``map_func_dict``.
          Otherwise, ``df`` is grouped by ``groupby_col`` and this becomes the index. Columns
          are determined by ``agg_kwargs`` and ``extend_col_names``.
      """
      if groupby_col and not agg_kwargs:
          raise ValueError(
              "Must specify `agg_kwargs` if grouping is requested via `groupby_col`."
          )
      if agg_kwargs and not groupby_col:
          log_message(
              f"`agg_kwargs` is ignored because `groupby_col` is None. "
              f"Specify `groupby_col` to allow aggregation.",
              LoggingLevelEnum.WARNING,
          )
  
      df = df.copy()
      if map_func_dict is not None:
          for col_name, func in map_func_dict.items():
              df[col_name] = df.apply(func, axis=1)
  
      if groupby_col is not None:
          groups = df.groupby(groupby_col)
          with warnings.catch_warnings():
              # Ignores pandas FutureWarning. Use NamedAgg in pandas 0.25.+
              warnings.filterwarnings(
                  "ignore",
                  message="using a dict with renaming is deprecated",
                  category=FutureWarning,
              )
              df_transformed = groups.agg(**agg_kwargs)
          if (
              extend_col_names is not None
              and df_transformed.columns.nlevels > 1
          ):
              # Flattens multi-level column index
              if extend_col_names:
                  # By concatenating names
                  df_transformed.columns = [
                      "_".join(col).strip("_")
                      for col in df_transformed.columns
                  ]
              else:
                  # By using level 1 names
                  df_transformed.columns = list(
                      df_transformed.columns.get_level_values(1)
                  )
                  if np.any(df_transformed.columns.duplicated()):
                      warnings.warn(
                          "Column names are not unique. Use `extend_col_names=True` "
                          "to uniquely identify every column."
                      )
      else:
          # No grouping is requested
          df_transformed = df
  
      if unpack_list and df_transformed.shape[0] > 0:
          # Identifies the columns that contain list elements
          which_list_cols = df_transformed.iloc[0].apply(
              lambda x: isinstance(x, (list, tuple))
          )
          list_cols = list(which_list_cols[which_list_cols].index)
          for col in list_cols:
              if isinstance(df_transformed[col], pd.DataFrame):
                  warnings.warn(
                      f"Skipping list unpacking for `{col}`. There are multiple columns "
                      f"with this name. Make sure column names are unique to enable unpacking."
                  )
                  continue
              # Unpacks the column, creating one column for each list entry
              list_df = pd.DataFrame(df_transformed[col].to_list())
              n_cols = list_df.shape[1]
              # Adds column names
              if list_names_dict is not None and col in list_names_dict:
                  found_length = len(list_names_dict[col])
                  if found_length != n_cols:
                      raise ValueError(
                          f"list_names_dict['{col}'] has length {found_length}, "
                          f"but there are {n_cols} columns to name. Example row(s):\n"
                          f"{list_df.head(2)}"
                      )
                  list_df.columns = [
                      f"{list_names_dict.get(col)[i]}"
                      for i in range(n_cols)
                  ]
              else:
                  list_df.columns = [f"{col}{i}" for i in range(n_cols)]
              # replaces original column with new ones
              list_df.index = df_transformed.index
              del df_transformed[col]
              df_transformed = pd.concat(
                  [df_transformed, list_df], axis=1
              )
  
          if list_names_dict:
              unused_names = sorted(
                  list(set(list_names_dict.keys()) - set(list_cols))
              )
              if len(unused_names) > 0:
                  warnings.warn(
                      "These names from `list_names_dict` are not used, because the "
                      "column (key) is not found in the dataframe after aggregation:\n"
                      f"{unused_names}.\nAvailable columns are:\n"
                      f"{list_cols}."
                  )
  
      return df_transformed
  
imports: import warnings
  import numpy
  import pandas
  import plotly.graph_objects
  from plotly.colors import DEFAULT_PLOTLY_COLORS
  from plotly.subplots import make_subplots
  from greykite.common import constants
  from greykite.common.features.timeseries_features import build_time_features_df
  from greykite.common.logging import LoggingLevelEnum
  from greykite.common.logging import log_message
  from greykite.common.python_utils import update_dictionary
  from greykite.common.viz.colors_utils import get_color_palette
  from greykite.common.viz.colors_utils import get_distinct_colors
label_desc: Use image node_id 8 for calling a global function with example usage: flexible_grouping_evaluation(df, map_func_dict, groupby_col, agg_kwargs, extend_col_names, unpack_list, list_names_dict) and returns: df_transformed