docs: def zip_cluster(data, k, init=None, max_iters=100):
      """
      Performs hard EM clustering using the zero-inflated Poisson distribution.
  
      Args:
          data (array): A 2d array- genes x cells
          k (int): Number of clusters
          init (array, optional): Initial centers - genes x k array. Default: None, use kmeans++
          max_iters (int, optional): Maximum number of iterations. Default: 100
  
      Returns:
          assignments (array): integer assignments of cells to clusters (length cells)
          L (array): Poisson parameter (genes x k)
          M (array): zero-inflation parameter (genes x k)
      """
      genes, cells = data.shape
      init, new_assignments = kmeans_pp(data+eps, k, centers=init)
      centers = np.copy(init)
      M = np.zeros(centers.shape)
      assignments = new_assignments
      for c in range(k):
          centers[:,c], M[:,c] = zip_fit_params_mle(data[:, assignments==c])
      for it in range(max_iters):
          lls = zip_ll(data, centers, M)
          new_assignments = np.argmax(lls, 1)
          if np.equal(assignments, new_assignments).all():
              return assignments, centers, M
          for c in range(k):
              centers[:,c], M[:,c] = zip_fit_params_mle(data[:, assignments==c])
          assignments = new_assignments
      return assignments, centers, M