alinemol.splitters¶
The splitters module provides a unified API for molecular dataset splitting with multiple strategies to simulate different types of distribution shift.
Quick Start¶
Using the Factory Function (Recommended)¶
The easiest way to create splitters is using the get_splitter() factory function:
from alinemol.splitters import get_splitter, get_splitter_names
# List all available splitters
print(get_splitter_names())
# ['butina', 'datasail', 'hi', 'kmeans', 'lo', 'max_dissimilarity',
# 'molecular_logp', 'molecular_weight', 'molecular_weight_reverse',
# 'perimeter', 'random', 'scaffold', 'scaffold_generic',
# 'scaffold_kmeans', 'umap']
# Create a splitter
splitter = get_splitter("scaffold", make_generic=True, n_splits=5, test_size=0.2)
# Use with SMILES directly
smiles = ["CCO", "c1ccccc1", "CCN", ...]
for train_idx, test_idx in splitter.split(smiles):
train_smiles = [smiles[i] for i in train_idx]
test_smiles = [smiles[i] for i in test_idx]
Direct Class Import¶
You can also import splitter classes directly:
from alinemol.splitters import ScaffoldSplit, KMeansSplit, MolecularWeightSplit
# Create splitter instance
splitter = ScaffoldSplit(make_generic=True, n_splits=5, test_size=0.2)
# Split your data
for train_idx, test_idx in splitter.split(smiles_list):
# train_idx and test_idx are numpy arrays of indices
pass
Available Splitters¶
Structure-Based Splitters¶
| Splitter | Description |
|---|---|
scaffold |
Bemis-Murcko scaffold-based splitting |
scaffold_generic |
Generic scaffold-based splitting |
butina |
Taylor-Butina clustering algorithm |
Property-Based Splitters¶
| Splitter | Description |
|---|---|
molecular_weight |
Split by molecular weight (test on larger) |
molecular_weight_reverse |
Split by molecular weight (test on smaller) |
molecular_logp |
Split by lipophilicity (LogP) |
Clustering-Based Splitters¶
| Splitter | Description |
|---|---|
kmeans |
K-means clustering on fingerprints |
umap |
UMAP + hierarchical clustering |
max_dissimilarity |
Maximum dissimilarity selection |
perimeter |
Perimeter-based sampling |
scaffold_kmeans |
Scaffold extraction + k-means on scaffold ECFP |
Similarity-Based Splitters¶
| Splitter | Description |
|---|---|
hi |
Hi-split: ensures low train/test similarity |
lo |
Lo-split: for lead optimization scenarios |
Other Splitters¶
| Splitter | Description |
|---|---|
random |
Random baseline splitting |
datasail |
DataSAIL integration for advanced splitting |
Factory Functions¶
get_splitter
¶
get_splitter(name: str, **kwargs: Any) -> BaseMolecularSplitter
Factory function to create a splitter instance by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Name of the splitter (case-insensitive). Supports aliases. Available splitters can be listed with list_splitters(). |
required |
**kwargs
|
Any
|
Configuration parameters passed to splitter constructor. Common parameters include: - n_splits: Number of splits (default varies by splitter) - test_size: Proportion or count for test set - random_state: Random seed for reproducibility |
{}
|
Returns:
| Type | Description |
|---|---|
BaseMolecularSplitter
|
Configured splitter instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If splitter name is not recognized. |
Example
Create a scaffold splitter¶
splitter = get_splitter("scaffold", make_generic=True, n_splits=5) for train_idx, test_idx in splitter.split(smiles_list): ... pass ...
Create a KMeans splitter¶
splitter = get_splitter("kmeans", n_clusters=10, test_size=0.2) ...
Using aliases¶
splitter = get_splitter("mw", generalize_to_larger=True) # molecular_weight
get_splitter_names
¶
Return list of all registered splitter names.
Returns:
| Type | Description |
|---|---|
List[str]
|
Sorted list of splitter names. |
Example
names = get_splitter_names() print(names) ['butina', 'datasail', 'hi', 'kmeans', ...]
list_splitters
¶
list_splitters() -> Dict[str, Type[BaseMolecularSplitter]]
Return dictionary of all registered splitters.
Returns:
| Type | Description |
|---|---|
Dict[str, Type[BaseMolecularSplitter]]
|
Dictionary mapping splitter names to their classes. |
Example
splitters = list_splitters() print(splitters.keys()) dict_keys(['scaffold', 'kmeans', 'molecular_weight', ...])
register_splitter
¶
Decorator to register a splitter class in the registry.
Use this decorator on splitter classes to make them available via the get_splitter() factory function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Primary name for the splitter (lowercase, underscore-separated). |
required |
aliases
|
Optional[List[str]]
|
Optional list of alternative names for the splitter. |
None
|
Returns:
| Type | Description |
|---|---|
|
Decorator function that registers the class. |
Example
@register_splitter("my_splitter", aliases=["my-splitter", "mysplit"]) ... class MySplitter(BaseMolecularSplitter): ... def _iter_indices(self, X, y=None, groups=None): ... yield train_idx, test_idx ... splitter = get_splitter("my_splitter") splitter = get_splitter("mysplit") # Also works via alias
Base Class¶
BaseMolecularSplitter
¶
BaseMolecularSplitter(
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseCrossValidator, ABC
Abstract base class for all molecular splitters in ALineMol.
All splitters should inherit from this class to ensure a consistent API. The primary API is SMILES-first, with optional feature input support.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. |
5
|
test_size
|
Optional[Union[float, int]]
|
Proportion or absolute number of samples for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or absolute number of samples for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
n_splits |
Number of splitting iterations. |
|
test_size |
Size of the test set. |
|
train_size |
Size of the training set. |
|
random_state |
Random state for reproducibility. |
Example
class MySplitter(BaseMolecularSplitter): ... def _iter_indices(self, X, y=None, groups=None): ... # Implementation here ... yield train_idx, test_idx ... splitter = MySplitter(n_splits=5, test_size=0.2) for train_idx, test_idx in splitter.split(smiles_list): ... train = [smiles_list[i] for i in train_idx] ... test = [smiles_list[i] for i in test_idx]
split
¶
split(
X: Union[List[str], ndarray],
y: Optional[ndarray] = None,
groups: Optional[ndarray] = None,
) -> Iterator[Tuple[ndarray, ndarray]]
Generate indices to split data into training and test sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
Union[List[str], ndarray]
|
SMILES strings or feature array. If SMILES strings are provided, they will be converted to features internally when needed. |
required |
y
|
Optional[ndarray]
|
Target variable (optional). Used for stratified splitting. |
None
|
groups
|
Optional[ndarray]
|
Group labels (optional). Used for group-based splitting. |
None
|
Yields:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
Array of training set indices. |
test_indices |
ndarray
|
Array of test set indices. |
Example
splitter = MySplitter(n_splits=3, test_size=0.2) smiles = ["CCO", "c1ccccc1", "CCN", "CCCC", "CC(C)C"] for train_idx, test_idx in splitter.split(smiles): ... print(f"Train: {train_idx}, Test: {test_idx}")
get_n_splits
¶
get_n_splits(
X: Optional[Union[List[str], ndarray]] = None,
y: Optional[ndarray] = None,
groups: Optional[ndarray] = None,
) -> int
Return the number of splitting iterations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
Optional[Union[List[str], ndarray]]
|
Ignored, present for API compatibility. |
None
|
y
|
Optional[ndarray]
|
Ignored, present for API compatibility. |
None
|
groups
|
Optional[ndarray]
|
Ignored, present for API compatibility. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of splitting iterations (n_splits). |
set_smiles
¶
set_smiles(smiles: List[str]) -> BaseMolecularSplitter
Set SMILES for splitting when features are passed to split().
This is useful when you want to pass pre-computed features to split() but the splitter needs access to the original SMILES strings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
smiles
|
List[str]
|
List of SMILES strings. |
required |
Returns:
| Type | Description |
|---|---|
BaseMolecularSplitter
|
Self, for method chaining. |
Example
features = compute_fingerprints(smiles_list) splitter = ScaffoldSplit().set_smiles(smiles_list) for train_idx, test_idx in splitter.split(features): ... pass
Splitter Classes¶
Wrapper Classes (splito-based)¶
ScaffoldSplit
¶
ScaffoldSplit(
n_splits: int = 5,
make_generic: bool = False,
n_jobs: Optional[int] = None,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
Bemis-Murcko scaffold-based molecular splitter.
Groups molecules by their Bemis-Murcko scaffolds, ensuring molecules with the same scaffold are in the same split. This helps evaluate model generalization to novel scaffolds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. Default 5. |
5
|
make_generic
|
bool
|
If True, use generic scaffolds (atoms replaced). Default False. |
False
|
n_jobs
|
Optional[int]
|
Number of parallel jobs for scaffold extraction. |
None
|
test_size
|
Optional[Union[float, int]]
|
Proportion or count for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
Example
splitter = ScaffoldSplit(make_generic=True, n_splits=5) for train_idx, test_idx in splitter.split(smiles_list): ... # Molecules with same scaffold will be in same set ... train = [smiles_list[i] for i in train_idx]
KMeansSplit
¶
KMeansSplit(
n_clusters: int = 10,
n_splits: int = 5,
metric: Union[str, Callable] = "euclidean",
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
n_jobs: Optional[int] = None,
)
Bases: BaseMolecularSplitter
K-Means clustering based molecular splitter.
Groups molecules into clusters using K-Means clustering on molecular fingerprints, then assigns clusters to train/test sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of clusters to create. Default 10. |
10
|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. Default 5. |
5
|
metric
|
Union[str, Callable]
|
Distance metric for clustering. Default "euclidean". |
'euclidean'
|
test_size
|
Optional[Union[float, int]]
|
Proportion or count for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
n_jobs
|
Optional[int]
|
Number of parallel jobs. Default None. |
None
|
Example
splitter = KMeansSplit(n_clusters=10, n_splits=5, test_size=0.2) for train_idx, test_idx in splitter.split(smiles_list): ... train = [smiles_list[i] for i in train_idx] ... test = [smiles_list[i] for i in test_idx]
MolecularWeightSplit
¶
MolecularWeightSplit(
generalize_to_larger: bool = True,
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
Molecular weight-based splitter.
Splits molecules based on molecular weight, allowing evaluation of model generalization to larger (or smaller) molecules.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
generalize_to_larger
|
bool
|
If True, test set contains heavier molecules. If False, test set contains lighter molecules. Default True. |
True
|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. Default 5. |
5
|
test_size
|
Optional[Union[float, int]]
|
Proportion or count for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
Example
splitter = MolecularWeightSplit(generalize_to_larger=True) for train_idx, test_idx in splitter.split(smiles_list): ... # Test molecules will have higher MW than train ... pass
MaxDissimilaritySplit
¶
MaxDissimilaritySplit(
n_clusters: int = 25,
metric: Union[str, Callable] = "euclidean",
n_jobs: Optional[int] = None,
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
Maximum dissimilarity-based splitter.
Uses greedy maximum dissimilarity algorithm to select diverse test molecules, ensuring good coverage of chemical space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of diverse molecules to select. Default 25. |
25
|
metric
|
Union[str, Callable]
|
Distance metric for diversity calculation. Default "euclidean". |
'euclidean'
|
n_jobs
|
Optional[int]
|
Number of parallel jobs. |
None
|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. Default 5. |
5
|
test_size
|
Optional[Union[float, int]]
|
Proportion or count for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
Example
splitter = MaxDissimilaritySplit(n_clusters=25, n_splits=5) for train_idx, test_idx in splitter.split(smiles_list): ... # Test set contains maximally diverse molecules ... pass
PerimeterSplit
¶
PerimeterSplit(
n_clusters: int = 25,
metric: Union[str, Callable] = "euclidean",
n_jobs: Optional[int] = None,
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
Perimeter-based molecular splitter.
Uses perimeter-based sampling to create diverse train/test splits by selecting molecules from the perimeter of the chemical space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of perimeter points to consider. Default 25. |
25
|
metric
|
Union[str, Callable]
|
Distance metric for perimeter calculation. Default "euclidean". |
'euclidean'
|
n_jobs
|
Optional[int]
|
Number of parallel jobs. |
None
|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. Default 5. |
5
|
test_size
|
Optional[Union[float, int]]
|
Proportion or count for test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
Example
splitter = PerimeterSplit(n_clusters=25, n_splits=5) for train_idx, test_idx in splitter.split(smiles_list): ... pass
Native Splitters¶
RandomSplit
¶
RandomSplit(
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: RandomStateType = None,
n_jobs: Optional[int] = None,
)
Bases: ShuffleSplit
Uniform random train/test splitter (in-distribution baseline).
Thin wrapper around sklearn.model_selection.ShuffleSplit that accepts SMILES
in split() and is registered in the factory under the name "random".
Useful as an ID baseline against the OOD-style splitters in this package.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_splits
|
int
|
Number of re-shuffling & splitting iterations. |
5
|
test_size
|
Optional[Union[float, int]]
|
Proportion (float) or absolute count (int) of test samples. |
None
|
train_size
|
Optional[Union[float, int]]
|
Proportion (float) or absolute count (int) of train samples. |
None
|
random_state
|
RandomStateType
|
Random seed for reproducibility. |
None
|
n_jobs
|
Optional[int]
|
Accepted for API symmetry; ignored (random sampling is fast and serial). |
None
|
Examples:
>>> from alinemol.splitters import RandomSplit
>>> splitter = RandomSplit(n_splits=3, test_size=0.2, random_state=0)
>>> smiles = ["CCO", "c1ccccc1", "CCN", "CCCC", "CC(C)C"] * 10
>>> for train_idx, test_idx in splitter.split(smiles):
... print(len(train_idx), len(test_idx))
... break
MolecularLogPSplit
¶
MolecularLogPSplit(
generalize_to_larger: bool = True,
n_splits: int = 5,
smiles: Optional[SMILESList] = None,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: RandomStateType = None,
)
Bases: BaseShuffleSplit
Split a molecular dataset by sorting molecules according to their LogP values.
This splitter is designed for chemical domain shift experiments, where you want to evaluate how well models generalize to molecules with different physical properties than those they were trained on. LogP (octanol-water partition coefficient) is a measure of lipophilicity, which affects molecular solubility, permeability, and binding properties.
The splitter works by: 1. Calculating LogP values for all molecules 2. Sorting molecules by their LogP values 3. Splitting the sorted list according to train/test size parameters
When generalize_to_larger=True (default), the training set contains molecules with lower LogP values, and the test set contains those with higher LogP values. This mimics the real-world scenario of testing on molecules with properties outside the training distribution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
generalize_to_larger
|
bool
|
bool, default=True If True, train set will have smaller LogP values, test set will have larger values. If False, train set will have larger LogP values, test set will have smaller values. |
True
|
n_splits
|
int
|
int, default=5 Number of re-shuffling & splitting iterations. Note that for this deterministic splitter, all iterations will produce the same split. |
5
|
smiles
|
Optional[SMILESList]
|
List[str], optional List of SMILES strings if not provided directly as input in split() or _iter_indices(). Useful when the input X to those methods is not a list of SMILES strings but some other feature representation. |
None
|
test_size
|
Optional[Union[float, int]]
|
float or int, optional If float, represents the proportion of the dataset to include in the test split. If int, represents the absolute number of test samples. If None, the value is set to the complement of the train size. |
None
|
train_size
|
Optional[Union[float, int]]
|
float or int, optional If float, represents the proportion of the dataset to include in the train split. If int, represents the absolute number of train samples. If None, the value is automatically set to the complement of the test size. |
None
|
random_state
|
RandomStateType
|
int or RandomState instance, optional Controls the randomness of the training and testing indices produced. Note that this splitter is deterministic, so random_state only affects the implementation of _validate_shuffle_split. |
None
|
Examples:
>>> from alinemol.splitters import MolecularLogPSplit
>>> import numpy as np
>>> # Example with list of SMILES
>>> smiles = ["CCO", "CC(=O)O", "c1ccccc1", "CCN", "CCCCCCC"]
>>> splitter = MolecularLogPSplit(generalize_to_larger=True, test_size=0.4)
>>> for train_idx, test_idx in splitter.split(smiles):
... print(f"Training on: {[smiles[i] for i in train_idx]}")
... print(f"Testing on: {[smiles[i] for i in test_idx]}")
... break # Just show the first split
>>> # Example with separate features and target
>>> X = np.random.randn(5, 10) # Some molecular features
>>> y = np.random.randint(0, 2, 5) # Binary target
>>> splitter = MolecularLogPSplit(smiles=smiles, test_size=0.4)
>>> for train_idx, test_idx in splitter.split(X, y):
... X_train, X_test = X[train_idx], X[test_idx]
... y_train, y_test = y[train_idx], y[test_idx]
... break # Just show the first split
Notes
- LogP values are calculated using the Crippen method implemented in datamol
- This splitter is deterministic - calling split() multiple times will produce the same split regardless of n_splits value
- Useful for testing model extrapolation to molecules with different physical-chemical properties than the training set
UMAPSplit
¶
UMAPSplit(
n_clusters: int = 10,
n_neighbors: int = 100,
min_dist: float = 0.1,
n_components: int = 2,
umap_metric: Union[str, Callable] = "jaccard",
linkage: str = "ward",
n_splits: int = 5,
n_jobs: int = -1,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[Union[int, RandomState]] = None,
**kwargs,
)
Bases: GroupShuffleSplit
Group-based split that uses the UMAP clustering in the input space for splitting.
From the following papers: 1. "UMAP-based clustering split for rigorous evaluation of AI models for virtual screening on cancer cell lines" https://doi.org/10.26434/chemrxiv-2024-f1v2v-v2 2. "On the Best Way to Cluster NCI-60 Molecules" https://doi.org/10.3390/biom13030498
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
The number of clusters to use for clustering |
10
|
n_neighbors
|
int
|
The number of neighbors to use for the UMAP algorithm |
100
|
min_dist
|
float
|
The minimum distance between points in the UMAP embedding |
0.1
|
n_components
|
int
|
The number of components to use for the PCA algorithm |
2
|
umap_metric
|
Union[str, Callable]
|
The metric to use for the UMAP algorithm |
'jaccard'
|
linkage
|
str
|
The linkage to use for the AgglomerativeClustering algorithm |
'ward'
|
n_splits
|
int
|
The number of splits to use for the split |
5
|
test_size
|
Optional[Union[float, int]]
|
The size of the test set |
None
|
train_size
|
Optional[Union[float, int]]
|
The size of the train set |
None
|
random_state
|
Optional[Union[int, RandomState]]
|
The random state to use for the split |
None
|
Examples:
>>> from alinemol.splitters import UMAPSplit
>>> splitter = UMAPSplit(n_clusters=2, linkage="ward", n_neighbors=3, min_dist=0.1, n_components=2, n_splits=5)
>>> smiles = ["c1ccccc1", "CCC", "CCCC(CCC)C(=O)O", "NC1CCCCC1N","COc1cc(CNC(=O)CCCCC=CC(C)C)ccc1O", "Cc1cc(Br)c(O)c2ncccc12", "OCC(O)c1oc(O)c(O)c1O"]
>>> for train_idx, test_idx in splitter.split(smiles):
... print(train_idx)
... print(test_idx)
... break # Just show the first split
BUTINASplit
¶
BUTINASplit(
n_clusters: int = 10,
n_splits: int = 5,
metric: Union[str, Callable] = "euclidean",
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[Union[int, RandomState]] = None,
cutoff: float = 0.65,
)
Bases: GroupShuffleSplit
Group-based split that uses the BUTINA clustering in the input space for splitting. From "BUTINA: A New Method for the Clustering of Chemical Compounds" https://doi.org/10.1021/ci9803381
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
The number of clusters to use for clustering |
10
|
n_splits
|
int
|
The number of splits to generate |
5
|
metric
|
Union[str, Callable]
|
The metric to use for clustering |
'euclidean'
|
test_size
|
Optional[Union[float, int]]
|
The size of the test set |
None
|
cutoff
|
float
|
The cutoff value to use for clustering |
0.65
|
Examples:
>>> from alinemol.splitters import BUTINASplit
>>> splitter = BUTINASplit(n_clusters=10, n_splits=5, cutoff=0.65)
>>> train_idx, test_idx = splitter.split(X, y, groups)
ScaffoldKMeansSplit
¶
ScaffoldKMeansSplit(
n_clusters: int = 10,
make_generic: bool = False,
n_splits: int = 5,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[Union[int, RandomState]] = None,
n_jobs: Optional[int] = None,
)
Bases: GroupShuffleSplit
Group-based split that extracts Bemis-Murcko scaffolds, clusters them with k-means on ECFP fingerprints, and assigns each molecule to its scaffold's cluster.
This creates a middle ground between ScaffoldSplit (exact scaffold matching, many small groups) and KMeansSplit (clusters on whole-molecule fingerprints, ignores scaffold structure).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of k-means clusters for scaffolds. |
10
|
make_generic
|
bool
|
Whether to use generic Bemis-Murcko scaffolds. |
False
|
n_splits
|
int
|
Number of splits to generate. |
5
|
test_size
|
Optional[Union[float, int]]
|
Size of the test set. |
None
|
train_size
|
Optional[Union[float, int]]
|
Size of the train set. |
None
|
random_state
|
Optional[Union[int, RandomState]]
|
Random state for reproducibility. |
None
|
n_jobs
|
Optional[int]
|
Number of jobs for parallelized scaffold extraction. |
None
|
Examples:
>>> from alinemol.splitters import ScaffoldKMeansSplit
>>> splitter = ScaffoldKMeansSplit(n_clusters=5, n_splits=2, make_generic=True)
>>> smiles = ["CCO", "CCCO", "c1ccccc1", "c1ccc(O)cc1"] * 10
>>> for train_idx, test_idx in splitter.split(smiles):
... print(len(train_idx), len(test_idx))
... break
Similarity-Based Splitters¶
HiSplit
¶
HiSplit(
similarity_threshold: float = 0.4,
train_min_frac: float = 0.7,
test_min_frac: float = 0.15,
coarsening_threshold: Optional[float] = None,
verbose: bool = True,
max_mip_gap: float = 0.1,
n_splits: int = 1,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
A splitter that creates train/test splits with no molecules in the test set having ECFP4 Tanimoto similarity greater than similarity_threshold to molecules in the train set.
This splitter is designed for evaluating model generalization to structurally dissimilar molecules. It uses a min vertex k-cut algorithm to optimally partition molecules while respecting similarity constraints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
similarity_threshold
|
float
|
ECFP4 Tanimoto threshold. Molecules in the test set won't have a similarity greater than this threshold to those in the train set. |
0.4
|
train_min_frac
|
float
|
Minimum fraction for the train set, e.g., 0.7 of the entire dataset. |
0.7
|
test_min_frac
|
float
|
Minimum fraction for the test set, e.g., 0.1 of the entire dataset. It's possible that the k-cut might not be feasible without discarding some molecules, so ensure that the sum of train_min_frac and test_min_frac is less than 1.0. |
0.15
|
coarsening_threshold
|
Optional[float]
|
Molecules with a similarity greater than the coarsening_threshold will be clustered together. It speeds up execution, but makes the solution less optimal. None -- Disables clustering (default value). 1.0 -- Won't do anything 0.90 -- will cluster molecules with similarity > 0.90 together |
None
|
verbose
|
bool
|
If set to False, suppresses status messages. |
True
|
max_mip_gap
|
float
|
Determines when to halt optimization based on proximity to the optimal solution. For example, setting it to 0.5 yields a faster but less optimal solution, while 0.01 aims for a more optimal solution, potentially at the cost of more computation time. |
0.1
|
n_splits
|
int
|
Number of splits to generate (default 1, as this is deterministic). |
1
|
test_size
|
Optional[Union[float, int]]
|
Accepted for API symmetry with the other splitters. The Hi
partition is governed by |
None
|
train_size
|
Optional[Union[float, int]]
|
Accepted for API symmetry; see |
None
|
random_state
|
Optional[int]
|
Accepted for API symmetry with the other splitters. This splitter is deterministic, so the value is ignored. |
None
|
k_fold_split
¶
k_fold_split(
smiles: List[str], k: int = 3, fold_min_frac: Optional[float] = None
) -> List[List[int]]
Split the dataset into k folds such that no molecule in any fold has an ECFP4 Tanimoto similarity greater than similarity_threshold when compared to molecules in another fold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
smiles
|
List[str]
|
List of SMILES strings representing molecules |
required |
k
|
int
|
Number of folds |
3
|
fold_min_frac
|
Optional[float]
|
Minimum fraction of a fold (e.g., 0.2 of the entire dataset). If not specified (None), it defaults to 0.9 / k. |
None
|
Returns:
| Type | Description |
|---|---|
List[List[int]]
|
List[List[int]]: List of lists, where each list contains the indices of molecules in that fold |
LoSplit
¶
LoSplit(
threshold: float = 0.4,
min_cluster_size: int = 5,
max_clusters: int = 50,
std_threshold: float = 0.6,
n_jobs: int = -1,
verbose: int = 1,
n_splits: int = 1,
test_size: Optional[Union[float, int]] = None,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
)
Bases: BaseMolecularSplitter
A splitter that prepares data for training ML models for Lead Optimization or to guide molecular generative models. These models must be sensitive to minor modifications of molecules, and this splitter constructs a test that allows the evaluation of a model's ability to distinguish those modifications.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
ECFP4 1024-bit Tanimoto similarity threshold. Molecules more similar than this threshold are considered too similar and can be grouped together in one cluster. |
0.4
|
min_cluster_size
|
int
|
the minimum number of molecules per cluster. |
5
|
max_clusters
|
int
|
the maximum number of selected clusters. The remaining molecules go to the training set. This can be useful for limiting your test set to get more molecules in the train set. |
50
|
std_threshold
|
float
|
the lower bound of the acceptable standard deviation for a cluster's values. It should be greater than the measurement noise. For ChEMBL-like data set it to 0.60 for logKi and 0.70 for logIC50. Set it lower if you have a high-quality dataset. |
0.6
|
n_jobs
|
int
|
number of parallel jobs to run, -1 means use all processors. |
-1
|
verbose
|
int
|
set to 0 to turn off the progress bar. |
1
|
n_splits
|
int
|
Number of splits to generate (default 1, as this is deterministic). |
1
|
test_size
|
Optional[Union[float, int]]
|
Accepted for API symmetry with the other splitters. The Lo
test set size is governed by the clustering parameters
( |
None
|
train_size
|
Optional[Union[float, int]]
|
Accepted for API symmetry; see |
None
|
random_state
|
Optional[int]
|
Accepted for API symmetry with the other splitters. This splitter is deterministic, so the value is ignored. |
None
|
For more information, see a tutorial in the docs and Steshin 2023, Lo-Hi: Practical ML Drug Discovery Benchmark.
Advanced Splitters¶
DataSAILSplit
¶
DataSAILSplit(
technique: str = "C",
cluster_method: str = "ECFP",
n_splits: int = 1,
test_size: Optional[Union[float, int]] = 0.2,
train_size: Optional[Union[float, int]] = None,
random_state: Optional[int] = None,
delta: float = 0.1,
)
Bases: BaseMolecularSplitter
DataSAIL-based splitter for molecular datasets.
Uses the DataSAIL algorithm to create train/test splits that minimize information leakage based on molecular similarity. DataSAIL uses clustering-based approaches to ensure molecules in the test set are structurally different from those in the training set.
This implementation wraps the datasail library and provides a consistent interface with other ALineMol splitters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
technique
|
str
|
Splitting technique. Options: - "R": Random splitting (baseline) - "I": Identity-based splitting (exact duplicates) - "C": Cluster-based splitting (default, recommended) |
'C'
|
cluster_method
|
str
|
Clustering method when technique="C". Options: "ECFP" (default), "Murcko", etc. |
'ECFP'
|
n_splits
|
int
|
Number of splits to generate. Default 1. |
1
|
test_size
|
Optional[Union[float, int]]
|
Fraction or count for test set. Default 0.2. |
0.2
|
train_size
|
Optional[Union[float, int]]
|
Fraction or count for train set. |
None
|
random_state
|
Optional[int]
|
Random seed for reproducibility. |
None
|
delta
|
float
|
Allowed deviation from requested split sizes. Default 0.1. |
0.1
|
Example
splitter = DataSAILSplit(technique="C", test_size=0.2) for train_idx, test_idx in splitter.split(smiles_list): ... train = [smiles_list[i] for i in train_idx] ... test = [smiles_list[i] for i in test_idx]
Note
Requires the datasail package to be installed: pip install datasail
get_n_splits
¶
get_n_splits(
X: Optional[Union[List[str], ndarray]] = None,
y: Optional[ndarray] = None,
groups: Optional[ndarray] = None,
) -> int
Return the number of splitting iterations.
Split Quality Analysis¶
SplitAnalyzer complements the splitters by quantifying how a given split
behaves: train↔test Tanimoto similarity distribution, scaffold overlap,
property-distribution divergence, and basic size metrics.
See the guide
For usage patterns — comparing splitters, reusing a precomputed Jaccard distance matrix, and interpreting the metrics — see the Split Quality Analysis guide. The reference below documents the class and its report dataclasses.
SplitAnalyzer
¶
SplitAnalyzer(
smiles: List[str],
fingerprint_type: str = "ecfp",
fingerprint_radius: int = 2,
fingerprint_nbits: int = 2048,
compute_properties: Optional[List[str]] = None,
n_jobs: int = 1,
precomputed_distance_matrix: Optional[Union[ndarray, str, Path]] = None,
)
Analyzer for evaluating and comparing train/test split quality.
This class provides methods to compute comprehensive quality metrics for molecular dataset splits, including similarity distributions, scaffold overlap, and property distributions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
smiles
|
List[str]
|
List of SMILES strings for the full dataset. |
required |
fingerprint_type
|
str
|
Type of fingerprint to use for similarity computation. Options: "ecfp", "fcfp", "maccs", etc. Default: "ecfp". |
'ecfp'
|
fingerprint_radius
|
int
|
Radius for circular fingerprints. Default: 2. |
2
|
fingerprint_nbits
|
int
|
Number of bits for fingerprints. Default: 2048. |
2048
|
compute_properties
|
Optional[List[str]]
|
List of properties to compute. Default includes MW, LogP, TPSA, HBD, HBA. |
None
|
n_jobs
|
int
|
Number of parallel jobs. Default: 1. |
1
|
precomputed_distance_matrix
|
Optional[Union[ndarray, str, Path]]
|
Optional precomputed pairwise Jaccard
distance matrix of shape Caveats:
* The matrix rows/cols must be indexed in the same order as
|
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
smiles |
The input SMILES strings. |
|
n_molecules |
Number of molecules in the dataset. |
Example
analyzer = SplitAnalyzer(smiles)
Analyze a single split¶
report = analyzer.analyze_split(train_idx, test_idx, "scaffold") print(f"Mean similarity: {report.similarity_metrics.mean_sim:.3f}")
Compare multiple splitters¶
comparison = analyzer.compare_splitters(["scaffold", "kmeans", "random"]) print(comparison)
Reuse a precomputed Jaccard distance matrix to avoid recomputing¶
fingerprint-based similarity on every call:¶
analyzer = SplitAnalyzer( ... smiles, ... precomputed_distance_matrix="datasets/TDC/CYP2C9/Jaccard_distance.npy", ... )
scaffolds
property
¶
Lazily compute and cache Bemis-Murcko scaffolds.
analyze_split
¶
analyze_split(
train_idx: Union[List[int], ndarray],
test_idx: Union[List[int], ndarray],
splitter_name: str = "unknown",
split_index: int = 0,
labels: Optional[ArrayLike] = None,
compute_similarity: bool = True,
compute_scaffolds: bool = True,
compute_properties: bool = True,
) -> SplitQualityReport
Analyze a single train/test split and compute quality metrics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
train_idx
|
Union[List[int], ndarray]
|
Indices of training samples. |
required |
test_idx
|
Union[List[int], ndarray]
|
Indices of test samples. |
required |
splitter_name
|
str
|
Name of the splitting strategy used. |
'unknown'
|
split_index
|
int
|
Index of this split (for cross-validation). |
0
|
labels
|
Optional[ArrayLike]
|
Optional labels for computing balance metrics. |
None
|
compute_similarity
|
bool
|
Whether to compute similarity metrics. |
True
|
compute_scaffolds
|
bool
|
Whether to compute scaffold metrics. |
True
|
compute_properties
|
bool
|
Whether to compute property distributions. |
True
|
Returns:
| Type | Description |
|---|---|
SplitQualityReport
|
SplitQualityReport containing all computed metrics. |
Example
report = analyzer.analyze_split(train_idx, test_idx, "scaffold") print(f"Mean similarity: {report.similarity_metrics.mean_sim:.3f}")
analyze_splitter
¶
analyze_splitter(
splitter_name: str,
n_splits: int = 5,
test_size: float = 0.2,
labels: Optional[ArrayLike] = None,
**splitter_kwargs: Any,
) -> List[SplitQualityReport]
Analyze multiple splits from a single splitting strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
splitter_name
|
str
|
Name of the splitter to use (e.g., "scaffold", "kmeans"). |
required |
n_splits
|
int
|
Number of splits to analyze. |
5
|
test_size
|
float
|
Fraction of data for test set. |
0.2
|
labels
|
Optional[ArrayLike]
|
Optional labels for balance metrics. |
None
|
**splitter_kwargs
|
Any
|
Additional arguments passed to the splitter. |
{}
|
Returns:
| Type | Description |
|---|---|
List[SplitQualityReport]
|
List of SplitQualityReport, one for each split. |
Example
reports = analyzer.analyze_splitter("scaffold", n_splits=5) mean_sim = np.mean([r.similarity_metrics.mean_sim for r in reports])
compare_splitters
¶
compare_splitters(
splitter_names: List[str],
n_splits: int = 5,
test_size: float = 0.2,
labels: Optional[ArrayLike] = None,
aggregate: bool = True,
) -> DataFrame
Compare multiple splitting strategies on the same dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
splitter_names
|
List[str]
|
List of splitter names to compare. |
required |
n_splits
|
int
|
Number of splits per splitter. |
5
|
test_size
|
float
|
Fraction of data for test set. |
0.2
|
labels
|
Optional[ArrayLike]
|
Optional labels for balance metrics. |
None
|
aggregate
|
bool
|
If True, aggregate metrics across splits (mean ± std). |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with comparison metrics. If aggregate=True, shows mean |
DataFrame
|
values with standard deviations. Otherwise, shows all splits. |
Example
comparison = analyzer.compare_splitters(["scaffold", "kmeans", "random"]) print(comparison[["splitter_name", "sim_mean_sim", "scaffold_overlap_percentage"]])
get_summary_stats
¶
Get summary statistics from a list of reports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reports
|
List[SplitQualityReport]
|
List of SplitQualityReport objects. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Dict[str, float]]
|
Dictionary with summary statistics for key metrics. |
Command-Line Interface¶
The scripts/splitting.py tool provides a CLI for dataset splitting:
# List available splitters
python scripts/splitting.py --list-splitters
# Basic usage
python scripts/splitting.py -f data/molecules.csv -sp scaffold --save
# Run all splitters
python scripts/splitting.py -f data/molecules.csv -sp all --save
# Dry run (preview without saving)
python scripts/splitting.py -f data/molecules.csv -sp kmeans --dry-run
# Custom output directory
python scripts/splitting.py -f data/molecules.csv -sp scaffold --save -o results/
CLI Options¶
| Option | Description |
|---|---|
-f, --file_path |
Path to CSV/TSV file with SMILES column |
-sp, --splitter |
Splitter name or "all" for all splitters |
-te, --test_size |
Test set fraction (default: 0.2) |
-ns, --n_splits |
Number of splits to generate (default: 10) |
-o, --output_dir |
Custom output directory |
--save |
Save split files to disk |
--dry-run |
Preview operations without saving |
--list-splitters |
List available splitters and exit |