class_docstr: Confidence estimation block, uses inductive conformal predictors (ICPs) for model agnosticity 
class_docstr_tok: ["Confidence","estimation","block",",","uses","inductive","conformal","predictors","(","ICPs",")","for","model","agnosticity"]
code: def explain(
      self,
      row_insights: pd.DataFrame,
      global_insights: Dict[str, object],
      **kwargs,
  ) -> Tuple[pd.DataFrame, Dict[str, object]]:
      ns = SimpleNamespace(**kwargs)
  
      is_categorical = ns.target_dtype in (
          dtype.binary,
          dtype.categorical,
          dtype.cat_array,
          dtype.cat_tsarray,
      )
      is_numerical = ns.target_dtype in (
          dtype.integer,
          dtype.float,
          dtype.quantity,
          dtype.num_array,
          dtype.num_tsarray,
      )
      is_multi_ts = ns.tss.is_timeseries and ns.tss.horizon > 1
      is_anomaly_task = (
          is_numerical and ns.tss.is_timeseries and ns.anomaly_detection
      )
  
      if "confidence" in ns.predictions.columns:
          # bypass calibrator if model already outputs confidence
          row_insights["prediction"] = ns.predictions["prediction"]
          if (
              "upper" in ns.predictions.columns
              and "lower" in ns.predictions.columns
          ):
              row_insights["upper"] = ns.predictions["upper"]
              row_insights["lower"] = ns.predictions["lower"]
  
          if (
              not isinstance(ns.predictions["confidence"].iloc[0], list)
              and ns.tss.horizon > 1
          ):
              row_insights["confidence"] = ns.predictions[
                  "confidence"
              ].astype(object)
              row_insights["confidence"] = row_insights[
                  "confidence"
              ].apply(lambda x: [x for _ in range(ns.tss.horizon)])
          else:
              row_insights["confidence"] = ns.predictions["confidence"]
          return self._formatted(
              row_insights, global_insights, ns, is_numerical
          )
  
      if ns.analysis["icp"]["__mdb_active"]:
          icp_X = deepcopy(ns.data)
  
          # replace observed data w/predictions
          preds = ns.predictions["prediction"]
          if is_multi_ts and is_numerical:
              preds = np.array([np.array(p) for p in preds])
  
              for col in [
                  f"timestep_{i}" for i in range(1, ns.tss.horizon)
              ]:
                  if col in icp_X.columns:
                      icp_X.pop(col)  # erase ignorable columns
  
              target_cols = [ns.target_name] + [
                  f"{ns.target_name}_timestep_{i}"
                  for i in range(1, ns.tss.horizon)
              ]
              icp_X[target_cols] = preds
          elif is_multi_ts and is_categorical:
              preds = [p[0] for p in preds]
              icp_X[ns.target_name] = preds
          else:
              icp_X[ns.target_name] = preds
  
          if (is_numerical or is_categorical) and ns.analysis[
              "icp"
          ].get("__mdb_active", False):
              base_icp = ns.analysis["icp"]["__default"]
              # reorder DF index
              index = base_icp.index.values
              if ns.target_name not in index:
                  if is_multi_ts:
                      index = np.array(
                          list(index)
                          + [ns.target_name]
                          + [
                              f"{ns.target_name}_timestep_{i}"
                              for i in range(1, ns.tss.horizon)
                          ]
                      )
                  else:
                      index = np.append(index, ns.target_name)
              icp_X = icp_X.reindex(
                  columns=index
              )  # important, else bounds can be invalid
  
              # only one normalizer, even if it's a grouped time series task
              normalizer = base_icp.nc_function.normalizer
              if normalizer:
                  normalizer.prediction_cache = normalizer(
                      ns.encoded_data, args=PredictionArguments
                  )
                  icp_X[
                      "__mdb_selfaware_scores"
                  ] = normalizer.prediction_cache
  
              # get ICP predictions
              if is_multi_ts:
                  result_cols = (
                      [
                          "significance",
                          "lower",
                          "upper",
                      ]
                      + [
                          f"lower_timestep_{i}"
                          for i in range(1, ns.tss.horizon)
                      ]
                      + [
                          f"upper_timestep_{i}"
                          for i in range(1, ns.tss.horizon)
                      ]
                      + [
                          f"significance_timestep_{i}"
                          for i in range(1, ns.tss.horizon)
                      ]
                  )
              elif is_numerical:
                  result_cols = ["lower", "upper", "significance"]
              else:
                  result_cols = ["significance"]
              result = pd.DataFrame(
                  index=icp_X.index, columns=result_cols
              )
  
              # base ICP
              X = deepcopy(icp_X)
              # Calling `values` multiple times increased runtime of this function; referenced var is faster
              icp_values = X.values
  
              # get all possible ranges
              if is_numerical:
                  base_icp.nc_function.model.prediction_cache = preds
                  all_confs = base_icp.predict(icp_values)
  
              # categorical
              else:
                  predicted_proba = (
                      True
                      if any(
                          [
                              "__mdb_proba" in col
                              for col in ns.predictions.columns
                          ]
                      )
                      else False
                  )
                  if predicted_proba:
                      all_cat_cols = [
                          col
                          for col in ns.predictions.columns
                          if "__mdb_proba" in col
                          and "__mdb_unknown_cat" not in col
                      ]
                      class_dists = ns.predictions[all_cat_cols].values
                      for icol, cat_col in enumerate(all_cat_cols):
                          row_insights.loc[
                              X.index, cat_col
                          ] = class_dists[:, icol]
                  else:
                      ohe_enc = ns.analysis["label_encoders"]
                      class_dists = ohe_enc.transform(
                          np.array([p[0] for p in preds]).reshape(-1, 1)
                      )
  
                  base_icp.nc_function.model.prediction_cache = (
                      class_dists
                  )
  
                  all_ranges = np.array([base_icp.predict(icp_values)])
                  all_confs = np.swapaxes(
                      np.swapaxes(all_ranges, 0, 2), 0, 1
                  )
  
              # convert (B, 2, 99) into (B, 2) given width or error rate constraints
              if is_multi_ts and is_numerical:
                  significances, confs = get_ts_conf_range(
                      all_confs,
                      df_target_stddev=ns.analysis["df_target_stddev"],
                      positive_domain=ns.positive_domain,
                      fixed_conf=ns.pred_args.fixed_confidence,
                  )
  
                  result = self._ts_assign_confs(
                      result, X, confs, significances, ns.tss
                  )
  
              elif is_numerical:
                  significances, confs = get_numeric_conf_range(
                      all_confs,
                      df_target_stddev=ns.analysis["df_target_stddev"],
                      positive_domain=ns.positive_domain,
                      fixed_conf=ns.pred_args.fixed_confidence,
                  )
                  result.loc[X.index, "lower"] = confs[:, 0]
                  result.loc[X.index, "upper"] = confs[:, 1]
                  result.loc[X.index, "significance"] = significances
  
              else:
                  significances = get_categorical_conf(all_confs)
                  result.loc[
                      X.index, "significance"
                  ] = significances.flatten()
  
              # grouped time series, we replace bounds in rows that have a trained ICP
              if ns.analysis["icp"].get("__mdb_groups", False):
                  icps = ns.analysis["icp"]
                  group_keys = icps["__mdb_group_keys"]
  
                  for group in icps["__mdb_groups"]:
                      icp = icps[tuple(group)]
  
                      # check ICP has calibration scores
                      if icp.cal_scores[0].shape[0] > 0:
                          # filter rows by group
                          X = deepcopy(icp_X)
                          for key, val in zip(group_keys, group):
                              X = X[X[key] == val]
  
                          if X.size > 0:
                              # set ICP caches
                              if is_multi_ts and is_numerical:
                                  target_cols = [ns.target_name] + [
                                      f"{ns.target_name}_timestep_{i}"
                                      for i in range(1, ns.tss.horizon)
                                  ]
                                  icp.nc_function.model.prediction_cache = X[
                                      target_cols
                                  ].values
                                  [X.pop(col) for col in target_cols]
                              elif is_multi_ts and is_categorical:
                                  ohe_enc = ns.analysis[
                                      "label_encoders"
                                  ]
                                  preds = X.pop(ns.target_name).values
                                  pred_cache = ohe_enc.transform(
                                      np.array(
                                          [p[0] for p in preds]
                                      ).reshape(-1, 1)
                                  )
                                  icp.nc_function.model.prediction_cache = (
                                      pred_cache
                                  )
                              else:
                                  icp.nc_function.model.prediction_cache = X.pop(
                                      ns.target_name
                                  ).values
                              if icp.nc_function.normalizer:
                                  icp.nc_function.normalizer.prediction_cache = X.pop(
                                      "__mdb_selfaware_scores"
                                  ).values
  
                              # predict and get confidence level given width or error rate constraints
                              if is_multi_ts and is_numerical:
                                  all_confs = icp.predict(X.values)
                                  fixed_conf = (
                                      ns.pred_args.fixed_confidence
                                  )
                                  (
                                      significances,
                                      confs,
                                  ) = get_ts_conf_range(
                                      all_confs,
                                      df_target_stddev=ns.analysis[
                                          "df_target_stddev"
                                      ],
                                      positive_domain=ns.positive_domain,
                                      group=tuple(group),
                                      fixed_conf=fixed_conf,
                                  )
                                  result = self._ts_assign_confs(
                                      result,
                                      X,
                                      confs,
                                      significances,
                                      ns.tss,
                                  )
  
                              elif is_numerical:
                                  all_confs = icp.predict(X.values)
                                  fixed_conf = (
                                      ns.pred_args.fixed_confidence
                                  )
                                  (
                                      significances,
                                      confs,
                                  ) = get_numeric_conf_range(
                                      all_confs,
                                      df_target_stddev=ns.analysis[
                                          "df_target_stddev"
                                      ],
                                      positive_domain=ns.positive_domain,
                                      group=tuple(group),
                                      fixed_conf=fixed_conf,
                                  )
  
                                  # only replace where grouped ICP is more informative (i.e. tighter)
                                  if fixed_conf is None:
                                      default_widths = (
                                          result.loc[X.index, "upper"]
                                          - result.loc[X.index, "lower"]
                                      )
                                      grouped_widths = np.subtract(
                                          confs[:, 1], confs[:, 0]
                                      )
                                      insert_index = (
                                          default_widths
                                          > grouped_widths
                                      )[lambda x: x.isin([True])].index
                                      conf_index = (
                                          default_widths.reset_index(
                                              drop=True
                                          )
                                          > grouped_widths
                                      )[lambda x: x.isin([True])].index
  
                                      result.loc[
                                          insert_index, "lower"
                                      ] = confs[conf_index, 0]
                                      result.loc[
                                          insert_index, "upper"
                                      ] = confs[conf_index, 1]
                                      result.loc[
                                          insert_index, "significance"
                                      ] = significances[conf_index]
  
                              else:
                                  all_ranges = np.array(
                                      [icp.predict(X.values)]
                                  )
                                  all_confs = np.swapaxes(
                                      np.swapaxes(all_ranges, 0, 2),
                                      0,
                                      1,
                                  )
                                  significances = get_categorical_conf(
                                      all_confs
                                  )
                                  result.loc[
                                      X.index, "significance"
                                  ] = significances.flatten()
  
              row_insights["confidence"] = result["significance"]
  
              if is_numerical:
                  row_insights["lower"] = result["lower"]
                  row_insights["upper"] = result["upper"]
  
              # anomaly detection
              if is_anomaly_task:
                  row_insights["anomaly"] = None
                  if ns.target_name in ns.data.columns:
                      anomalies = get_anomalies(
                          row_insights,
                          ns.data[ns.target_name],
                          cooldown=ns.pred_args.anomaly_cooldown,
                      )
                      if "__mdb_ts_inferred" in ns.data.columns:
                          ignore_idxs = np.where(
                              ns.data["__mdb_ts_inferred"].values
                          )
                          anomalies[ignore_idxs] = None
                      row_insights["anomaly"] = list(anomalies)
  
          if ns.tss.is_timeseries and ns.tss.horizon > 1:
              if is_numerical and ns.pred_args.simple_ts_bounds:
                  row_insights = add_tn_num_conf_bounds(
                      row_insights, ns.tss
                  )
              elif not is_numerical:
                  row_insights = add_tn_cat_conf_bounds(
                      row_insights, ns.tss
                  )
  
          row_insights, global_insights = self._formatted(
              row_insights, global_insights, ns, is_numerical
          )
  
      return row_insights, global_insights
  
imports: import inspect
  from copy import deepcopy
  from typing import Dict, Tuple, Optional
  from types import SimpleNamespace
  import numpy
  import pandas
  from sklearn.preprocessing import OneHotEncoder
  from type_infer.dtype import dtype
  from lightwood.api.types import PredictionArguments
  from lightwood.helpers.ts import add_tn_num_conf_bounds, add_tn_cat_conf_bounds, get_ts_groups
  from lightwood.data import EncodedDs
  from lightwood.analysis.base import BaseAnalysisBlock
  from lightwood.analysis.nc.norm import Normalizer
  from lightwood.analysis.nc.icp import IcpRegressor, IcpClassifier, IcpTSRegressor
  from lightwood.analysis.nc.base import CachedRegressorAdapter, CachedClassifierAdapter, CachedTSAdapter
  from lightwood.analysis.nc.nc import BoostedAbsErrorErrFunc, RegressorNc, ClassifierNc, MarginErrFunc, TSNc, TSAbsErrorErrFunc
  from lightwood.analysis.nc.util import clean_df, set_conf_range, get_numeric_conf_range, get_categorical_conf, get_anomalies, get_ts_conf_range
label_desc: Use image node_id 3 for calling the ICP obj's underlying member method code with example usage: obj.explain(row_insights, global_insights) and returns: row_insights,  global_insights, self