Transform

The scikit-learn glue: SMILES↔Mol conversion, feature unions, NaN handling, scaling, and a pipeline builder for the common case.

Everything here exists so that a QSAR workflow is an ordinary sklearn.pipeline.Pipeline — which means GridSearchCV, cross_val_score and clone all work without special cases.

A whole workflow in one object

>>> from qsarkit.models import QSARRegressor
>>> from qsarkit.representation import MorganFingerprint
>>> from qsarkit.transform import make_qsar_pipeline
>>> pipeline = make_qsar_pipeline(
...     MorganFingerprint(n_bits=64),
...     QSARRegressor("rf", random_state=0),
...     from_smiles=True,
... )
>>> [name for name, _ in pipeline.steps]
['smiles_to_mol', 'representation', 'nan', 'model']
>>> pipeline.fit(DEMO_SMILES, DEMO_Y).predict(DEMO_SMILES[:3]).shape
(3,)

Taking SMILES directly matters more than it looks: it puts structure parsing inside the cross-validation fold, so no preprocessing happens outside the loop where it could leak.

Conversion

>>> from qsarkit.transform import MolToSmiles, SmilesToMol
>>> mols = SmilesToMol().transform(["CCO", "c1ccccc1"])
>>> MolToSmiles().transform(mols)
['CCO', 'c1ccccc1']

Combining representations

>>> from qsarkit.representation import MACCSKeysFingerprint, PhysicochemicalDescriptors
>>> from qsarkit.transform import MoleculeFeatureUnion
>>> union = MoleculeFeatureUnion([
...     ("maccs", MACCSKeysFingerprint()),
...     ("physchem", PhysicochemicalDescriptors()),
... ])
>>> union.fit_transform(demo_mols).shape
(24, 176)

Unlike the individual transformers, the union requires fit before transform — it has to learn each branch’s output width to know where the blocks join.

Scaling and missing values

>>> import numpy as np
>>> from qsarkit.transform import DescriptorScaler, NaNHandler
>>> X = np.array([[1.0, np.nan], [3.0, 4.0], [5.0, 6.0]])
>>> np.isfinite(NaNHandler(strategy="median").fit_transform(X)).all()
np.True_
>>> scaled = DescriptorScaler(method="robust").fit_transform(X[1:])
>>> scaled.shape
(2, 2)

NaNHandler belongs before any estimator in a descriptor pipeline: RDKit emits NaN for undefined quantities (a 3D descriptor with no conformer, a ratio with a zero denominator), and most estimators refuse to fit on them.

Warning

Scale inside the pipeline, never on the full dataset beforehand. Fitting a scaler on all the data before splitting leaks the test set’s mean and variance into the transform.

API

scikit-learn glue for composing molecule transformers into pipelines.

Building preprocessing as a Pipeline rather than applying it up front is what keeps imputation and scaling fitted on training folds only – the leak that otherwise inflates cross-validated QSAR scores.

Examples

>>> from qsarkit.representation import MorganFingerprint
>>> from qsarkit.transform import make_qsar_pipeline
>>> from sklearn.ensemble import RandomForestRegressor
>>> pipe = make_qsar_pipeline(
...     MorganFingerprint(n_bits=64), RandomForestRegressor(n_estimators=5),
...     from_smiles=True,
... )
>>> _ = pipe.fit(["CCO", "CCN", "c1ccccc1"], [1.0, 2.0, 3.0])
>>> pipe.predict(["CCO"]).shape
(1,)

References

class qsarkit.transform.SmilesToMol(sanitize=True, on_error='none')[source]

Bases: BaseEstimator, TransformerMixin

Parse SMILES strings into RDKit molecules.

The entry point that lets a pipeline start from a raw SMILES column, so the whole workflow — parsing, featurizing, modeling — is one fitted object that can be pickled and reapplied.

Parameters:
  • sanitize (bool) – Sanitize on parse. Turning this off keeps structures RDKit would reject, which is occasionally useful for diagnostics but unsafe for modeling.

  • on_error (Literal['none', 'raise']) – "none" yields None for unparseable input, preserving positional alignment with y; "raise" stops at the first failure.

Examples

>>> from rdkit import Chem
>>> mols = SmilesToMol().transform(["CCO", "c1ccccc1"])
>>> Chem.MolToSmiles(mols[0])
'CCO'

References

fit(X, y=None)[source]

No-op; parsing is stateless.

Parameters:
Return type:

SmilesToMol

transform(X)[source]

Parse each SMILES string.

Parameters:

X (Iterable[str])

Returns:

None where parsing failed and on_error="none".

Return type:

List[Any]

class qsarkit.transform.MolToSmiles(isomeric=True, canonical=True)[source]

Bases: BaseEstimator, TransformerMixin

Serialize RDKit molecules back to canonical SMILES.

Parameters:
  • isomeric (bool) – Include stereochemistry.

  • canonical (bool) – Emit RDKit’s canonical form, so identical structures produce identical strings.

Examples

>>> from rdkit import Chem
>>> MolToSmiles().transform([Chem.MolFromSmiles("OCC")])
['CCO']

References

  • Weininger, D., Weininger, A. & Weininger, J. L. (1989). “SMILES 2. Algorithm for Generation of Unique SMILES Notation.” J. Chem. Inf. Comput. Sci., 29(2), 97-101. https://doi.org/10.1021/ci00062a008

fit(X, y=None)[source]

No-op; serialization is stateless.

Return type:

MolToSmiles

transform(X)[source]

Serialize each molecule.

Parameters:

X (Iterable[Any])

Returns:

None where the input molecule was None.

Return type:

List[Optional[str]]

class qsarkit.transform.MoleculeFeatureUnion(transformers)[source]

Bases: BaseEstimator, TransformerMixin

Concatenate several molecule transformers into one feature matrix.

scikit-learn’s own FeatureUnion cannot be used here because it validates X as a numeric array before the transformers run, which rejects a list of RDKit molecules outright. This does the same job while leaving the input untouched.

Parameters:

transformers (Sequence[Any]) – Molecule transformers, or (name, transformer) pairs.

Variables:

transformers (list) – The fitted transformers.

Examples

>>> from rdkit import Chem
>>> from qsarkit.representation import MorganFingerprint, MACCSKeysFingerprint
>>> union = MoleculeFeatureUnion([
...     MorganFingerprint(n_bits=64), MACCSKeysFingerprint(),
... ])
>>> union.fit_transform([Chem.MolFromSmiles("CCO")]).shape
(1, 231)

References

transformers_: List[Any]
fit(X, y=None)[source]

Fit every member on the same molecules.

Parameters:
Return type:

MoleculeFeatureUnion

transform(X)[source]

Horizontally stack every member’s output.

Parameters:

X (Iterable[Any])

Return type:

ndarray[tuple[Any, ...], dtype[double]]

get_feature_names_out(input_features=None)[source]

Concatenated feature names, prefixed by member.

Return type:

ndarray[tuple[Any, ...], dtype[str_]]

class qsarkit.transform.NaNHandler(strategy='median', fill_value=0.0, max_nan_fraction=0.5)[source]

Bases: BaseEstimator, TransformerMixin

Replace or drop non-finite descriptor values.

RDKit descriptors return NaN or inf for molecules where they are undefined — a logP contribution for an unparameterized element, a ring descriptor for an acyclic molecule. Left alone these propagate silently through scaling and crash the estimator much later, with an error that names neither the descriptor nor the molecule.

Parameters:
  • strategy (Literal['mean', 'median', 'constant', 'drop_columns']) – How to handle them. "drop_columns" removes any column containing a non-finite value, which is the honest choice when a descriptor is undefined for a whole class of molecule rather than a stray one.

  • fill_value (float) – Used by strategy="constant".

  • max_nan_fraction (float) – Columns with a greater fraction of non-finite values are dropped regardless of strategy — imputing most of a column invents data.

Variables:
  • statistics (ndarray) – Per-column fill values.

  • support (ndarray of bool) – Columns retained.

Examples

>>> import numpy as np
>>> X = np.array([[1.0, np.nan], [3.0, 2.0]])
>>> NaNHandler().fit_transform(X)
array([[1., 2.],
       [3., 2.]])

References

statistics_: ndarray[tuple[Any, ...], dtype[float64]]
support_: ndarray[tuple[Any, ...], dtype[bool]]
n_features_in_: int
fit(X, y=None)[source]

Learn per-column fill values and which columns to keep.

Parameters:
Return type:

NaNHandler

transform(X)[source]

Impute and drop columns as learned during fit.

Parameters:

X (Union[Buffer, _SupportsArray[dtype[Any]], _NestedSequence[_SupportsArray[dtype[Any]]], complex, bytes, str, _NestedSequence[complex | bytes | str]])

Return type:

ndarray[tuple[Any, ...], dtype[double]]

get_feature_names_out(input_features=None)[source]

Names of the retained columns.

Return type:

ndarray[tuple[Any, ...], dtype[str_]]

class qsarkit.transform.VarianceThresholdMol(threshold=0.0)[source]

Bases: BaseEstimator, TransformerMixin

Drop near-constant descriptor columns.

A descriptor that takes the same value for every molecule carries no information but still costs a parameter, and constant columns break correlation-based selection and scaling by producing zero variance.

Parameters:

threshold (float) – Columns with variance at or below this are dropped. The default removes exactly-constant columns.

Variables:
  • variances (ndarray) – Per-column variance.

  • support (ndarray of bool) – Columns retained.

Examples

>>> import numpy as np
>>> X = np.array([[1.0, 5.0], [2.0, 5.0], [3.0, 5.0]])
>>> VarianceThresholdMol().fit_transform(X).shape
(3, 1)

References

variances_: ndarray[tuple[Any, ...], dtype[float64]]
support_: ndarray[tuple[Any, ...], dtype[bool]]
n_features_in_: int
fit(X, y=None)[source]

Measure per-column variance.

Parameters:
Return type:

VarianceThresholdMol

transform(X)[source]

Keep only the columns that passed the variance threshold.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

get_support(indices=False)[source]

Mask (or indices) of the retained columns.

Return type:

ndarray[tuple[Any, ...], dtype[Any]]

class qsarkit.transform.DescriptorScaler(method='standard', clip=False)[source]

Bases: BaseEstimator, TransformerMixin

Standardize descriptors while keeping their names.

Descriptors span wildly different scales — molecular weight in the hundreds, Fsp3 in [0, 1] — so any distance- or penalty-based method (SVM, k-NN, ridge, PCA, most applicability domains) is dominated by whichever descriptor happens to have the largest units unless they are scaled first. Fingerprints, being already 0/1, should not be scaled.

Parameters:
  • method (Literal['standard', 'minmax', 'robust']) – "robust" centres on the median and scales by the IQR, which is the right choice when the descriptor distribution has the long tail typical of counts.

  • clip (bool) – Clip transformed values to the range seen during fit, preventing a single extreme test molecule from dominating downstream.

Variables:

scaler (sklearn scaler) – The fitted scikit-learn scaler.

Examples

>>> import numpy as np
>>> X = np.array([[1.0, 100.0], [2.0, 200.0], [3.0, 300.0]])
>>> scaled = DescriptorScaler().fit_transform(X)
>>> bool(np.allclose(scaled.mean(axis=0), 0.0))
True

References

scaler_: Any
n_features_in_: int
fit(X, y=None)[source]

Fit the underlying scaler.

Parameters:
Return type:

DescriptorScaler

transform(X)[source]

Scale the descriptors.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

inverse_transform(X)[source]

Map scaled values back to the original units.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

get_feature_names_out(input_features=None)[source]

Pass feature names through unchanged (scaling is column-wise).

Return type:

ndarray[tuple[Any, ...], dtype[str_]]

qsarkit.transform.make_qsar_pipeline(representation, model, scale=False, handle_nan=True, from_smiles=False)[source]

Assemble the standard QSAR pipeline.

Chains, in order: optional SMILES parsing, the representation, NaN handling, optional scaling, and the estimator. Building it as one Pipeline matters for more than tidiness — it is what keeps the imputation and scaling fitted on training folds only, which is exactly the leak that inflates cross-validated QSAR scores when preprocessing is done up-front on the whole dataset.

Parameters:
Return type:

Pipeline

Examples

>>> from qsarkit.representation import MorganFingerprint
>>> from sklearn.ensemble import RandomForestRegressor
>>> pipe = make_qsar_pipeline(
...     MorganFingerprint(n_bits=64), RandomForestRegressor(n_estimators=5),
...     from_smiles=True,
... )
>>> _ = pipe.fit(["CCO", "CCN", "c1ccccc1"], [1.0, 2.0, 3.0])
>>> pipe.predict(["CCO"]).shape
(1,)

References

References