Toolkit containing different molecule clustering techniques and algorithms.
Three clustering methods are implemented:
- Butina-based clustering — fingerprint similarity with the Butina algorithm;
- MCS-based clustering — Maximum Common Substructure similarity;
- Rascal MCES-based clustering — Maximum Common Edge Subgraph similarity via the RASCAL algorithm.
All methods share a common set of clustering algorithms that operate on the computed similarity matrix, selectable through cluster_molecules(algorithm=...): "DBSCAN", "Hierarchical", "HierarchicalSilhouette", "Spectral" and "GraphBased". ButinaClustering additionally offers "Butina" (its default), and RascalMCES offers its own fuzzy and non-fuzzy MCES clustering.
Every one of these returns an integer array with one cluster label per molecule, in
input order, and leaves the same array in self.mol_clusters — so the result can be
assigned straight to a dataframe column regardless of which algorithm produced it.
The exceptions are Rascal's own fuzzy_mces_clustering and butina_mces_clustering,
which return RDKit's groups of molecule indices, as a molecule may belong to more
than one fuzzy cluster. ButinaClustering also exposes its index groups as
self.cluster_members, where the first index of each tuple is the cluster centroid.
python -m pip install git+https://github.com/David-Araripe/MolClusterkit.git
import pandas as pd
from MolClusterkit import ButinaClustering
df = pd.read_csv(...) # Your dataframe
smiles_list = [...] # Your list of SMILES
bclusterer = ButinaClustering(smiles_list)
clusters = bclusterer.cluster_molecules(dist_th=0.4)
# if you want to assign the clusters your dataframe:
df = df.assign(cluster_id = clusters)
# the shared algorithms are available on the same object:
clusters = bclusterer.cluster_molecules(algorithm="Hierarchical", t=5)Unparseable SMILES raise a ValueError naming the offending indices, so the returned
cluster labels always line up one-to-one with the input list.
from MolClusterkit import MCSClustering
smiles_list = [...] # Your list of SMILES
mcs_cluster = MCSClustering(smiles_list, timeout=15)
smarts_arr, similarity_matrix = mcs_cluster.compute_similarity_matrix()
# Now you can cluster the molecules using any of the methods:
clusters = mcs_cluster.dbscan_clustering(...)
clusters = mcs_cluster.hierarchical_clustering(...)
clusters = mcs_cluster.hierarchical_silhouette_clustering(max_clusters=20)
clusters = mcs_cluster.graph_based_clustering(...)
clusters = mcs_cluster.spectral_clustering(...)compute_similarity_matrix has to be called first here, and on RascalMCES: both run
a maximum common substructure search over every pair of molecules, which is slow and
needs a similarity_metric you should be choosing yourself, so neither will start one
behind your back. ButinaClustering is the deliberate exception — its matrix is
Tanimoto over fingerprints it already holds, so cluster_molecules just computes it
on demand.
The timeout parameter is the wall-time in seconds per pairwise MCS computation. If the timeout is reached, the algorithm stops and returns the current result. Default is 15 seconds. See RDKit's MCS docs for more details.
Two similarity metrics are available (default is "johnson"):
- Johnson:
(atoms_mcs + bonds_mcs)² / ((atoms_mol1 + bonds_mol1) × (atoms_mol2 + bonds_mol2)) - smaller/mces:
atoms_mcs / min(atoms_mol1, atoms_mol2)
hierarchical_silhouette_clustering automatically selects the best number of clusters by maximizing the silhouette score across a range of 2 to max_clusters. The sweep is additionally capped at n_molecules - 1, since a silhouette score is undefined once every molecule sits in its own cluster; at least 3 molecules are required.
from MolClusterkit import RascalMCES
smiles_list = [...] # Your list of SMILES
rascal = RascalMCES(smiles_list, similarityThreshold=0.7)
smarts_arr, similarity_matrix = rascal.compute_similarity_matrix()
# Use any of the shared clustering algorithms:
clusters = rascal.hierarchical_silhouette_clustering(max_clusters=20)
# Or use Rascal's own clustering methods:
clusters = rascal.fuzzy_mces_clustering(cutoff=0.7) # molecules can belong to multiple clusters
clusters = rascal.butina_mces_clustering(cutoff=0.7) # non-fuzzyfuzzy_mces_clustering and butina_mces_clustering let RDKit do the MCES comparisons
internally, so neither one needs compute_similarity_matrix to have been called.
The similarityThreshold parameter controls a fast pre-filter: molecule pairs with an estimated similarity below this threshold skip the full MCES computation and get a similarity of 0. This makes Rascal fast but can produce sparse similarity matrices. Lower the threshold if you need more complete coverage.
Neither MCSClustering nor RascalMCES needs a SMILES list to be constructed. Left
empty, they work as configured front-ends to RDKit's rdFMCS and rdRascalMCES:
set the comparison options once, then query any molecules you like. This is often the
quickest way to explore how the MCS options change the substructure you get back,
since the option objects and enums are wrapped behind plain keyword arguments.
from MolClusterkit import MCSClustering, RascalMCES
# Configure once, use for any pair
mcs = MCSClustering(timeout=15)
smarts, score = mcs.mcs_similarity(("CCO", "CCN"))
rascal = RascalMCES(similarityThreshold=0.0)
smarts, score = rascal.mces_similarity(("CCO", "CCN"))mcs_in_many takes any number of molecules at once, which makes comparing settings
a matter of changing one keyword:
series = ["c1ccccc1C(=O)O", "c1ccccc1CC(=O)O", "c1ccccc1CCC(=O)O"]
strict = MCSClustering(ringMatchesRingOnly=True, completeRingsOnly=True, timeout=5)
loose = MCSClustering(atomCompare="CompareAny", bondCompare="CompareAny", timeout=5)
print(strict.mcs_in_many(series).smartsString)
print(loose.mcs_in_many(series).smartsString)The keyword arguments are validated against the options RDKit accepts, so a
misspelled setting raises instead of being silently ignored. atomCompare,
bondCompare and ringCompare take the enum member name as a string (e.g.
"CompareAny"); the rest — maximizeBonds, threshold, matchValences,
ringMatchesRingOnly, completeRingsOnly, matchChiralTag, verbose — are passed
straight through. Unparseable SMILES raise a ValueError naming the offending
indices. Check result.canceled to see whether the search hit the timeout before
finding the true maximum.
This package clusters molecules and stops there. Since every method returns labels in input order, whatever you want to do with the clusters is one step of ordinary pandas, kept in your hands rather than behind a flag:
df = df.assign(cluster_id=bclusterer.cluster_molecules(dist_th=0.35))
# e.g. the highest-scoring compound of each cluster
best = df.loc[df.groupby("cluster_id")["pIC50"].idxmax()]