code: def _k_prototypes_single(
      Xnum,
      Xcat,
      nnumattrs,
      ncatattrs,
      n_clusters,
      n_points,
      max_iter,
      num_dissim,
      cat_dissim,
      gamma,
      init,
      init_no,
      verbose,
      random_state,
      sample_weight=None,
  ):
      # For numerical part of initialization, we don't have a guarantee
      # that there is not an empty cluster, so we need to retry until
      # there is none.
      random_state = check_random_state(random_state)
      init_tries = 0
      while True:
          init_tries += 1
          # _____ INIT _____
          if verbose:
              print("Init: initializing centroids")
          if isinstance(init, str) and init.lower() == "huang":
              centroids = init_huang(
                  Xcat, n_clusters, cat_dissim, random_state
              )
          elif isinstance(init, str) and init.lower() == "cao":
              centroids = init_cao(Xcat, n_clusters, cat_dissim)
          elif isinstance(init, str) and init.lower() == "random":
              seeds = random_state.choice(range(n_points), n_clusters)
              centroids = Xcat[seeds]
          elif isinstance(init, list):
              # Make sure inits are 2D arrays.
              init = [
                  np.atleast_2d(cur_init).T
                  if len(cur_init.shape) == 1
                  else cur_init
                  for cur_init in init
              ]
              assert init[0].shape[0] == n_clusters, (
                  "Wrong number of initial numerical centroids in init "
                  f"({init[0].shape[0]}, should be {n_clusters})."
              )
              assert init[0].shape[1] == nnumattrs, (
                  "Wrong number of numerical attributes in init "
                  f"({init[0].shape[1]}, should be {nnumattrs})."
              )
              assert init[1].shape[0] == n_clusters, (
                  "Wrong number of initial categorical centroids in init "
                  f"({init[1].shape[0]}, should be {n_clusters})."
              )
              assert init[1].shape[1] == ncatattrs, (
                  "Wrong number of categorical attributes in init "
                  f"({init[1].shape[1]}, should be {ncatattrs})."
              )
              centroids = [
                  np.asarray(init[0], dtype=np.float64),
                  np.asarray(init[1], dtype=np.uint16),
              ]
          else:
              raise NotImplementedError(
                  "Initialization method not supported."
              )
  
          if not isinstance(init, list):
              # Numerical is initialized by drawing from normal distribution,
              # categorical following the k-modes methods.
              meanx = np.mean(Xnum, axis=0)
              stdx = np.std(Xnum, axis=0)
              centroids = [
                  meanx
                  + random_state.randn(n_clusters, nnumattrs) * stdx,
                  centroids,
              ]
  
          if verbose:
              print("Init: initializing clusters")
          membship = np.zeros((n_clusters, n_points), dtype=np.bool_)
          # Keep track of the sum of attribute values per cluster so that we
          # can do k-means on the numerical attributes.
          cl_attr_sum = np.zeros(
              (n_clusters, nnumattrs), dtype=np.float64
          )
          # Same for the membership sum per cluster
          cl_memb_sum = np.zeros(n_clusters, dtype=np.float64)
          # cl_attr_freq is a list of lists with dictionaries that contain
          # the frequencies of values per cluster and attribute.
          cl_attr_freq = [
              [defaultdict(float) for _ in range(ncatattrs)]
              for _ in range(n_clusters)
          ]
          for ipoint in range(n_points):
              weight = (
                  sample_weight[ipoint]
                  if sample_weight is not None
                  else 1
              )
              # Initial assignment to clusters
              clust = np.argmin(
                  num_dissim(centroids[0], Xnum[ipoint])
                  + gamma
                  * cat_dissim(
                      centroids[1],
                      Xcat[ipoint],
                      X=Xcat,
                      membship=membship,
                  )
              )
              membship[clust, ipoint] = 1
              cl_memb_sum[clust] += weight
              # Count attribute values per cluster.
              for iattr, curattr in enumerate(Xnum[ipoint]):
                  cl_attr_sum[clust, iattr] += curattr * weight
              for iattr, curattr in enumerate(Xcat[ipoint]):
                  cl_attr_freq[clust][iattr][curattr] += weight
  
          # If no empty clusters, then consider initialization finalized.
          if membship.sum(axis=1).min() > 0:
              break
  
          if init_tries == MAX_INIT_TRIES:
              # Could not get rid of empty clusters. Randomly
              # initialize instead.
              init = "random"
          elif init_tries == RAISE_INIT_TRIES:
              raise ValueError(
                  "Clustering algorithm could not initialize. "
                  "Consider assigning the initial clusters manually."
              )
  
      # Perform an initial centroid update.
      for ik in range(n_clusters):
          for iattr in range(nnumattrs):
              centroids[0][ik, iattr] = (
                  cl_attr_sum[ik, iattr] / cl_memb_sum[ik]
              )
          for iattr in range(ncatattrs):
              centroids[1][ik, iattr] = get_max_value_key(
                  cl_attr_freq[ik][iattr]
              )
  
      # _____ ITERATION _____
      if verbose:
          print("Starting iterations...")
      itr = 0
      labels = None
      converged = False
  
      _, cost = labels_cost(
          Xnum,
          Xcat,
          centroids,
          num_dissim,
          cat_dissim,
          gamma,
          membship,
          sample_weight,
      )
  
      epoch_costs = [cost]
      while itr < max_iter and not converged:
          itr += 1
          (
              centroids,
              cl_attr_sum,
              cl_memb_sum,
              cl_attr_freq,
              membship,
              moves,
          ) = _k_prototypes_iter(
              Xnum,
              Xcat,
              centroids,
              cl_attr_sum,
              cl_memb_sum,
              cl_attr_freq,
              membship,
              num_dissim,
              cat_dissim,
              gamma,
              random_state,
              sample_weight,
          )
  
          # All points seen in this iteration
          labels, ncost = labels_cost(
              Xnum,
              Xcat,
              centroids,
              num_dissim,
              cat_dissim,
              gamma,
              membship,
              sample_weight,
          )
          converged = (moves == 0) or (ncost >= cost)
          epoch_costs.append(ncost)
          cost = ncost
          if verbose:
              print(
                  f"Run: {init_no + 1}, iteration: {itr}/{max_iter}, "
                  f"moves: {moves}, ncost: {ncost}"
              )
  
      return centroids, labels, cost, itr, epoch_costs
  
imports: from collections import defaultdict
  import numpy
  from joblib import Parallel, delayed
  from scipy import sparse
  from sklearn.utils import check_random_state
  from sklearn.utils.validation import check_array
  from .None import kmodes
  from .util import get_max_value_key, encode_features, get_unique_rows, decode_centroids, pandas_to_numpy
  from .util.dissim import matching_dissim, euclidean_dissim
  from .util.init_methods import init_cao, init_huang
label_desc: Use image node_id 3 for calling a global function with example usage: _k_prototypes_single(Xnum, Xcat, nnumattrs, ncatattrs, n_clusters, n_points, max_iter, num_dissim, cat_dissim, gamma, init, init_no, verbose, random_state, sample_weight) and returns: centroids,  labels,  cost,  itr,  epoch_costs