Skip to content

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

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

get_splitter_names() -> List[str]

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

register_splitter(name: str, aliases: Optional[List[str]] = None)

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 train_min_frac/test_min_frac (the similarity-cut constraints), so this value is not used.

None
train_size Optional[Union[float, int]]

Accepted for API symmetry; see test_size. Not used.

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 (max_clusters/min_cluster_size), so this value is not used.

None
train_size Optional[Union[float, int]]

Accepted for API symmetry; see test_size. Not used.

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 (n_molecules, n_molecules), supplied either as an np.ndarray or as a path to a .npy file. When provided, similarity metrics use similarity = 1 - distance sliced from this matrix instead of recomputing fingerprints — a large speed-up for repeated analyses on the same dataset.

Caveats: * The matrix rows/cols must be indexed in the same order as smiles; the caller is responsible for alignment. * Only the train/test similarity path uses it. Scaffold and property metrics still derive from the SMILES via RDKit. * fingerprint_type/fingerprint_radius/fingerprint_nbits are ignored when this matrix is supplied — the matrix encodes whatever fingerprint was used at generation time.

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", ... )

fingerprints property

fingerprints: ndarray

Lazily compute and cache molecular fingerprints.

scaffolds property

scaffolds: List[Optional[str]]

Lazily compute and cache Bemis-Murcko scaffolds.

properties property

properties: DataFrame

Lazily compute and cache molecular properties.

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_stats(
    reports: List[SplitQualityReport],
) -> Dict[str, Dict[str, float]]

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