Neighbors¶
Tanimoto/Jaccard similarity search and k-NN estimators for fingerprints.
Warning
Euclidean distance is the wrong metric for sparse binary
fingerprints. Two molecules that share no substructures are “close” in
Euclidean terms because they agree on the thousands of bits that are
jointly zero — bits that carry no chemical information. Every distance
here is Tanimoto/Jaccard, verified against RDKit’s
BulkTanimotoSimilarity.
Distances¶
>>> from qsarkit.neighbors import is_binary, jaccard_distance, tanimoto_similarity_matrix
>>> X = demo_fingerprints(256)
>>> round(float(jaccard_distance(X[0], X[1])), 3)
0.45
>>> is_binary(X)
True
>>> similarity = tanimoto_similarity_matrix(X)
>>> similarity.shape, round(float(similarity[0, 1]), 3)
((24, 24), 0.55)
Similarity search¶
>>> from qsarkit.neighbors import JaccardNeighborSearch
>>> search = JaccardNeighborSearch(n_neighbors=3).fit(X)
>>> distances, indices = search.kneighbors(X[:1])
>>> indices.shape
(1, 3)
>>> int(indices[0, 0]) # a molecule is its own neighbour
0
>>> distances.round(3).tolist()
[[0.0, 0.421, 0.45]]
The distances are fixed, but the third index is not: three molecules in this set sit at exactly 0.45, and which of them fills the last slot is whatever the underlying partition happens to return. Ranking ties are ordinary in fingerprint space, where similarity takes few distinct values, so treat the membership of a k-nearest list as one of several equally valid answers rather than the answer.
A threshold search returns everything similar enough, rather than a fixed count — which is what a chemist actually wants when asking “what else looks like this”:
>>> hits = search.similarity_search(X[:1], threshold=0.5)
>>> [(i, round(s, 2)) for i, s in hits[0]]
[(0, 1.0), (3, 0.58), (1, 0.55), (2, 0.55), (5, 0.55)]
k-NN estimators¶
>>> from qsarkit.neighbors import JaccardKNeighborsRegressor
>>> model = JaccardKNeighborsRegressor(n_neighbors=1).fit(X, DEMO_Y)
>>> round(float(model.predict(X[:1])[0]), 2) # its own label, exactly
5.1
With more neighbours the prediction averages their labels, and because of the ties above the exact average depends on which tied molecule is drawn in. What the method guarantees is the bound, not the value:
>>> averaged = JaccardKNeighborsRegressor(n_neighbors=3).fit(X, DEMO_Y)
>>> prediction = float(averaged.predict(X[:1])[0])
>>> bool(DEMO_Y.min() <= prediction <= DEMO_Y.max())
True
weights="similarity" weights neighbours by Tanimoto similarity rather
than inverse distance, which is the natural reading for fingerprints:
>>> weighted = JaccardKNeighborsRegressor(
... n_neighbors=3, weights="similarity").fit(X, DEMO_Y)
>>> weighted.predict(X[:1]).shape
(1,)
A k-NN model on Tanimoto distance is worth fitting even when you intend to use something else: it is the direct expression of the similar property principle, so it is the baseline any more complex model has to beat to justify itself.
API¶
Fingerprint similarity search and k-NN estimators under Jaccard/Tanimoto distance.
- qsarkit.neighbors.jaccard_distance(u, v)[source]¶
Jaccard (1 - Tanimoto) distance between two fingerprint vectors.
This is the callable to hand to scikit-learn estimators that accept
metric=<callable>.- Parameters:
u (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Fingerprint vectors.v (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Fingerprint vectors.
- Returns:
Distance in [0, 1].
- Return type:
Examples
>>> round(jaccard_distance([1, 1, 0, 0], [1, 0, 0, 0]), 4) 0.5
- qsarkit.neighbors.jaccard_similarity(u, v)[source]¶
Tanimoto/Jaccard similarity between two fingerprint vectors.
Uses the binary expression when both vectors are binary and the generalized MinMax expression otherwise.
- Parameters:
u (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Fingerprint vectors.v (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Fingerprint vectors.
- Returns:
Similarity in [0, 1]. Two all-zero vectors are defined to have similarity 1.0 (they are identical), following the convention used by RDKit’s
BulkTanimotoSimilarityfor empty fingerprints.- Return type:
Examples
>>> round(jaccard_similarity([1, 1, 0, 0], [1, 0, 0, 0]), 4) 0.5
- qsarkit.neighbors.jaccard_distance_matrix(X, Y=None)[source]¶
Pairwise Jaccard distance matrix (
1 - tanimoto_similarity_matrix).- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])Y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None])
- Returns:
Pairwise distances in [0, 1].
- Return type:
Examples
>>> D = jaccard_distance_matrix(np.array([[1, 1, 0], [1, 0, 0]])) >>> bool(np.allclose(np.diag(D), 0.0)) True
- qsarkit.neighbors.tanimoto_similarity_matrix(X, Y=None)[source]¶
Vectorized pairwise Tanimoto/Jaccard similarity matrix.
Computes all pairs at once with matrix algebra rather than a Python loop, which is what makes fingerprint similarity searches over tens of thousands of molecules practical.
For binary inputs the intersection is
X @ Y.Tand the union is|x| + |y| - intersection. For count inputs the MinMax form is computed in chunks.- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Fingerprint matrix.Y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Second fingerprint matrix. Defaults toX.
- Returns:
Pairwise similarities in [0, 1].
- Return type:
Examples
>>> S = tanimoto_similarity_matrix(np.array([[1, 1, 0], [1, 0, 0]])) >>> S.shape (2, 2) >>> bool(np.allclose(np.diag(S), 1.0)) True
- qsarkit.neighbors.is_binary(X)[source]¶
Return True if every entry of
Xis 0 or 1.- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Array to inspect.- Returns:
Whether the array holds only 0/1 values.
- Return type:
Examples
>>> is_binary(np.array([[0, 1], [1, 0]])) True >>> is_binary(np.array([[0, 2]])) False
- class qsarkit.neighbors.JaccardNeighborSearch(n_neighbors=5, chunk_size=1024)[source]¶
Bases:
BaseEstimatorNearest-neighbour search over fingerprints under Jaccard/Tanimoto distance.
This is the similarity-search primitive behind chemical-similarity applicability domains, read-across, and virtual screening triage. It is deliberately exact (brute force over a vectorized similarity matrix) rather than approximate: chemical fingerprint spaces are high-dimensional and sparse, where tree-based indices degrade to linear scans anyway.
- Parameters:
n_neighbors (
int) – Default number of neighbours returned bykneighbors().chunk_size (
int) – Number of query rows processed per block, bounding peak memory atchunk_size x n_trainfloats.
- Variables:
Examples
>>> import numpy as np >>> X = np.array([[1, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 1]]) >>> search = JaccardNeighborSearch(n_neighbors=2).fit(X) >>> dist, idx = search.kneighbors(np.array([[1, 1, 0, 0]])) >>> int(idx[0, 0]) 0 >>> float(dist[0, 0]) 0.0
References
Willett, P., Barnard, J. M. & Downs, G. M. (1998). “Chemical Similarity Searching.” J. Chem. Inf. Comput. Sci., 38(6), 983-996. https://doi.org/10.1021/ci9800211
Bajusz, D., Racz, A. & Heberger, K. (2015). “Why is Tanimoto Index an Appropriate Choice for Fingerprint-Based Similarity Calculations?” J. Cheminform., 7, 20. https://doi.org/10.1186/s13321-015-0069-3
scikit-learn nearest-neighbours documentation: https://scikit-learn.org/stable/modules/neighbors.html
- fit(X, y=None)[source]¶
Store the reference fingerprint matrix.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Reference fingerprints.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Present for scikit-learn API compatibility.
- Returns:
The fitted estimator.
- Return type:
- kneighbors(X, n_neighbors=None, return_distance=True)[source]¶
Find the
kmost similar reference fingerprints for each query.- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Query fingerprints.n_neighbors (
Optional[int]) – Overridesself.n_neighborsfor this call.return_distance (
bool) – If True return(distances, indices), else justindices.
- Return type:
Union[Tuple[ndarray[tuple[Any,...],dtype[double]],ndarray[tuple[Any,...],dtype[int_]]],ndarray[tuple[Any,...],dtype[int_]]]- Returns:
distances (
ndarrayofshape (n_queries,n_neighbors)) – Jaccard distances, ascending.indices (
ndarrayofshape (n_queries,n_neighbors)) – Indices into the fitted reference set.
- radius_neighbors(X, radius=0.3)[source]¶
Find all reference fingerprints within
radiusJaccard distance.
- similarity_search(X, threshold=0.7, max_hits=None)[source]¶
Classic similarity search: hits above a Tanimoto
threshold.- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Query fingerprints.threshold (
float) – Minimum Tanimoto similarity for a hit. 0.7 on ECFP4 is the long-standing rule-of-thumb cutoff for “similar” molecules.max_hits (
Optional[int]) – Truncate each query’s hit list to this many best hits.
- Returns:
Per-query hits sorted by descending similarity.
- Return type:
References
Maggiora, G. et al. (2014). “Molecular Similarity in Medicinal Chemistry.” J. Med. Chem., 57(8), 3186-3204. https://doi.org/10.1021/jm401411z
- class qsarkit.neighbors.JaccardKNeighborsClassifier(n_neighbors=5, weights='uniform', chunk_size=1024)[source]¶
Bases:
ClassifierMixin,_BaseJaccardKNNk-nearest-neighbours classifier under Jaccard/Tanimoto distance.
The canonical similarity-based QSAR classifier: a compound is predicted from the observed classes of its most structurally similar training neighbours. Using Tanimoto rather than Euclidean distance matters because fingerprints are sparse binary vectors, where Euclidean distance is dominated by the (huge) number of jointly-absent bits — exactly the bits that carry no chemical information.
- Parameters:
n_neighbors (
int) – Number of neighbours voting on each prediction.weights (
Literal['uniform','distance','similarity']) – Vote weighting."similarity"weights each neighbour by its Tanimoto coefficient;"distance"by1/d(with exact matches taking the row outright).chunk_size (
int) – Query block size for the similarity computation.
- Variables:
Examples
>>> import numpy as np >>> X = np.array([[1, 1, 0, 0], [1, 1, 1, 0], [0, 0, 1, 1], [0, 0, 1, 0]]) >>> y = np.array([1, 1, 0, 0]) >>> clf = JaccardKNeighborsClassifier(n_neighbors=1).fit(X, y) >>> int(clf.predict(np.array([[1, 1, 0, 0]]))[0]) 1
References
Cover, T. & Hart, P. (1967). “Nearest Neighbor Pattern Classification.” IEEE Trans. Inf. Theory, 13(1), 21-27. https://doi.org/10.1109/TIT.1967.1053964
Bajusz, D., Racz, A. & Heberger, K. (2015). “Why is Tanimoto Index an Appropriate Choice for Fingerprint-Based Similarity Calculations?” J. Cheminform., 7, 20. https://doi.org/10.1186/s13321-015-0069-3
Willett, P. (2006). “Similarity-Based Virtual Screening Using 2D Fingerprints.” Drug Discov. Today, 11(23-24), 1046-1053. https://doi.org/10.1016/j.drudis.2006.10.005
scikit-learn classifier API: https://scikit-learn.org/stable/developers/develop.html
- fit(X, y)[source]¶
Store training fingerprints and labels.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Training fingerprints.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Class labels.
- Returns:
The fitted estimator.
- Return type:
- set_score_request(*, sample_weight='$UNCHANGED$')¶
Configure whether metadata should be requested to be passed to the
scoremethod.Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with
enable_metadata_routing=True(seesklearn.set_config()). Please check the User Guide on how the routing mechanism works.The options for each parameter are:
True: metadata is requested, and passed toscoreif provided. The request is ignored if metadata is not provided.False: metadata is not requested and the meta-estimator will not pass it toscore.None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.str: metadata should be passed to the meta-estimator with this given alias instead of the original name.
The default (
sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.Added in version 1.3.
- class qsarkit.neighbors.JaccardKNeighborsRegressor(n_neighbors=5, weights='uniform', chunk_size=1024)[source]¶
Bases:
RegressorMixin,_BaseJaccardKNNk-nearest-neighbours regressor under Jaccard/Tanimoto distance.
Predicts a continuous endpoint (pIC50, logS, …) as the weighted mean of its structurally nearest training neighbours — the numerical form of read-across, and a strong similarity-only baseline that any fitted QSAR model should be expected to beat.
- Parameters:
n_neighbors (
int) – Number of neighbours averaged for each prediction.weights (
Literal['uniform','distance','similarity']) – Averaging weights, as inJaccardKNeighborsClassifier.chunk_size (
int) – Query block size for the similarity computation.
- Variables:
Examples
>>> import numpy as np >>> X = np.array([[1, 1, 0, 0], [1, 1, 1, 0], [0, 0, 1, 1]]) >>> y = np.array([7.0, 6.5, 4.0]) >>> reg = JaccardKNeighborsRegressor(n_neighbors=1).fit(X, y) >>> float(reg.predict(np.array([[1, 1, 0, 0]]))[0]) 7.0
References
Cover, T. & Hart, P. (1967). IEEE Trans. Inf. Theory, 13(1), 21-27. https://doi.org/10.1109/TIT.1967.1053964
Sheridan, R. P. et al. (2004). “Similarity to Molecules in the Training Set Is a Good Discriminator for Prediction Accuracy in QSAR.” J. Chem. Inf. Comput. Sci., 44(6), 1912-1928. https://doi.org/10.1021/ci049782w
Bajusz, D., Racz, A. & Heberger, K. (2015). J. Cheminform., 7, 20. https://doi.org/10.1186/s13321-015-0069-3
- fit(X, y)[source]¶
Store training fingerprints and endpoint values.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Training fingerprints.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Continuous target values.
- Returns:
The fitted estimator.
- Return type:
- set_score_request(*, sample_weight='$UNCHANGED$')¶
Configure whether metadata should be requested to be passed to the
scoremethod.Note that this method is only relevant when this estimator is used as a sub-estimator within a meta-estimator and metadata routing is enabled with
enable_metadata_routing=True(seesklearn.set_config()). Please check the User Guide on how the routing mechanism works.The options for each parameter are:
True: metadata is requested, and passed toscoreif provided. The request is ignored if metadata is not provided.False: metadata is not requested and the meta-estimator will not pass it toscore.None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.str: metadata should be passed to the meta-estimator with this given alias instead of the original name.
The default (
sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.Added in version 1.3.
References¶
Willett, P. (2006). “Similarity-Based Virtual Screening Using 2D Fingerprints.” Drug Discov. Today, 11(23-24), 1046-1053. doi:10.1016/j.drudis.2006.10.005
Bajusz, D., Racz, A. & Heberger, K. (2015). “Why Is Tanimoto Index an Appropriate Choice for Fingerprint-Based Similarity Calculations?” J. Cheminform., 7, 20. doi:10.1186/s13321-015-0069-3
Johnson, M. A. & Maggiora, G. M. (1990). “Concepts and Applications of Molecular Similarity.” Wiley.