From 2f18c7324651db9bead4be416e2288fb921b699c Mon Sep 17 00:00:00 2001 From: rayair250-droid Date: Fri, 3 Jul 2026 02:09:46 +0200 Subject: [PATCH 1/2] Add ButinaClustering estimator (part of #328) Adds a scikit-learn-style ButinaClustering (BaseEstimator, ClusterMixin) mirroring the existing MaxMinClustering: fit/predict/fit_predict, _parameter_constraints, labels_, centroid_indices_/centroid_bitvectors_/centroids_, and get_clusters_and_points. Uses RDKit Butina.ClusterData on condensed Tanimoto distances. Accepts dense arrays, CSR sparse arrays, or ExplicitBitVect sequences, like MaxMinClustering. Tests mirror the MaxMin clustering tests. --- skfp/clustering/__init__.py | 1 + skfp/clustering/butina.py | 221 ++++++++++++++++++++++++++++++++++++ tests/clustering/butina.py | 118 +++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 skfp/clustering/butina.py create mode 100644 tests/clustering/butina.py diff --git a/skfp/clustering/__init__.py b/skfp/clustering/__init__.py index b1be6367..f7d7d903 100644 --- a/skfp/clustering/__init__.py +++ b/skfp/clustering/__init__.py @@ -1 +1,2 @@ +from .butina import ButinaClustering from .maxmin import MaxMinClustering diff --git a/skfp/clustering/butina.py b/skfp/clustering/butina.py new file mode 100644 index 00000000..4ef6e745 --- /dev/null +++ b/skfp/clustering/butina.py @@ -0,0 +1,221 @@ +from collections.abc import Sequence + +import numpy as np +from rdkit.DataStructs import BulkTanimotoSimilarity +from rdkit.DataStructs.cDataStructs import ExplicitBitVect +from rdkit.ML.Cluster import Butina +from scipy import sparse +from sklearn.base import BaseEstimator, ClusterMixin +from sklearn.utils._param_validation import Interval, RealNotInt +from sklearn.utils.validation import check_is_fitted, validate_data + + +class ButinaClustering(BaseEstimator, ClusterMixin): + """ + Taylor-Butina clustering. + + A density-based clustering algorithm for binary fingerprints using Tanimoto + similarity, also known as sphere exclusion or leader-following clustering [1]_ [2]_. + Cluster centroids are chosen so that no two centroids are closer than a given + Tanimoto distance ``distance_threshold``; every remaining sample joins the cluster + of the first centroid within that radius. In contrast to centroid-based methods + like MaxMin clustering, Butina clusters follow the density of the data and can vary + widely in size. + + Clustering uses RDKit ``Butina.ClusterData`` with ``reordering=True`` for + deterministic output. After fitting, new samples are assigned with :meth:`predict` + to the centroid with the highest Tanimoto similarity. + + Parameters + ---------- + distance_threshold : float, default=0.65 + Tanimoto distance threshold (distance = 1 - Tanimoto similarity), the minimal + distance between cluster centroids. Must be between 0 and 1. The default follows + the ECFP4 activity threshold used by the Butina train/test splitter [3]_. + + Attributes + ---------- + centroid_indices_ : list of int + Indices of samples chosen as cluster centroids after :meth:`fit`. + + centroid_bitvectors_ : list of ExplicitBitVect + Centroid fingerprints as RDKit ExplicitBitVect objects. + + centroids_ : ndarray of uint8, shape (n_centroids, n_bits) + Centroids as a boolean NumPy array when the input was a dense array or + sparse matrix. + + labels_ : ndarray of int, shape (n_samples,) + Cluster labels for each sample. + + Notes + ----- + This estimator follows the scikit-learn estimator API and accepts dense NumPy + arrays, SciPy CSR sparse arrays, or lists/tuples of RDKit ``ExplicitBitVect`` + objects as input. + + References + ---------- + .. [1] `Darko Butina + "Unsupervised Data Base Clustering Based on Daylight's Fingerprint and Tanimoto + Similarity: A Fast and Automated Way To Cluster Small and Large Data Sets" + J. Chem. Inf. Comput. Sci., 1999, 39, 4, 747-750 + `_ + + .. [2] `Robin Taylor + "Simulation Analysis of Experimental Design Strategies for Screening Random + Compounds as Potential New Drugs and Agrochemicals" + J. Chem. Inf. Comput. Sci., 1995, 35, 1, 59-67 + `_ + + .. [3] `Roger A. Sayle + "2D similarity, diversity and clustering in RDKit" + RDKit User Group Meeting 2019 + `_ + """ + + _parameter_constraints: dict = { + "distance_threshold": [Interval(RealNotInt, 0, 1, closed="both")], + } + + def __init__(self, distance_threshold: float = 0.65): + self.distance_threshold = distance_threshold + + def fit(self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=None): # noqa: ARG002 + """ + Fit the Butina clustering model. + + Parameters + ---------- + X : {array-like, sparse matrix, sequence of ExplicitBitVect} + Binary fingerprint data of shape ``(n_samples, n_bits)``, or a + list/tuple of RDKit ``ExplicitBitVect`` objects. + + y : ignored + Not used, present for API consistency with scikit-learn. + + Returns + ------- + self : ButinaClustering + Fitted estimator. + """ + super()._validate_params() + X = validate_data(self, X, accept_sparse=["csr"], ensure_2d=False) + + fps = self._array_to_bitvectors(X) + n_samples = len(fps) + + # Condensed lower-triangle Tanimoto distances, the format expected by + # Butina.ClusterData(isDistData=True): dist(i, j) for i > j, row-major. + condensed_distances: list[float] = [] + for i in range(1, n_samples): + sims = BulkTanimotoSimilarity(fps[i], fps[:i]) + condensed_distances.extend(1.0 - sim for sim in sims) + + clusters = Butina.ClusterData( + condensed_distances, + n_samples, + self.distance_threshold, + isDistData=True, + reordering=True, + ) + + # In each RDKit Butina cluster the first element is the centroid. + self.centroid_indices_ = [cluster[0] for cluster in clusters] + self.centroid_bitvectors_ = [fps[i] for i in self.centroid_indices_] + + if sparse.issparse(X) or isinstance(X, np.ndarray): + arr = X.todense() if sparse.issparse(X) else X + self.centroids_ = np.asarray(arr)[self.centroid_indices_].astype(np.uint8) + + labels = np.empty(n_samples, dtype=int) + for cluster_id, cluster in enumerate(clusters): + for sample_idx in cluster: + labels[sample_idx] = cluster_id + self.labels_ = labels + + return self + + def predict( + self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect] + ) -> np.ndarray: + """ + Assign new samples to existing centroids. + + Parameters + ---------- + X : {array-like, sparse matrix, sequence of ExplicitBitVect} + New samples to assign. The input formats match those accepted by :meth:`fit`. + + Returns + ------- + labels : ndarray of int, shape (n_samples,) + Cluster labels for the input samples, assigned to the nearest centroid by + Tanimoto similarity. + """ + check_is_fitted(self) + X = validate_data(self, X, accept_sparse=["csr"], ensure_2d=False) + bitvecs = self._array_to_bitvectors(X) + return self._assign_labels(bitvecs) + + def fit_predict( + self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=None + ) -> np.ndarray: + """ + Fit the Butina clustering model and return cluster labels. + + This is a convenience method that calls :meth:`fit` and returns ``labels_``. + """ + self.fit(X, y) + return self.labels_ + + def get_clusters_and_points(self) -> dict[int, np.ndarray]: + """ + Return a mapping from cluster id to the indices of its member samples. + """ + check_is_fitted(self) + return { + k: np.where(self.labels_ == k)[0] + for k in range(len(self.centroid_indices_)) + } + + def _array_to_bitvectors( + self, X: np.ndarray | sparse.csr_array + ) -> list[ExplicitBitVect]: + """ + Convert input data to a list of RDKit ExplicitBitVect objects. + """ + if np.ndim(X) == 1 and len(X) > 0 and isinstance(X[0], ExplicitBitVect): + return list(X) + + bitvecs: list[ExplicitBitVect] = [] + + if sparse.issparse(X): + X = X.tocsr() + n_samples, n_bits = X.shape + for i in range(n_samples): + vec = ExplicitBitVect(n_bits) + row_start, row_end = X.indptr[i], X.indptr[i + 1] + for bit in X.indices[row_start:row_end]: + vec.SetBit(int(bit)) + bitvecs.append(vec) + return bitvecs + + n_samples, n_bits = X.shape + for i in range(n_samples): + vec = ExplicitBitVect(n_bits) + for bit in np.flatnonzero(X[i]): + vec.SetBit(int(bit)) + bitvecs.append(vec) + + return bitvecs + + def _assign_labels(self, vectors: list[ExplicitBitVect]) -> np.ndarray: + """ + Assign each sample to the nearest centroid by Tanimoto similarity. + """ + labels = np.empty(len(vectors), dtype=int) + for i, fp in enumerate(vectors): + sims = BulkTanimotoSimilarity(fp, self.centroid_bitvectors_) + labels[i] = int(np.argmax(sims)) + return labels diff --git a/tests/clustering/butina.py b/tests/clustering/butina.py new file mode 100644 index 00000000..f70f5503 --- /dev/null +++ b/tests/clustering/butina.py @@ -0,0 +1,118 @@ +import re + +import numpy as np +import pytest +from scipy.sparse import csr_matrix + +from skfp.clustering import ButinaClustering + + +@pytest.fixture(params=["dense", "sparse"]) +def binary_X(request): + X = np.array( + [ + [1, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 0, 0], + ], + dtype=np.uint8, + ) + + if request.param == "sparse": + return csr_matrix(X) + + return X + + +@pytest.fixture( + params=[ + {}, # default parameters + {"distance_threshold": 0.7}, + ] +) +def butina_clusterer(request): + return ButinaClustering(**request.param) + + +def test_fit_clustering_attributes(butina_clusterer, binary_X): + clusterer = butina_clusterer.fit(binary_X) + n_samples = binary_X.shape[0] + + assert hasattr(clusterer, "centroid_indices_") + assert isinstance(clusterer.centroid_indices_, list) + assert len(clusterer.centroid_indices_) > 0 + + assert hasattr(clusterer, "centroid_bitvectors_") + assert hasattr(clusterer, "centroids_") + + assert hasattr(clusterer, "labels_") + assert len(clusterer.labels_) == n_samples + + +def test_labels_within_cluster_range(butina_clusterer, binary_X): + clusterer = butina_clusterer.fit(binary_X) + n_clusters = len(clusterer.centroid_indices_) + assert clusterer.labels_.min() >= 0 + assert clusterer.labels_.max() < n_clusters + # every cluster id is used + assert set(clusterer.labels_.tolist()) == set(range(n_clusters)) + + +def test_deterministic(binary_X): + c1 = ButinaClustering(distance_threshold=0.5) + c2 = ButinaClustering(distance_threshold=0.5) + assert np.array_equal(c1.fit_predict(binary_X), c2.fit_predict(binary_X)) + + +def test_sparse_matches_dense(binary_X): + dense = np.asarray(binary_X.todense()) if hasattr(binary_X, "todense") else binary_X + labels_dense = ButinaClustering(distance_threshold=0.5).fit(dense).labels_ + labels_sparse = ( + ButinaClustering(distance_threshold=0.5).fit(csr_matrix(dense)).labels_ + ) + assert np.array_equal(labels_dense, labels_sparse) + + +def test_predict_assigns_to_nearest_centroid(binary_X): + from rdkit.DataStructs import BulkTanimotoSimilarity + + clusterer = ButinaClustering(distance_threshold=0.5).fit(binary_X) + bitvects = clusterer._array_to_bitvectors(binary_X) + preds = clusterer.predict(binary_X) + for i, fp in enumerate(bitvects): + sims = BulkTanimotoSimilarity(fp, clusterer.centroid_bitvectors_) + assert preds[i] == int(np.argmax(sims)) + + +def test_get_clusters_and_points(binary_X): + clusterer = ButinaClustering(distance_threshold=0.5).fit(binary_X) + clusters = clusterer.get_clusters_and_points() + for k, idx in clusters.items(): + assert np.all(clusterer.labels_[idx] == k) + + +def test_empty_input_raises(): + clusterer = ButinaClustering() + with pytest.raises( + ValueError, + match=re.escape( + "Found array with 0 sample(s) (shape=(0, 8)) while a minimum of 1 is " + "required by ButinaClustering." + ), + ): + clusterer.fit(np.empty((0, 8))) + + +def test_predict_before_fit_raises(binary_X): + clusterer = ButinaClustering() + with pytest.raises( + ValueError, + match=re.escape( + "This ButinaClustering instance is not fitted yet. " + "Call 'fit' with appropriate arguments before using this estimator." + ), + ): + clusterer.predict(binary_X) From ed468b2b334df60111151ca0f4a9850cd5eaac01 Mon Sep 17 00:00:00 2001 From: rayair250-droid Date: Fri, 3 Jul 2026 15:23:41 +0200 Subject: [PATCH 2/2] Address review: dedup clustering helpers into utils, trim docstring Move array_to_bitvectors/assign_labels/clusters_and_points into skfp/clustering/utils.py and use them from both ButinaClustering and MaxMinClustering (removes the duplicated private methods). Drop the leader-following naming and the extra technical paragraph from the Butina docstring. get_clusters_and_points now derives from labels_ directly. Global imports in tests. --- skfp/clustering/butina.py | 70 +++++++------------------------------- skfp/clustering/maxmin.py | 66 ++++------------------------------- skfp/clustering/utils.py | 47 +++++++++++++++++++++++++ tests/clustering/butina.py | 6 ++-- tests/clustering/maxmin.py | 3 +- 5 files changed, 71 insertions(+), 121 deletions(-) create mode 100644 skfp/clustering/utils.py diff --git a/skfp/clustering/butina.py b/skfp/clustering/butina.py index 4ef6e745..5c046a0c 100644 --- a/skfp/clustering/butina.py +++ b/skfp/clustering/butina.py @@ -9,22 +9,20 @@ from sklearn.utils._param_validation import Interval, RealNotInt from sklearn.utils.validation import check_is_fitted, validate_data +from skfp.clustering import utils + class ButinaClustering(BaseEstimator, ClusterMixin): """ Taylor-Butina clustering. A density-based clustering algorithm for binary fingerprints using Tanimoto - similarity, also known as sphere exclusion or leader-following clustering [1]_ [2]_. - Cluster centroids are chosen so that no two centroids are closer than a given - Tanimoto distance ``distance_threshold``; every remaining sample joins the cluster - of the first centroid within that radius. In contrast to centroid-based methods - like MaxMin clustering, Butina clusters follow the density of the data and can vary - widely in size. - - Clustering uses RDKit ``Butina.ClusterData`` with ``reordering=True`` for - deterministic output. After fitting, new samples are assigned with :meth:`predict` - to the centroid with the highest Tanimoto similarity. + similarity, also known as sphere exclusion clustering [1]_ [2]_. Cluster centroids + are chosen so that no two centroids are closer than a given Tanimoto distance + ``distance_threshold``; every remaining sample joins the cluster of the first + centroid within that radius. In contrast to centroid-based methods like MaxMin + clustering, Butina clusters follow the density of the data and can vary widely + in size. Parameters ---------- @@ -102,7 +100,7 @@ def fit(self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=No super()._validate_params() X = validate_data(self, X, accept_sparse=["csr"], ensure_2d=False) - fps = self._array_to_bitvectors(X) + fps = utils.array_to_bitvectors(X) n_samples = len(fps) # Condensed lower-triangle Tanimoto distances, the format expected by @@ -155,8 +153,8 @@ def predict( """ check_is_fitted(self) X = validate_data(self, X, accept_sparse=["csr"], ensure_2d=False) - bitvecs = self._array_to_bitvectors(X) - return self._assign_labels(bitvecs) + bitvecs = utils.array_to_bitvectors(X) + return utils.assign_labels(bitvecs, self.centroid_bitvectors_) def fit_predict( self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=None @@ -174,48 +172,4 @@ def get_clusters_and_points(self) -> dict[int, np.ndarray]: Return a mapping from cluster id to the indices of its member samples. """ check_is_fitted(self) - return { - k: np.where(self.labels_ == k)[0] - for k in range(len(self.centroid_indices_)) - } - - def _array_to_bitvectors( - self, X: np.ndarray | sparse.csr_array - ) -> list[ExplicitBitVect]: - """ - Convert input data to a list of RDKit ExplicitBitVect objects. - """ - if np.ndim(X) == 1 and len(X) > 0 and isinstance(X[0], ExplicitBitVect): - return list(X) - - bitvecs: list[ExplicitBitVect] = [] - - if sparse.issparse(X): - X = X.tocsr() - n_samples, n_bits = X.shape - for i in range(n_samples): - vec = ExplicitBitVect(n_bits) - row_start, row_end = X.indptr[i], X.indptr[i + 1] - for bit in X.indices[row_start:row_end]: - vec.SetBit(int(bit)) - bitvecs.append(vec) - return bitvecs - - n_samples, n_bits = X.shape - for i in range(n_samples): - vec = ExplicitBitVect(n_bits) - for bit in np.flatnonzero(X[i]): - vec.SetBit(int(bit)) - bitvecs.append(vec) - - return bitvecs - - def _assign_labels(self, vectors: list[ExplicitBitVect]) -> np.ndarray: - """ - Assign each sample to the nearest centroid by Tanimoto similarity. - """ - labels = np.empty(len(vectors), dtype=int) - for i, fp in enumerate(vectors): - sims = BulkTanimotoSimilarity(fp, self.centroid_bitvectors_) - labels[i] = int(np.argmax(sims)) - return labels + return utils.clusters_and_points(self.labels_) diff --git a/skfp/clustering/maxmin.py b/skfp/clustering/maxmin.py index fcf5e9c4..17fd96f9 100644 --- a/skfp/clustering/maxmin.py +++ b/skfp/clustering/maxmin.py @@ -1,7 +1,6 @@ from collections.abc import Sequence import numpy as np -from rdkit.DataStructs import BulkTanimotoSimilarity from rdkit.DataStructs.cDataStructs import ExplicitBitVect from rdkit.SimDivFilters import MaxMinPicker from scipy import sparse @@ -10,6 +9,8 @@ from sklearn.utils._param_validation import Interval, RealNotInt from sklearn.utils.validation import check_is_fitted, validate_data +from skfp.clustering import utils + class MaxMinClustering(BaseEstimator, ClusterMixin): """ @@ -114,7 +115,7 @@ def fit(self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=No # centroid selection (MaxMin) picker = MaxMinPicker() - fps = self._array_to_bitvectors(X) + fps = utils.array_to_bitvectors(X) rng = check_random_state(self.random_state) seed = rng.randint(0, 2**31 - 1) centroid_indices, _ = picker.LazyBitVectorPickWithThreshold( @@ -135,7 +136,7 @@ def fit(self, X: np.ndarray | sparse.csr_array | Sequence[ExplicitBitVect], y=No self.centroids_ = arr[self.centroid_indices_].astype(np.uint8) # cluster assignment - self.labels_ = self._assign_labels(fps) + self.labels_ = utils.assign_labels(fps, self.centroid_bitvectors_) # enforce invariant: each centroid labels itself for cluster_id, sample_idx in enumerate(self.centroid_indices_): @@ -163,8 +164,8 @@ def predict( check_is_fitted(self) X = validate_data(self, X, accept_sparse=["csr"], ensure_2d=False) - bitvecs = self._array_to_bitvectors(X) - return self._assign_labels(bitvecs) + bitvecs = utils.array_to_bitvectors(X) + return utils.assign_labels(bitvecs, self.centroid_bitvectors_) def fit_predict( self, @@ -206,57 +207,4 @@ def get_clusters_and_points(self) -> dict[int, np.ndarray]: indices of samples belonging to that cluster. """ check_is_fitted(self) - return { - k: np.where(self.labels_ == k)[0] - for k in range(len(self.centroid_indices_)) - } - - def _array_to_bitvectors( - self, X: np.ndarray | sparse.csr_array - ) -> list[ExplicitBitVect]: - """ - Convert input data to a list of RDKit ExplicitBitVect objects. - """ - bitvecs: list[ExplicitBitVect] = [] - if np.ndim(X) == 1 and len(X) > 0 and isinstance(X[0], ExplicitBitVect): - return list(X) - - if sparse.issparse(X): - X = X.tocsr() - n_samples, n_bits = X.shape - - for i in range(n_samples): - vec = ExplicitBitVect(n_bits) - row_start = X.indptr[i] - row_end = X.indptr[i + 1] - - for bit in X.indices[row_start:row_end]: - # RDKit ExplicitBitVect uses int indices, not Numpy integers - vec.SetBit(int(bit)) - - bitvecs.append(vec) - - return bitvecs - - n_samples, n_bits = X.shape - - for i in range(n_samples): - vec = ExplicitBitVect(n_bits) - for bit in np.flatnonzero(X[i]): - vec.SetBit(int(bit)) - bitvecs.append(vec) - - return bitvecs - - def _assign_labels(self, vectors: list[ExplicitBitVect]) -> np.ndarray: - """ - Assign each sample to the nearest centroid by Tanimoto similarity. - """ - n_samples = len(vectors) - labels = np.empty(n_samples, dtype=int) - - for i, fp in enumerate(vectors): - sims = BulkTanimotoSimilarity(fp, self.centroid_bitvectors_) - labels[i] = np.argmax(sims) - - return labels + return utils.clusters_and_points(self.labels_) diff --git a/skfp/clustering/utils.py b/skfp/clustering/utils.py new file mode 100644 index 00000000..074aa648 --- /dev/null +++ b/skfp/clustering/utils.py @@ -0,0 +1,47 @@ +import numpy as np +from rdkit.DataStructs import BulkTanimotoSimilarity +from rdkit.DataStructs.cDataStructs import ExplicitBitVect +from scipy import sparse + + +def array_to_bitvectors(X) -> list[ExplicitBitVect]: + """Convert input data to a list of RDKit ExplicitBitVect objects.""" + if np.ndim(X) == 1 and len(X) > 0 and isinstance(X[0], ExplicitBitVect): + return list(X) + + bitvecs: list[ExplicitBitVect] = [] + + if sparse.issparse(X): + X = X.tocsr() + n_samples, n_bits = X.shape + for i in range(n_samples): + vec = ExplicitBitVect(n_bits) + for bit in X.indices[X.indptr[i] : X.indptr[i + 1]]: + vec.SetBit(int(bit)) + bitvecs.append(vec) + return bitvecs + + n_samples, n_bits = X.shape + for i in range(n_samples): + vec = ExplicitBitVect(n_bits) + for bit in np.flatnonzero(X[i]): + vec.SetBit(int(bit)) + bitvecs.append(vec) + + return bitvecs + + +def assign_labels( + vectors: list[ExplicitBitVect], centroid_bitvectors: list[ExplicitBitVect] +) -> np.ndarray: + """Assign each sample to the nearest centroid by Tanimoto similarity.""" + labels = np.empty(len(vectors), dtype=int) + for i, fp in enumerate(vectors): + sims = BulkTanimotoSimilarity(fp, centroid_bitvectors) + labels[i] = int(np.argmax(sims)) + return labels + + +def clusters_and_points(labels: np.ndarray) -> dict[int, np.ndarray]: + """Map each cluster ID to the indices of its member samples.""" + return {int(k): np.where(labels == k)[0] for k in np.unique(labels)} diff --git a/tests/clustering/butina.py b/tests/clustering/butina.py index f70f5503..c024fa20 100644 --- a/tests/clustering/butina.py +++ b/tests/clustering/butina.py @@ -2,9 +2,11 @@ import numpy as np import pytest +from rdkit.DataStructs import BulkTanimotoSimilarity from scipy.sparse import csr_matrix from skfp.clustering import ButinaClustering +from skfp.clustering.utils import array_to_bitvectors @pytest.fixture(params=["dense", "sparse"]) @@ -77,10 +79,8 @@ def test_sparse_matches_dense(binary_X): def test_predict_assigns_to_nearest_centroid(binary_X): - from rdkit.DataStructs import BulkTanimotoSimilarity - clusterer = ButinaClustering(distance_threshold=0.5).fit(binary_X) - bitvects = clusterer._array_to_bitvectors(binary_X) + bitvects = array_to_bitvectors(binary_X) preds = clusterer.predict(binary_X) for i, fp in enumerate(bitvects): sims = BulkTanimotoSimilarity(fp, clusterer.centroid_bitvectors_) diff --git a/tests/clustering/maxmin.py b/tests/clustering/maxmin.py index ce1d099d..ec81fb30 100644 --- a/tests/clustering/maxmin.py +++ b/tests/clustering/maxmin.py @@ -7,6 +7,7 @@ from scipy.sparse import csr_matrix from skfp.clustering import MaxMinClustering +from skfp.clustering.utils import array_to_bitvectors @pytest.fixture(params=["dense", "sparse"]) @@ -60,7 +61,7 @@ def test_assignment_is_nearest_centroid(binary_X, maxmin_clusterer): clusterer = maxmin_clusterer clusterer.fit(binary_X) - bitvects = clusterer._array_to_bitvectors(binary_X) + bitvects = array_to_bitvectors(binary_X) centroids = clusterer.centroid_bitvectors_ labels = clusterer.labels_