Base

Shared estimator base classes, the exception hierarchy, and the lazy-import helper that keeps heavy optional dependencies out of import qsarkit.

Writing a transformer

The contract is one method. Implement _transform on a list of molecules, and inherit input validation, fit, fit_transform and full scikit-learn compatibility:

>>> import numpy as np
>>> from rdkit import Chem
>>> from qsarkit.base import MoleculeTransformer
>>> class HeavyAtomCount(MoleculeTransformer):
...     def _transform(self, mols):
...         return np.array([[m.GetNumHeavyAtoms()] for m in mols], dtype=float)
>>> HeavyAtomCount().fit_transform(demo_mols).shape
(24, 1)

It is a real estimator, so it composes and clones:

>>> from sklearn.base import clone
>>> clone(HeavyAtomCount())
HeavyAtomCount()

Three bases, by what they return

MoleculeTransformer

Molecules in, feature matrix out.

MoleculeToMoleculeTransformer

Molecules in, molecules out — standardization and curation. The separate type is what lets these chain with each other.

FittableMoleculeTransformer

Adds an is_fitted flag and a guard, so calling transform before fit raises rather than producing meaningless features:

>>> from qsarkit.base import FittableMoleculeTransformer
>>> class MeanCentredSize(FittableMoleculeTransformer):
...     def fit(self, mols, y=None):
...         self.mean_ = np.mean([m.GetNumHeavyAtoms() for m in mols])
...         self._is_fitted = True
...         return self
...     def _transform(self, mols):
...         self._check_is_fitted()
...         return np.array([m.GetNumHeavyAtoms() - self.mean_ for m in mols])
>>> MeanCentredSize().transform(demo_mols)
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.ModelNotFittedError: MeanCentredSize must be fitted...

Input validation

>>> from qsarkit.base import ensure_mol_list
>>> ensure_mol_list([None])
[None]

None passes through deliberately: a molecule that failed an earlier parsing step must keep its position, because dropping it here would silently shift every downstream label by one. Anything that is neither a molecule nor None is a programming error and is reported as one:

>>> ensure_mol_list(["CCO"])
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.InvalidMoleculeError: Element 0 is not an rdkit.Chem.Mol...

Optional dependencies

>>> from qsarkit.base import require
>>> require("numpy").__name__
'numpy'

A missing dependency raises an error naming the extra that provides it, rather than an ImportError the user has to interpret:

>>> require("nonexistent_package_xyz")
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.OptionalDependencyError: This feature requires...

API

Shared base classes, exceptions and helpers used across all of qsarkit.

exception qsarkit.base.QsarkitError[source]

Bases: Exception

Base class for all qsarkit exceptions.

exception qsarkit.base.InvalidMoleculeError[source]

Bases: QsarkitError

Raised when an input cannot be parsed or sanitized into an RDKit Mol.

exception qsarkit.base.ModelNotFittedError[source]

Bases: QsarkitError

Raised when .transform/.predict is called before .fit.

exception qsarkit.base.OptionalDependencyError(package, extra=None)[source]

Bases: QsarkitError

Raised when an optional dependency required by a feature is missing.

qsarkit.base.require(module_name)[source]

Import and return module_name, raising a helpful error if absent.

Parameters:

module_name (str) – Fully qualified module name, e.g. "torch" or "pdfminer.high_level".

Returns:

The imported module object.

Return type:

Any

Raises:

OptionalDependencyError – If the module is not installed. The error message names the pip extra (qsarkit-learn[extra]) that installs it.

Examples

>>> from qsarkit.base import require
>>> require("numpy").__name__
'numpy'

A missing dependency raises an error naming the extra that provides it, rather than an ImportError the caller has to interpret:

>>> require("nonexistent_package_xyz")
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.OptionalDependencyError: This feature requires...

Call it inside __init__ or fit, never at module import time – that is what keeps import qsarkit from pulling in PyTorch.

class qsarkit.base.MoleculeTransformer[source]

Bases: BaseEstimator, TransformerMixin, ABC

Abstract base for stateless/stateful molecule -> X transformers.

Subclasses implement _transform() and operate on Iterable[rdkit.Chem.Mol]. fit is a no-op by default (most chemistry transformers are stateless), but subclasses that need to learn parameters from data (e.g. a fingerprint vocabulary or a Mol2Vec embedding model) should override it.

Examples

See the module docstring for a complete subclass. Input validation is inherited, so a subclass never has to check its own arguments:

>>> import numpy as np
>>> from qsarkit.base import MoleculeTransformer
>>> class RingCount(MoleculeTransformer):
...     def _transform(self, mols):
...         return np.array([m.GetRingInfo().NumRings() for m in mols])
>>> RingCount().transform(["not a molecule"])
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.InvalidMoleculeError: Element 0 is not an rdkit.Chem.Mol...
fit(mols, y=None)[source]

Default no-op fit. Override in stateful subclasses.

Return type:

MoleculeTransformer

transform(mols)[source]

Validate input and dispatch to _transform().

Return type:

Any

set_fit_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the fit method.

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 (see sklearn.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 to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in fit.

Returns:

self – The updated object.

Return type:

object

set_transform_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the transform method.

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 (see sklearn.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 to transform if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to transform.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in transform.

Returns:

self – The updated object.

Return type:

object

class qsarkit.base.MoleculeToMoleculeTransformer[source]

Bases: MoleculeTransformer, ABC

Base for transformers that map Mol -> Mol (standardization, curation, …).

Identical to MoleculeTransformer in behaviour; the separate type documents that transform returns molecules rather than a feature matrix, so these can be chained with each other.

Examples

>>> from rdkit import Chem
>>> from qsarkit.base import MoleculeToMoleculeTransformer
>>> class StripStereo(MoleculeToMoleculeTransformer):
...     def _transform(self, mols):
...         out = []
...         for m in mols:
...             copy = Chem.Mol(m)
...             Chem.RemoveStereochemistry(copy)
...             out.append(copy)
...         return out
>>> mol = Chem.MolFromSmiles("C[C@H](N)C(=O)O")
>>> Chem.MolToSmiles(StripStereo().transform([mol])[0])
'CC(N)C(=O)O'
set_fit_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the fit method.

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 (see sklearn.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 to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in fit.

Returns:

self – The updated object.

Return type:

object

set_transform_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the transform method.

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 (see sklearn.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 to transform if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to transform.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in transform.

Returns:

self – The updated object.

Return type:

object

class qsarkit.base.FittableMoleculeTransformer[source]

Bases: MoleculeTransformer, ABC

Base for transformers with learned state (embeddings, vocabularies).

Adds an is_fitted flag and a _check_is_fitted guard, so calling transform before fit raises a clear error instead of producing silently meaningless features.

Examples

>>> import numpy as np
>>> from rdkit import Chem
>>> from qsarkit.base import FittableMoleculeTransformer
>>> class MeanCentredSize(FittableMoleculeTransformer):
...     def fit(self, mols, y=None):
...         self.mean_ = np.mean([m.GetNumHeavyAtoms() for m in mols])
...         self._is_fitted = True
...         return self
...     def _transform(self, mols):
...         self._check_is_fitted()
...         return np.array([m.GetNumHeavyAtoms() - self.mean_ for m in mols])
>>> mols = [Chem.MolFromSmiles(s) for s in ("CCO", "c1ccccc1")]
>>> transformer = MeanCentredSize()
>>> transformer.is_fitted
False
>>> transformer.transform(mols)
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.ModelNotFittedError: MeanCentredSize must be fitted...
>>> transformer.fit(mols).transform(mols).tolist()
[-1.5, 1.5]
property is_fitted: bool
set_fit_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the fit method.

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 (see sklearn.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 to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in fit.

Returns:

self – The updated object.

Return type:

object

set_transform_request(*, mols='$UNCHANGED$')

Configure whether metadata should be requested to be passed to the transform method.

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 (see sklearn.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 to transform if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to transform.

  • 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.

Parameters:

mols (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for mols parameter in transform.

Returns:

self – The updated object.

Return type:

object

qsarkit.base.ensure_mol_list(mols)[source]

Materialize an Iterable[Mol] into a list, validating entries.

Parameters:

mols (Iterable[Any]) – Iterable of rdkit.Chem.Mol objects. None entries are allowed through (representing molecules that failed an earlier parsing step) and are left untouched so callers can decide how to handle them positionally.

Returns:

The materialized list.

Return type:

List[Any]

Raises:

InvalidMoleculeError – If an entry is neither None nor an RDKit Mol.

Examples

>>> from rdkit import Chem
>>> from qsarkit.base import ensure_mol_list
>>> len(ensure_mol_list(Chem.MolFromSmiles(s) for s in ("CCO", "CCN")))
2

None is allowed through, because a molecule that failed an earlier parsing step must keep its position – dropping it here would silently shift every downstream label by one:

>>> ensure_mol_list([None])
[None]

Anything else is a programming error and is reported as one:

>>> ensure_mol_list(["CCO"])
Traceback (most recent call last):
    ...
qsarkit.base.exceptions.InvalidMoleculeError: Element 0 is not an rdkit.Chem.Mol...

References

  • Pedregosa, F. et al. (2011). “Scikit-learn: Machine Learning in Python.” J. Mach. Learn. Res., 12, 2825-2830. https://jmlr.org/papers/v12/pedregosa11a.html

  • Buitinck, L. et al. (2013). “API Design for Machine Learning Software: Experiences from the scikit-learn Project.” ECML PKDD Workshop. arXiv:1309.0238