code: def ale_num(
      predictor: Callable,
      X: np.ndarray,
      feature: int,
      feature_grid_points: Optional[np.ndarray] = None,
      min_bin_points: int = 4,
      check_feature_resolution: bool = True,
      low_resolution_threshold: int = 10,
      extrapolate_constant: bool = True,
      extrapolate_constant_perc: float = 10.0,
      extrapolate_constant_min: float = 0.1,
  ) -> Tuple[np.ndarray, ...]:
      """
      Calculate the first order ALE curve for a numerical feature.
  
      Parameters
      ----------
      predictor
          Model prediction function.
      X
          Dataset for which ALE curves are computed.
      feature
          Index of the numerical feature for which to calculate ALE.
      feature_grid_points
          Custom grid points. An `numpy` array defining the grid points for the given features.
      min_bin_points
          Minimum number of points each discretized interval should contain to ensure more precise
          ALE estimation. Only relevant for adaptive grid points (i.e., feature for which ``feature_grid_points=None``).
      check_feature_resolution
          Refer to :class:`ALE` documentation.
      low_resolution_threshold
          Refer to :class:`ALE` documentation.
      extrapolate_constant
          Refer to :class:`ALE` documentation.
      extrapolate_constant_perc
          Refer to :class:`ALE` documentation.
      extrapolate_constant_min
          Refer to :class:`ALE` documentation.
  
      Returns
      -------
      fvals
          Array of quantiles or custom grid-points of the input values.
      ale
          ALE values for each feature at each of the points in `fvals`.
      ale0
          The constant offset used to center the ALE curves.
  
      """
      if feature_grid_points is None:
          if check_feature_resolution:
              uniques = np.unique(X[:, feature])
              if len(uniques) <= low_resolution_threshold:
                  fvals = uniques
              else:
                  fvals, _ = adaptive_grid(
                      X[:, feature], min_bin_points
                  )
          else:
              fvals, _ = adaptive_grid(X[:, feature], min_bin_points)
      else:
          # set q to custom grid for feature
          min_val, max_val = X[:, feature].min(), X[:, feature].max()
          fvals = np.sort(feature_grid_points)
  
          if min_val > fvals[0]:
              # select the greatest grid point that is less or equal to the minimum feature value
              min_idx = np.where(fvals <= min_val)[0][-1]
              min_val = fvals[min_idx]
  
              if min_idx != 0:
                  logger.warning(
                      f"The leading bins of feature {feature} defined by the grid-points do not contain "
                      "any feature values. Automatically removing the empty leading bins to ensure that "
                      "each bin contains at least one feature value."
                  )
  
          if max_val < fvals[-1]:
              # select the smallest grid point that is larger or equal to the maximum feature value
              max_idx = np.where(fvals >= max_val)[0][0]
              max_val = fvals[max_idx]
  
              if max_idx != len(fvals) - 1:
                  logger.warning(
                      f"The ending bins of feature {feature} defined by the grid-points do not contain "
                      "any feature values. Automatically removing the empty ending bins to ensure that "
                      "each bin contains at least one feature value."
                  )
  
          # clip the values and remove duplicates
          fvals = np.unique(
              np.clip(fvals, a_min=min_val, a_max=max_val)
          )
  
          # add min feature value and maybe log a warning
          if fvals[0] > min_val:
              fvals = np.append(min_val, fvals)
              logger.warning(
                  f"Feature {feature} grid-points does not cover the lower feature values. "
                  "Automatically adding the minimum feature values to the grid-points."
              )
  
          # add max feature value and maybe log a warning
          if fvals[-1] < max_val:
              fvals = np.append(fvals, max_val)
              logger.warning(
                  f"Feature {feature} grid-points does not cover the larger feature values. "
                  "Automatically adding the maximum feature value to the grid points."
              )
  
          # check how many feature values are in each bin
          indices = np.searchsorted(fvals, X[:, feature], side="left")
          # put the smallest data point in the first interval
          indices[indices == 0] = 1
          # count the number of points in each interval without considering the first bin,
          # because the first bin will contain always 0 (see line above)
          interval_n = np.bincount(indices)[1:]
  
          if np.any(interval_n == 0):
              fvals = np.delete(
                  fvals, np.where(interval_n == 0)[0] + 1
              )  # +1 because we don't consider the first bin
              logger.warning(
                  f"Some bins of feature {feature} defined by the grid-points do not contain "
                  "any feature values. Automatically merging consecutive bins to ensure that "
                  "each bin contains at least one feature value."
              )
  
      # if the feature is constant, calculate the ALE on a small interval surrounding the feature value
      if len(fvals) == 1:
          if extrapolate_constant:
              delta = max(
                  fvals * extrapolate_constant_perc / 100,
                  extrapolate_constant_min,
              )
              fvals = np.hstack((fvals - delta, fvals + delta))
          else:
              # ALE is 0 at a constant feature value
              return fvals, np.array([[0.0]]), np.array([0.0])
  
      # find which interval each observation falls into
      indices = np.searchsorted(fvals, X[:, feature], side="left")
      indices[
          indices == 0
      ] = 1  # put the smallest data point in the first interval
      interval_n = np.bincount(
          indices
      )  # number of points in each interval
  
      # predictions for the upper and lower ranges of intervals
      z_low = X.copy()
      z_high = X.copy()
      z_low[:, feature] = fvals[indices - 1]
      z_high[:, feature] = fvals[indices]
      p_low = predictor(z_low)
      p_high = predictor(z_high)
  
      # finite differences
      p_deltas = p_high - p_low
  
      # make a dataframe for averaging over intervals
      concat = np.column_stack((p_deltas, indices))
      df = pd.DataFrame(concat)
      avg_p_deltas = (
          df.groupby(df.shape[1] - 1).mean().values
      )  # groupby indices
  
      # accumulate over intervals
      accum_p_deltas = np.cumsum(avg_p_deltas, axis=0)
  
      # pre-pend 0 for the left-most point
      zeros = np.zeros((1, accum_p_deltas.shape[1]))
      accum_p_deltas = np.insert(accum_p_deltas, 0, zeros, axis=0)
  
      # mean effect, R's `ALEPlot` and `iml` version (approximation per interval)
      ale0 = (
          0.5
          * (accum_p_deltas[:-1, :] + accum_p_deltas[1:, :])
          * interval_n[1:, np.newaxis]
      ).sum(axis=0)
      ale0 = ale0 / interval_n.sum()
  
      # crude approximation (assume data points on interval endpoints)
      # ale0 = accum_p_deltas.mean(axis=0)
  
      # exact marginalisation
      # exact_ale = accum_p_deltas[indices - 1] + ((X[:, feature] - q[indices])) / (q[indices] - q[indices - 1]) * (
      #            accum_p_deltas[indices] - accum_p_deltas[indices - 1])
      # ale0 = exact_ale.mean()
  
      # center
      ale = accum_p_deltas - ale0
  
      return fvals, ale, ale0
  
imports: import copy
  import math
  import numpy
  import pandas
  from itertools import count
  from functools import partial
  from typing import Callable, List, Optional, Tuple, Union, Dict, TYPE_CHECKING, no_type_check
  import sys
  import logging
  from alibi.api.interfaces import Explainer, Explanation
  from alibi.api.defaults import DEFAULT_META_ALE, DEFAULT_DATA_ALE
label_desc: Use image node_id 5 for calling a global function with example usage: ale_num(predictor, X, feature, feature_grid_points, min_bin_points, check_feature_resolution, low_resolution_threshold, extrapolate_constant, extrapolate_constant_perc, extrapolate_constant_min) and returns: fvals,  ale,  ale0, fvals,  np,  np