Apply Mapper algorithm on this projection and build a simplicial complex. Returns a dictionary with nodes and links.

        Parameters
        ----------
        lens: Numpy Array
            Lower dimensional representation of data. In general will be output of `fit_transform`.

        X: Numpy Array
            Original data or data to run clustering on. If `None`, then use `lens` as default. X can be a SciPy sparse matrix.

        clusterer: Default: DBSCAN
            Scikit-learn API compatible clustering algorithm. Must provide `fit` and `predict`.

        cover: kmapper.Cover
            Cover scheme for lens. Instance of kmapper.cover providing methods `fit` and `transform`.

        nerve: kmapper.Nerve
            Nerve builder implementing `__call__(nodes)` API

        precomputed : Boolean
            Tell Mapper whether the data that you are clustering on is a precomputed distance matrix. If set to
            `True`, the assumption is that you are also telling your `clusterer` that `metric='precomputed'` (which
            is an argument for DBSCAN among others), which 
            will then cause the clusterer to expect a square distance matrix for each hypercube. `precomputed=True` will give a square matrix
            to the clusterer to fit on for each hypercube.
            
        remove_duplicate_nodes: Boolean
            Removes duplicate nodes before edges are determined. A node is considered to be duplicate
            if it has exactly the same set of points as another node.

        nr_cubes: Int
            
            .. deprecated:: 1.1.6

                define Cover explicitly in future versions

            The number of intervals/hypercubes to create. Default = 10.
            
        overlap_perc: Float
            .. deprecated:: 1.1.6

                define Cover explicitly in future versions    

            The percentage of overlap "between" the intervals/hypercubes. Default = 0.1. 
            


        Returns
        =======
        simplicial_complex : dict
            A dictionary with "nodes", "links" and "meta" information.

        Examples
        ========

        >>> # Default mapping.
        >>> graph = mapper.map(X_projected, X_inverse)

        >>> # Apply clustering on the projection instead of on inverse X
        >>> graph = mapper.map(X_projected)

        >>> # Use 20 cubes/intervals per projection dimension, with a 50% overlap
        >>> graph = mapper.map(X_projected, X_inverse, 
        >>>                    cover=kmapper.Cover(n_cubes=20, perc_overlap=0.5))

        >>> # Use multiple different cubes/intervals per projection dimension, 
        >>> # And vary the overlap
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>                    cover=km.Cover(n_cubes=[10,20,5],
        >>>                                         perc_overlap=[0.1,0.2,0.5]))

        >>> # Use KMeans with 2 clusters
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=sklearn.cluster.KMeans(2))

        >>> # Use DBSCAN with "cosine"-distance
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=sklearn.cluster.DBSCAN(metric="cosine"))

        >>> # Use HDBSCAN as the clusterer
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=hdbscan.HDBSCAN())

        >>> # Parametrize the nerve of the covering
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     nerve=km.GraphNerve(min_intersection=3))

def map(
        self,
        lens,
        X=None,
        clusterer=cluster.DBSCAN(eps=0.5, min_samples=3),
        cover=Cover(n_cubes=10, perc_overlap=0.1),
        nerve=GraphNerve(),
        precomputed=False,
        remove_duplicate_nodes=False,
        # These arguments are all deprecated
        overlap_perc=None,
        nr_cubes=None
    ):
        """Apply Mapper algorithm on this projection and build a simplicial complex. Returns a dictionary with nodes and links.

        Parameters
        ----------
        lens: Numpy Array
            Lower dimensional representation of data. In general will be output of `fit_transform`.

        X: Numpy Array
            Original data or data to run clustering on. If `None`, then use `lens` as default. X can be a SciPy sparse matrix.

        clusterer: Default: DBSCAN
            Scikit-learn API compatible clustering algorithm. Must provide `fit` and `predict`.

        cover: kmapper.Cover
            Cover scheme for lens. Instance of kmapper.cover providing methods `fit` and `transform`.

        nerve: kmapper.Nerve
            Nerve builder implementing `__call__(nodes)` API

        precomputed : Boolean
            Tell Mapper whether the data that you are clustering on is a precomputed distance matrix. If set to
            `True`, the assumption is that you are also telling your `clusterer` that `metric='precomputed'` (which
            is an argument for DBSCAN among others), which 
            will then cause the clusterer to expect a square distance matrix for each hypercube. `precomputed=True` will give a square matrix
            to the clusterer to fit on for each hypercube.
            
        remove_duplicate_nodes: Boolean
            Removes duplicate nodes before edges are determined. A node is considered to be duplicate
            if it has exactly the same set of points as another node.

        nr_cubes: Int
            
            .. deprecated:: 1.1.6

                define Cover explicitly in future versions

            The number of intervals/hypercubes to create. Default = 10.
            
        overlap_perc: Float
            .. deprecated:: 1.1.6

                define Cover explicitly in future versions    

            The percentage of overlap "between" the intervals/hypercubes. Default = 0.1. 
            


        Returns
        =======
        simplicial_complex : dict
            A dictionary with "nodes", "links" and "meta" information.

        Examples
        ========

        >>> # Default mapping.
        >>> graph = mapper.map(X_projected, X_inverse)

        >>> # Apply clustering on the projection instead of on inverse X
        >>> graph = mapper.map(X_projected)

        >>> # Use 20 cubes/intervals per projection dimension, with a 50% overlap
        >>> graph = mapper.map(X_projected, X_inverse, 
        >>>                    cover=kmapper.Cover(n_cubes=20, perc_overlap=0.5))

        >>> # Use multiple different cubes/intervals per projection dimension, 
        >>> # And vary the overlap
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>                    cover=km.Cover(n_cubes=[10,20,5],
        >>>                                         perc_overlap=[0.1,0.2,0.5]))

        >>> # Use KMeans with 2 clusters
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=sklearn.cluster.KMeans(2))

        >>> # Use DBSCAN with "cosine"-distance
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=sklearn.cluster.DBSCAN(metric="cosine"))

        >>> # Use HDBSCAN as the clusterer
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     clusterer=hdbscan.HDBSCAN())

        >>> # Parametrize the nerve of the covering
        >>> graph = mapper.map(X_projected, X_inverse,
        >>>     nerve=km.GraphNerve(min_intersection=3))


        """

        start = datetime.now()

        nodes = defaultdict(list)
        meta = defaultdict(list)
        graph = {}

        # If inverse image is not provided, we use the projection as the inverse image (suffer projection loss)
        if X is None:
            X = lens

        # Deprecation warnings
        if nr_cubes is not None or overlap_perc is not None:
            warnings.warn(
                "Deprecation Warning: Please supply km.Cover object. Explicitly passing in n_cubes/nr_cubes and overlap_perc will be deprecated in future releases. ",
                DeprecationWarning,
            )


        # If user supplied nr_cubes, overlap_perc, or coverer, opt for those
        # TODO: remove this conditional after release in 1.2
        if nr_cubes is not None or overlap_perc is not None:
            n_cubes = nr_cubes if nr_cubes else 10
            overlap_perc = overlap_perc if overlap_perc else 0.1
            self.cover = Cover(n_cubes=n_cubes, perc_overlap=overlap_perc)
        else:
            self.cover = cover

        if self.verbose > 0:
            print(
                "Mapping on data shaped %s using lens shaped %s\n"
                % (str(X.shape), str(lens.shape))
            )

        # Prefix'ing the data with an ID column
        ids = np.array([x for x in range(lens.shape[0])])
        lens = np.c_[ids, lens]
        if issparse(X):
            X = hstack([ids[np.newaxis].T, X], format='csr')
        else:
            X = np.c_[ids, X]

        # Cover scheme defines a list of elements
        bins = self.cover.fit(lens)

        # Algo's like K-Means, have a set number of clusters. We need this number
        # to adjust for the minimal number of samples inside an interval before
        # we consider clustering or skipping it.
        cluster_params = clusterer.get_params()

        min_cluster_samples = cluster_params.get(
            "n_clusters",
            cluster_params.get(
                "min_cluster_size", cluster_params.get("min_samples", 1)
            ),
        )

        if self.verbose > 1:
            print(
                "Minimal points in hypercube before clustering: %d"
                % (min_cluster_samples)
            )

        # Subdivide the projected data X in intervals/hypercubes with overlap
        if self.verbose > 0:
            bins = list(bins)  # extract list from generator
            total_bins = len(bins)
            print("Creating %s hypercubes." % total_bins)

        for i, hypercube in enumerate(self.cover.transform(lens)):

            # If at least min_cluster_samples samples inside the hypercube
            if hypercube.shape[0] >= min_cluster_samples:
                # Cluster the data point(s) in the cube, skipping the id-column
                # Note that we apply clustering on the inverse image (original data samples) that fall inside the cube.
                ids = [int(nn) for nn in hypercube[:, 0]]
                X_cube = X[ids]

                fit_data = X_cube[:, 1:]
                if precomputed:
                    fit_data = fit_data[:, ids]

                cluster_predictions = clusterer.fit_predict(fit_data)

                if self.verbose > 1:
                    print(
                        "   > Found %s clusters in hypercube %s."
                        % (
                            np.unique(
                                cluster_predictions[cluster_predictions > -1]
                            ).shape[0], i
                        )
                    )
        
                for pred in np.unique(cluster_predictions):
                    # if not predicted as noise                       
                    if pred != -1 and not np.isnan(pred):  
                        cluster_id = "cube{}_cluster{}".format(i, int(pred))
                        
                        nodes[cluster_id] = hypercube[:, 0][cluster_predictions == pred].astype(int).tolist()
            elif self.verbose > 1:
                print("Cube_%s is empty.\n" % (i))

        if remove_duplicate_nodes:
            nodes = self._remove_duplicate_nodes(nodes)

        links, simplices = nerve.compute(nodes)

        graph["nodes"] = nodes
        graph["links"] = links
        graph["simplices"] = simplices
        graph["meta_data"] = {
            "projection": self.projection if self.projection else "custom",
            "n_cubes": self.cover.n_cubes,
            "perc_overlap": self.cover.perc_overlap,
            "clusterer": str(clusterer),
            "scaler": str(self.scaler),
        }
        graph["meta_nodes"] = meta

        if self.verbose > 0:
            self._summary(graph, str(datetime.now() - start))

        return graph