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
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
Cawley, G. C. & Talbot, N. L. C. (2010). “On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation.” J. Mach. Learn. Res., 11, 2079-2107. https://jmlr.org/papers/v11/cawley10a.html
- class qsarkit.transform.SmilesToMol(sanitize=True, on_error='none')[source]¶
Bases:
BaseEstimator,TransformerMixinParse 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"yieldsNonefor unparseable input, preserving positional alignment withy;"raise"stops at the first failure.
Examples
>>> from rdkit import Chem >>> mols = SmilesToMol().transform(["CCO", "c1ccccc1"]) >>> Chem.MolToSmiles(mols[0]) 'CCO'
References
Weininger, D. (1988). “SMILES, a Chemical Language and Information System.” J. Chem. Inf. Comput. Sci., 28(1), 31-36. https://doi.org/10.1021/ci00057a005
RDKit documentation: https://www.rdkit.org/docs/
- class qsarkit.transform.MolToSmiles(isomeric=True, canonical=True)[source]¶
Bases:
BaseEstimator,TransformerMixinSerialize RDKit molecules back to canonical SMILES.
- Parameters:
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
- class qsarkit.transform.MoleculeFeatureUnion(transformers)[source]¶
Bases:
BaseEstimator,TransformerMixinConcatenate several molecule transformers into one feature matrix.
scikit-learn’s own
FeatureUnioncannot be used here because it validatesXas 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
Pedregosa, F. et al. (2011). “Scikit-learn.” J. Mach. Learn. Res., 12, 2825-2830. https://jmlr.org/papers/v12/pedregosa11a.html
- class qsarkit.transform.NaNHandler(strategy='median', fill_value=0.0, max_nan_fraction=0.5)[source]¶
Bases:
BaseEstimator,TransformerMixinReplace or drop non-finite descriptor values.
RDKit descriptors return
NaNorinffor 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 bystrategy="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 (
ndarrayofbool) – 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
Little, R. J. A. & Rubin, D. B. (2019). “Statistical Analysis with Missing Data,” 3rd ed. Wiley. https://doi.org/10.1002/9781119482260
scikit-learn imputation documentation: https://scikit-learn.org/stable/modules/impute.html
- fit(X, y=None)[source]¶
Learn per-column fill values and which columns to keep.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None])
- Return type:
- class qsarkit.transform.VarianceThresholdMol(threshold=0.0)[source]¶
Bases:
BaseEstimator,TransformerMixinDrop 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 (
ndarrayofbool) – 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
scikit-learn feature selection documentation: https://scikit-learn.org/stable/modules/feature_selection.html
- fit(X, y=None)[source]¶
Measure per-column variance.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None])
- Return type:
- class qsarkit.transform.DescriptorScaler(method='standard', clip=False)[source]¶
Bases:
BaseEstimator,TransformerMixinStandardize 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
scikit-learn preprocessing documentation: https://scikit-learn.org/stable/modules/preprocessing.html
Todeschini, R. & Consonni, V. (2009). “Molecular Descriptors for Chemoinformatics.” Wiley-VCH. https://doi.org/10.1002/9783527628766
- fit(X, y=None)[source]¶
Fit the underlying scaler.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None])
- Return type:
- 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
Pipelinematters 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:
representation (
Any) – A molecule featurizer fromqsarkit.representation.model (
Any) – The final regressor or classifier.scale (
bool) – Insert aDescriptorScaler. Leave off for fingerprints, which are already 0/1; turn on for descriptors.handle_nan (
bool) – Insert aNaNHandler.from_smiles (
bool) – Prepend aSmilesToMolso the pipeline accepts SMILES.
- Return type:
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
Pedregosa, F. et al. (2011). “Scikit-learn.” J. Mach. Learn. Res., 12, 2825-2830. https://jmlr.org/papers/v12/pedregosa11a.html
Cawley, G. C. & Talbot, N. L. C. (2010). “On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation.” J. Mach. Learn. Res., 11, 2079-2107. https://jmlr.org/papers/v11/cawley10a.html
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.” ECML PKDD Workshop. arXiv:1309.0238