code: def infer_self_graph(
      res,
      emb,
      X,
      y,
      df,
      infer_on_umap_embedding=False,
      eps="auto",
      n_neighbors=7,
      verbose=False,
  ):
      """
      Infer a graph from a graphistry object
  
      args:
          df: outside minibatch dataframe to add to existing graph
          X: minibatch transformed dataframe
          emb: minibatch UMAP embedding distance threshold for a minibatch point to cluster to existing graph
          eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatch point to cluster to existing graph
          sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges.
              This sets the global stickiness of the graph, and is a good way to control the number of edges incuded from the old graph.
          n_neighbors, int: number of nearest neighbors to include per batch point within epsilon.
              This sets the local stickiness of the graph, and is a good way to control the number of edges between
              an added point and the existing graph.
      returns:
          graphistry Plottable object
      """
      # enhanced = is_notebook()
  
      print("-" * 50) if verbose else None
  
      if infer_on_umap_embedding and emb is not None:
          X_previously_fit = emb
          X_new = emb
          print(
              "Infering edges over UMAP embedding"
          ) if verbose else None
      else:  # can still be umap, but want to do the inference on the higher dimensional features
          X_previously_fit = X
          X_new = X
          print(
              "Infering edges over features embedding"
          ) if verbose else None
  
      print("-" * 45) if verbose else None
  
      assert (
          df.shape[0] == X.shape[0]
      ), "minibatches df and X must have same number of rows since f(df) = X"
      if emb is not None:
          assert (
              emb.shape[0] == df.shape[0]
          ), "minibatches emb and X must have same number of rows since h(df) = emb"
          df = df.assign(
              x=emb.x, y=emb.y
          )  # add x and y to df for graphistry instance
      else:  # if umap has been fit, but only transforming over features, need to add x and y or breaks plot binds of res
          df["x"] = np.random.random(df.shape[0])
          df["y"] = np.random.random(df.shape[0])
  
      #  if umap, need to add '_n' as node id to df, adding new indices to existing graph
      numeric_indices = np.arange(
          X_previously_fit.shape[0],
          dtype=np.float64,  # this seems off but works
      )
      df["_n"] = numeric_indices
      df[
          BATCH
      ] = 1  # 1 for minibatch, 0 for existing graph, here should all be `1`
      node = res._node
      if node not in df.columns:
          df[node] = numeric_indices
  
      src = res._source
      dst = res._destination
  
      old_nodes = []
      new_edges = []
      mdists = []
  
      for i in range(X_new.shape[0]):
          diff = X_previously_fit - X_new.iloc[i, :]
          dist = np.linalg.norm(diff, axis=1)  # Euclidean distance
          mdists.append(dist)
  
      m, std = np.mean(mdists), np.std(mdists)
      logger.info(
          f"--Mean distance to existing nodes  {m:.2f} +/- {std:.2f}"
      )
      print(
          f" Mean distance to existing nodes {m:.2f} +/- {std:.2f}"
      ) if verbose else None
      if eps == "auto":
          eps = np.min([np.abs(m - std), m])
      logger.info(
          f" epsilon = {eps:.2f} max distance threshold to be considered a neighbor"
      )
      print(
          f" Max distance threshold; epsilon = {eps:.2f}"
      ) if verbose else None
  
      print(
          f" Finding {n_neighbors} nearest neighbors"
      ) if verbose else None
      nn = []
      for i, dist in enumerate(mdists):
          record_df = df.iloc[i, :]
          nearest = np.where(dist < eps)[0]
          nn.append(len(nearest))
          for j in nearest[
              :n_neighbors
          ]:  # add n_neighbors nearest neighbors, if any, super speedup hack
              if i != j:
                  this_ndf = df.iloc[j, :]
                  weight = min(1 / (dist[j] + 1e-3), 1)
                  new_edges.append(
                      [this_ndf[node], record_df[node], weight, 1]
                  )
                  old_nodes.append(this_ndf)
  
      print(
          f" {np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}"
      ) if verbose else None
  
      new_edges = pd.DataFrame(
          new_edges, columns=[src, dst, WEIGHT, BATCH]
      )
      new_edges = new_edges.drop_duplicates()
      print(
          "", len(new_edges), "total edges after dropping duplicates"
      ) if verbose else None
      print(
          " ** Final graph has", len(df), "nodes"
      ) if verbose else None
      # #########################################################
      print("-" * 50) if verbose else None
      return hydrate_graph(
          res, df, new_edges, node, src, dst, emb, X, y
      )
  
file_path: pygraphistry/graphistry/ai_utils.py
imports: import pandas
  import numpy
  import graphistry
  from .constants import DISTANCE, WEIGHT, BATCH
  from logging import getLogger
label_desc: Use image node_id 9 for calling a global function with example usage: infer_self_graph(res, emb, X, y, df, infer_on_umap_embedding, eps, n_neighbors, verbose) and returns: hydrate_graph