code: def apply_func(feature, selected_metrics, df, arr):
      """Apply function to dataframe
  
      :param str feature: feature currently looping over
      :param list selected_metrics: list of selected metrics to apply to
      :param df: pandas data frame that function in arr is applied to
      :param dict arr: dictionary containing the function to be applied to pandas dataframe.
      :return: dictionary with outputs of applied-to metric pd.Series
      """
      # basic checks of feature
      if (
          "features" in arr
          and len(arr["features"]) > 0
          and feature not in arr["features"]
      ):
          return {}
  
      # get func input
      keys = list(arr.keys())
  
      assert "func" in keys, "function input is insufficient."
      func = arr["func"]
  
      if "prefix" not in keys or arr["prefix"] is None:
          arr["prefix"] = ""
      if len(arr["prefix"]) > 0 and not arr["prefix"].endswith("_"):
          arr["prefix"] = arr["prefix"] + "_"
      if "suffix" not in keys or arr["suffix"] is None:
          arr["suffix"] = (
              func.__name__ if len(arr["prefix"]) == 0 else ""
          )
      if len(arr["suffix"]) > 0 and not arr["suffix"].startswith("_"):
          arr["suffix"] = "_" + arr["suffix"]
      suffix = arr["suffix"]
      prefix = arr["prefix"]
  
      args = ()
      kwargs = {}
      if "kwargs" in keys:
          kwargs = arr["kwargs"]
      if "args" in keys:
          args = arr["args"]
  
      # apply func
      if len(selected_metrics) > 0 or (
          "metrics" in keys and len(arr["metrics"]) > 0
      ):
          metrics = (
              arr["metrics"]
              if ("metrics" in keys and len(arr["metrics"]) > 0)
              else selected_metrics
          )
          metrics = [m for m in metrics if m in df.columns]
          if len(metrics) == 0:
              return {}
          df = df[metrics] if len(metrics) >= 2 else df[metrics[0]]
  
      if (
          "entire" in arr
          and arr["entire"] is not None
          and arr["entire"] is not False
          and arr["entire"] != 0
      ):
          obj = func(df, *args, **kwargs)
      else:
          obj = df.apply(func, args=args, **kwargs)
  
      # convert object to dict format
      if not isinstance(
          obj, (pd.Series, pd.DataFrame, list, tuple, np.ndarray)
      ) and isinstance(df, pd.Series):
          obj = {df.name: obj}
      elif not isinstance(
          obj, (pd.Series, pd.DataFrame, list, tuple, np.ndarray)
      ) and isinstance(df, pd.DataFrame):
          obj = {"_".join(df.columns): obj}
      elif (
          isinstance(obj, (list, tuple, np.ndarray))
          and isinstance(df, pd.DataFrame)
          and len(df.columns) == len(obj)
      ):
          obj = dict(zip(df.columns, obj))
      elif (
          isinstance(obj, (list, tuple, np.ndarray))
          and isinstance(df, pd.Series)
          and len(df.index) == len(obj)
      ):
          obj = {df.name: pd.Series(data=obj, index=df.index)}
      elif (
          isinstance(obj, (list, tuple, np.ndarray))
          and isinstance(df, pd.DataFrame)
          and len(df.index) == len(obj)
      ):
          obj = {
              "_".join(df.columns): pd.Series(data=obj, index=df.index)
          }
      elif (
          isinstance(obj, pd.Series)
          and isinstance(df, pd.Series)
          and len(obj) == len(df)
          and all(obj.index == df.index)
      ):
          obj = {df.name: obj}
      elif (
          isinstance(obj, pd.Series)
          and isinstance(df, pd.DataFrame)
          and len(obj) == len(df)
          and all(obj.index == df.index)
      ):
          obj = {"_".join(df.columns): obj}
      # e.g. output of normalized_hist_mean_cov: a dataframe with one column, actually a series
      elif (
          isinstance(obj, pd.DataFrame)
          and len(obj.columns) == 1
          and (
              len(obj.index) != len(df.index)
              or (
                  len(obj.index) == len(df.index)
                  and (obj.index != df.index).any()
              )
          )
      ):
          obj = obj[obj.columns[0]].to_dict()
      elif isinstance(obj, pd.Series):
          # e.g. output of np.mean of np.std: results in one number per column when applied to a dataframe
          obj = obj.to_dict()
      elif isinstance(obj, pd.DataFrame):
          # when apply to one column returns another column
          obj = {c: obj[c] for c in obj.columns}
      assert isinstance(obj, dict)
  
      # add prefix and suffix to dict keys
      keys = list(obj.keys())
      for k in keys:
          obj[prefix + k + suffix] = obj.pop(k)
  
      return obj
  
file_path: popmon/popmon/analysis/apply_func.py
imports: from __future__ import annotations
  import warnings
  import numpy
  import pandas
  from popmon.base import Module
  from popmon.utils import parallel
label_desc: Use image node_id 2 for calling a global function with example usage: apply_func(feature, selected_metrics, df, arr) and returns: obj, dict, dict