code: def private_numeric_binning(
      X_col,
      sample_weight,
      noise_scale,
      max_bins,
      min_feature_val,
      max_feature_val,
      rng=None,
  ):
      # TODO: a few ways to improve this function:
      #   - it prefers the leftmost bin since it starts from there and accumulates
      #   - we are not guaranteed max_bins bins.  We could have more or less.  If for example all the
      #     noise were positive by chance, more could be above target_weight than planned. On the opposite
      #     side we could have badly placed weights and collapse more than required. The noisy bin weights
      #     are public information after the noise has been added, so we can use much more aggressively optimizing
      #     algorithms to more evently distribute the weight
  
      native = Native.get_native_singleton()
      uniform_weights, uniform_edges = np.histogram(
          X_col,
          bins=max_bins * 2,
          range=(min_feature_val, max_feature_val),
          weights=sample_weight,
      )
      noisy_weights = uniform_weights + native.generate_gaussian_random(
          rng=rng, stddev=noise_scale, count=uniform_weights.shape[0]
      )
  
      # Postprocess to ensure realistic bin values (min=0)
      noisy_weights = np.clip(noisy_weights, 0, None)
  
      # TODO PK: check with Harsha, but we can probably alternate the taking of nibbles from both ends
      # so that the larger leftover bin tends to be in the center rather than on the right.
  
      # Greedily collapse bins until they meet or exceed target_weight threshold
      sample_weight_total = (
          len(X_col) if sample_weight is None else np.sum(sample_weight)
      )
      target_weight = sample_weight_total / max_bins
      bin_weights, bin_cuts = [0], [uniform_edges[0]]
      curr_weight = 0
      for index, right_edge in enumerate(uniform_edges[1:]):
          curr_weight += noisy_weights[index]
          if curr_weight >= target_weight:
              bin_cuts.append(right_edge)
              bin_weights.append(curr_weight)
              curr_weight = 0
  
      if len(bin_weights) == 1:
          # since we're adding unbounded random noise, it's possible that the total weight is less than the
          # threshold required for a single bin.  It could in theory even be negative.
          # clip to the target_weight.  If we had more than the target weight we'd have a bin
  
          bin_weights.append(target_weight)
          bin_cuts = np.empty(0, dtype=np.float64)
      else:
          # Ignore min/max value as part of cut definition
          bin_cuts = np.array(bin_cuts, dtype=np.float64)[1:-1]
  
          # All leftover datapoints get collapsed into final bin
          bin_weights[-1] += curr_weight
  
      return bin_cuts, bin_weights
  
file_path: interpret/python/interpret-core/interpret/utils/_privacy.py
imports: import numpy
  from scipy.stats import norm
  from scipy.optimize import brentq
  from ._native import Native
  import logging
label_desc: Use image node_id 5 for calling a global function with example usage: private_numeric_binning(X_col, sample_weight, noise_scale, max_bins, min_feature_val, max_feature_val, rng) and returns: bin_cuts,  bin_weights