From 96daff0ded90920e83c84cb242319a4f7a88d081 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Fri, 22 Nov 2024 11:11:15 +0100 Subject: [PATCH 01/24] Added draft support for multi-output prediction and AD estimation --- scikit_mol/adapters.py | 175 +++++++++++++++++++++++ scikit_mol/applicability.py | 277 ++++++++++++++++++++++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 scikit_mol/adapters.py create mode 100644 scikit_mol/applicability.py diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py new file mode 100644 index 0000000..e5451f9 --- /dev/null +++ b/scikit_mol/adapters.py @@ -0,0 +1,175 @@ +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin, OneToOneFeatureMixin +from sklearn.utils._pprint import _EstimatorPrettyPrinter +from sklearn.utils._param_validation import validate_params +from sklearn.utils.metaestimators import available_if +from sklearn.utils.validation import check_is_fitted, check_array +from sklearn.utils._estimator_html_repr import _VisualBlock +from sklearn.utils._set_output import _safe_set_output +from scipy.stats import chi2 + + +class EstimatorUnion(BaseEstimator, TransformerMixin): + def __init__(self, estimators): + self.estimators = estimators + + def fit(self, X, y=None): + self.fitted_estimators_ = [] + for name, estimator in self.estimators: + if hasattr(estimator, "fit"): + fitted_estimator = estimator.fit(X, y) + self.fitted_estimators_.append((name, fitted_estimator)) + else: + self.fitted_estimators_.append((name, estimator)) + return self + + def transform(self, X): + check_is_fitted(self) + results = [] + for name, estimator in self.fitted_estimators_: + if hasattr(estimator, "predict"): + results.append(estimator.predict(X)) + elif hasattr(estimator, "transform"): + results.append(estimator.transform(X)) + return np.column_stack(results) + + def predict(self, X): + return self.transform(X) + + @available_if(lambda self: hasattr(self, "fitted_estimators_")) + def get_feature_names_out(self, input_features=None): + feature_names = [] + for name, estimator in self.fitted_estimators_: + if hasattr(estimator, "get_feature_names_out"): + feature_names.extend(estimator.get_feature_names_out()) + else: + feature_names.append(name) + return np.array(feature_names) + + def set_output(self, *, transform=None): + """Set output container for all estimators. + + Parameters + ---------- + transform : {"default", "pandas"}, default=None + Configure output of `transform` and `fit_transform`. + + Returns + ------- + self : estimator instance + Estimator instance. + """ + for _, estimator in self.estimators: + _safe_set_output(estimator, transform=transform) + return super().set_output(transform=transform) + + def __repr__(self): + class_name = self.__class__.__name__ + estimator_reprs = [] + for name, estimator in self.estimators: + estimator_repr = f"{name}={estimator.__repr__()}" + estimator_reprs.append(estimator_repr) + estimators_str = ",\n".join(estimator_reprs) + return f"{class_name}([\n{estimators_str}\n])" + + def _sk_visual_block_(self): + names, transformers = zip(*self.estimators) + return _VisualBlock("parallel", transformers, names=names) + + +class SigmoidThresholdTransformer(BaseEstimator, TransformerMixin): + def __init__(self, threshold, steepness=1, feature_name="Sigmoid_", prefix=True): + self.threshold = threshold + self.steepness = steepness + self.feature_name = feature_name + self.prefix = prefix + + def fit(self, X, y=None): + return self + + def transform(self, X): + return 1 / (1 + np.exp(self.steepness * (X - self.threshold))) + + def predict(self, X): + return self.transform(X) + + @available_if(lambda self: hasattr(self, "fitted_estimators_")) + def get_feature_names_out(self, input_features=None): + check_is_fitted(self) + + if input_features is None: + if ( + hasattr(self, "feature_names_in_") + and self.feature_names_in_ is not None + ): + input_features = self.feature_names_in_ + else: + input_features = [f"x{i}" for i in range(self.n_features_in_)] + + if self.feature_name: + if self.prefix: + return np.array( + [f"{self.feature_name}{feature}" for feature in input_features] + ) + else: + if len(input_features) > 1: + return np.array( + [f"{self.feature_name}{i}" for i in range(len(input_features))] + ) + else: + return np.array([self.feature_name]) + else: + return np.array(input_features) + + +class NullEstimator(BaseEstimator, TransformerMixin, OneToOneFeatureMixin): + def __init__( + self, + accept_sparse=False, + ): + self.accept_sparse = accept_sparse + + def fit(self, X, y=None): + # Check and store the input + self.X_ = check_array( + X, accept_sparse=self.accept_sparse, force_all_finite="allow-nan" + ) + self.n_features_in_ = self.X_.shape[1] + self.feature_names_in_ = getattr(X, "columns", None) + return self + + def transform(self, X): + check_is_fitted(self) + X = check_array( + X, accept_sparse=self.accept_sparse, force_all_finite="allow-nan" + ) + + # Check that the input is of the same shape as the one passed during fit. + if X.shape[1] != self.n_features_in_: + raise ValueError( + f"Shape of input is different from what was seen in `fit`" + f" Expected {self.n_features_in_} features, got {X.shape[1]}" + ) + return X + + def predict(self, X): + return self.transform(X) + + @available_if(lambda self: hasattr(self, "fitted_estimators_")) + def get_feature_names_out(self, input_features=None): + check_is_fitted(self) + + # Do I need to heck that the size of input_features is correct? + # if len(input_features) != self.n_features_out_: + # raise ValueError(f"Expected {self.n_features_in_} features, got {len(input_features)}") + + if input_features: + return input_features + else: + return np.array([f"x{i}" for i in range(self.n_features_in_)]) + + def _more_tags(self): + return { + "allow_nan": True, + "X_types": ["2darray"] + (["sparse"] if self.accept_sparse else []), + } diff --git a/scikit_mol/applicability.py b/scikit_mol/applicability.py new file mode 100644 index 0000000..8cfda1b --- /dev/null +++ b/scikit_mol/applicability.py @@ -0,0 +1,277 @@ +import numpy as np +from scipy.sparse import csr_matrix +from sklearn.neighbors import NearestNeighbors +from sklearn.base import BaseEstimator, TransformerMixin, check_array, check_is_fitted +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_is_fitted, check_array +import numpy as np +from scipy import linalg + + +class NearestNeighborsDistance(BaseEstimator, TransformerMixin): + def __init__(self, n_neighbors=1): + self.n_neighbors = n_neighbors + self.feature_name = "nn_distance" + + def fit(self, X, y=None): + self.X_sparse = csr_matrix(X) + self.nn = NearestNeighbors(n_neighbors=self.n_neighbors, metric="cosine") + self.nn.fit(self.X_sparse) + return self + + def transform(self, X): + X_sparse = csr_matrix(X) + distances, _ = self.nn.kneighbors(X_sparse) + avg_distances = np.mean(distances, axis=1) + return avg_distances.reshape(-1, 1) # Return 2D array for consistency + + def predict(self, X): + return self.transform(X) + + def get_feature_names_out(self, input_features=None): + return np.array([self.feature_name]) + + +class LeverageDistanceSlow(BaseEstimator, TransformerMixin): + """Calculate leverage-based distances for applicability domain assessment. + + The leverage approach measures how far a sample is from the center of the + X variable space. It's based on the hat matrix H = X(X'X)^(-1)X'. + + Parameters + ---------- + threshold_factor : float, default=3 + Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n + where p is the number of features and n is the number of samples. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + X_fit_ : ndarray + Training data used in fit. + leverage_threshold_ : float + Calculated leverage threshold (h*). + """ + + def __init__(self, threshold_factor=3): + self.threshold_factor = threshold_factor + + def fit(self, X, y=None): + """Fit the model using X as training data. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X, accept_sparse=False) + self.n_features_in_ = X.shape[1] + self.X_fit_ = X + + # Calculate leverage threshold h* + n_samples = X.shape[0] + self.leverage_threshold_ = ( + self.threshold_factor * (self.n_features_in_ + 1) / n_samples + ) + + # Store (X'X)^(-1) for later use + self.xtx_inv_ = np.linalg.inv(X.T @ X) + + return self + + def transform(self, X): + """Calculate leverage-based distances for X. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to calculate leverage distances for. + + Returns + ------- + h : ndarray of shape (n_samples, 1) + The leverage values for each sample. + """ + check_is_fitted(self) + X = check_array(X, accept_sparse=False) + + if X.shape[1] != self.n_features_in_: + raise ValueError( + f"X has {X.shape[1]} features, but LeverageDistance " + f"was fitted with {self.n_features_in_} features." + ) + + # Calculate leverage values h = diag(X(X'X)^(-1)X') + # Slighlty different implementation (from another package) + # hat_matrix = X @ self.xtx_inv_ @ X.T + # leverages = np.diag(hat_matrix) + + h = np.sum(X @ self.xtx_inv_ * X, axis=1) + + return h.reshape(-1, 1) + + def predict(self, X): + """Alias for transform, following scikit-learn conventions.""" + return self.transform(X) + + def get_feature_names_out(self, input_features=None): + """Get output feature names. + + Parameters + ---------- + input_features : None + Ignored as the transformer generates new feature names. + + Returns + ------- + feature_names_out : ndarray of str objects + Leverage distance feature name. + """ + check_is_fitted(self) + return np.array(["leverage_distance"]) + + +# Faster but gives some _very_ large distances for some compounds! +class LeverageDistance(BaseEstimator, TransformerMixin): + """Calculate leverage-based distances for applicability domain assessment. + + Parameters + ---------- + threshold_factor : float, default=3 + Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n + """ + + def __init__(self, threshold_factor=3): + self.threshold_factor = threshold_factor + + def fit(self, X, y=None): + X = check_array(X, accept_sparse=False) + self.n_features_in_ = X.shape[1] + n_samples = X.shape[0] + + # Calculate leverage threshold h* + self.leverage_threshold_ = ( + self.threshold_factor * (self.n_features_in_ + 1) / n_samples + ) + + # Use more efficient matrix operations + # Calculate (X'X)^(-1) using SVD which is more stable + U, s, Vh = linalg.svd(X, full_matrices=False) + + # Store components for faster transform + self.s_inv_ = 1 / s + self.U_ = U + self.Vh_ = Vh + + return self + + def transform(self, X): + check_is_fitted(self) + X = check_array(X, accept_sparse=False) + + if X.shape[1] != self.n_features_in_: + raise ValueError( + f"X has {X.shape[1]} features, but LeverageDistance " + f"was fitted with {self.n_features_in_} features." + ) + + # Efficient leverage calculation using stored SVD components + # This avoids explicit matrix inversion + Z = X @ self.Vh_.T * self.s_inv_ + h = np.sum(Z * Z, axis=1) + + return h.reshape(-1, 1) + + def predict(self, X): + return self.transform(X) + + def get_feature_names_out(self, input_features=None): + check_is_fitted(self) + return np.array(["leverage_distance"]) + + +class MahalanobisDistance(BaseEstimator, TransformerMixin): + """Calculate Mahalanobis distances for applicability domain assessment. + + Parameters + ---------- + threshold_quantile : float, default=0.975 + Quantile of chi-square distribution to use as threshold. + threshold_strategy : str, default='chi2' + Strategy to compute threshold. Options: + - 'chi2': Use chi-square distribution (theoretical) + - 'empirical': Use empirical distribution from training data + - None: Don't compute threshold (useful for CV) + """ + + def __init__(self, threshold_quantile=0.975, threshold_strategy="chi2"): + self.threshold_quantile = threshold_quantile + self.threshold_strategy = threshold_strategy + + def fit(self, X, y=None): + X = check_array(X) + self.n_features_in_ = X.shape[1] + + # Compute mean and covariance + self.mean_ = np.mean(X, axis=0) + self.covariance_ = np.cov(X, rowvar=False) + self.inv_covariance_ = np.linalg.inv(self.covariance_) + + # Calculate distances for training set + train_distances = self._mahalanobis(X) + self.train_distances_ = train_distances + + # Set threshold based on strategy + if self.threshold_strategy == "chi2": + self.threshold_ = chi2.ppf(self.threshold_quantile, df=self.n_features_in_) + elif self.threshold_strategy == "empirical": + self.threshold_ = np.quantile(train_distances, self.threshold_quantile) + elif self.threshold_strategy is None: + self.threshold_ = None + else: + raise ValueError(f"Unknown threshold_strategy: {self.threshold_strategy}") + + return self + + def _mahalanobis(self, X): + """Calculate Mahalanobis distances.""" + X_centered = X - self.mean_ + return np.sqrt(np.sum(X_centered @ self.inv_covariance_ * X_centered, axis=1)) + + def transform(self, X): + check_is_fitted(self) + X = check_array(X) + + if X.shape[1] != self.n_features_in_: + raise ValueError( + f"X has {X.shape[1]} features, but {self.__class__.__name__} " + f"was fitted with {self.n_features_in_} features." + ) + + distances = self._mahalanobis(X) + return distances.reshape(-1, 1) + + def set_threshold(self, threshold): + """Set threshold manually, e.g., from cross-validation.""" + self.threshold_ = threshold + return self + + def get_feature_names_out(self, input_features=None): + check_is_fitted(self) + return np.array(["mahalanobis_distance"]) + + def _more_tags(self): + return { + "requires_fit": True, + "X_types": ["2darray"], + "poor_score": False, + "allow_nan": False, + } From 83cde658225718f848cd21844ab716954c2ed206 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Thu, 9 Jan 2025 16:03:52 +0100 Subject: [PATCH 02/24] Fixed type on Readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c6255f..72117ed 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ The first draft for the project was created at the [RDKIT UGM 2022 hackathon](ht - Standardizer
-- safeinference + +- Safeinference - SafeInferenceWrapper - set_safe_inference_mode From 7c48055e6b5e0cc79c8e1d35939d37df1201aeb0 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Feb 2025 08:47:54 +0100 Subject: [PATCH 03/24] Added AD domain estimators from MLChemAD, need some work on consistent API --- scikit_mol/applicability/LICENSE.MIT | 19 ++ scikit_mol/applicability/README.md | 22 +++ scikit_mol/applicability/__init__.py | 25 +++ scikit_mol/applicability/bounding_box.py | 139 ++++++++++++++ scikit_mol/applicability/convex_hull.py | 113 ++++++++++++ scikit_mol/applicability/hotelling.py | 169 ++++++++++++++++++ scikit_mol/applicability/isolation_forest.py | 154 ++++++++++++++++ scikit_mol/applicability/kernel_density.py | 143 +++++++++++++++ scikit_mol/applicability/knn.py | 150 ++++++++++++++++ scikit_mol/applicability/leverage.py | 103 +++++++++++ scikit_mol/applicability/local_outlier.py | 119 ++++++++++++ scikit_mol/applicability/mahalanobis.py | 158 ++++++++++++++++ scikit_mol/applicability/standardization.py | 165 +++++++++++++++++ scikit_mol/applicability/topkat.py | 148 +++++++++++++++ ...{applicability.py => applicability_old.py} | 11 +- 15 files changed, 1632 insertions(+), 6 deletions(-) create mode 100644 scikit_mol/applicability/LICENSE.MIT create mode 100644 scikit_mol/applicability/README.md create mode 100644 scikit_mol/applicability/__init__.py create mode 100644 scikit_mol/applicability/bounding_box.py create mode 100644 scikit_mol/applicability/convex_hull.py create mode 100644 scikit_mol/applicability/hotelling.py create mode 100644 scikit_mol/applicability/isolation_forest.py create mode 100644 scikit_mol/applicability/kernel_density.py create mode 100644 scikit_mol/applicability/knn.py create mode 100644 scikit_mol/applicability/leverage.py create mode 100644 scikit_mol/applicability/local_outlier.py create mode 100644 scikit_mol/applicability/mahalanobis.py create mode 100644 scikit_mol/applicability/standardization.py create mode 100644 scikit_mol/applicability/topkat.py rename scikit_mol/{applicability.py => applicability_old.py} (97%) diff --git a/scikit_mol/applicability/LICENSE.MIT b/scikit_mol/applicability/LICENSE.MIT new file mode 100644 index 0000000..5d7ac46 --- /dev/null +++ b/scikit_mol/applicability/LICENSE.MIT @@ -0,0 +1,19 @@ +Copyright (c) 2023 Olivier J. M. Béquignon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/scikit_mol/applicability/README.md b/scikit_mol/applicability/README.md new file mode 100644 index 0000000..3fd2bfb --- /dev/null +++ b/scikit_mol/applicability/README.md @@ -0,0 +1,22 @@ +# Applicability Domain Estimators + +This module contains applicability domain estimators for chemical modeling. + +## License Information + +Files in this module are licensed under LGPL as part of scikit-mol, with some files containing code adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD). + +- Files containing the following header are adapted from MLChemAD (originally MIT licensed): + + ```python + """ + This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) + Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) + Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) + See LICENSE.MIT in this directory for the original MIT license. + """ + ``` + +- All other files are original implementations under scikit-mol's GPL/LGPL license. + +The original MLChemAD MIT license is preserved in LICENSE.MIT for reference. diff --git a/scikit_mol/applicability/__init__.py b/scikit_mol/applicability/__init__.py new file mode 100644 index 0000000..003c3be --- /dev/null +++ b/scikit_mol/applicability/__init__.py @@ -0,0 +1,25 @@ +from .bounding_box import BoundingBoxApplicabilityDomain +from .convex_hull import ConvexHullApplicabilityDomain +from .hotelling import HotellingT2ApplicabilityDomain +from .isolation_forest import IsolationForestApplicabilityDomain +from .kernel_density import KernelDensityApplicabilityDomain +from .knn import KNNApplicabilityDomain +from .leverage import LeverageApplicabilityDomain +from .local_outlier import LocalOutlierFactorApplicabilityDomain +from .mahalanobis import MahalanobisApplicabilityDomain +from .standardization import StandardizationApplicabilityDomain +from .topkat import TopkatApplicabilityDomain + +__all__ = [ + "BoundingBoxApplicabilityDomain", + "ConvexHullApplicabilityDomain", + "HotellingT2ApplicabilityDomain", + "IsolationForestApplicabilityDomain", + "KNNApplicabilityDomain", + "KernelDensityApplicabilityDomain", + "LeverageApplicabilityDomain", + "LocalOutlierFactorApplicabilityDomain", + "MahalanobisApplicabilityDomain", + "StandardizationApplicabilityDomain", + "TopkatApplicabilityDomain", +] diff --git a/scikit_mol/applicability/bounding_box.py b/scikit_mol/applicability/bounding_box.py new file mode 100644 index 0000000..4c946fe --- /dev/null +++ b/scikit_mol/applicability/bounding_box.py @@ -0,0 +1,139 @@ +""" +Bounding box applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class BoundingBoxApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain defined by feature value ranges. + + Samples falling outside the allowed range for any feature are considered + outside the domain. + + Parameters + ---------- + percentile : float or tuple of float, default=(0.1, 99.9) + Percentile(s) of the training set distribution used to define + the bounding box. If float, uses (percentile, 100-percentile). + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + min_ : ndarray of shape (n_features,) + Minimum allowed value for each feature. + max_ : ndarray of shape (n_features,) + Maximum allowed value for each feature. + + Examples + -------- + >>> from sklearn.pipeline import make_pipeline + >>> from sklearn.preprocessing import StandardScaler + >>> from sklearn.decomposition import PCA + >>> from scikit_mol.applicability import BoundingBoxApplicabilityDomain + + Basic usage: + >>> ad = BoundingBoxApplicabilityDomain(percentile=1) + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + + With preprocessing: + >>> pipe = make_pipeline( + ... StandardScaler(), + ... BoundingBoxApplicabilityDomain(percentile=1) + ... ) + >>> pipe.fit(X_train) + >>> predictions = pipe.predict(X_test) + + With PCA preprocessing: + >>> pipe = make_pipeline( + ... StandardScaler(), + ... PCA(n_components=0.9), + ... BoundingBoxApplicabilityDomain(percentile=1) + ... ) + >>> pipe.fit(X_train) + >>> predictions = pipe.predict(X_test) + """ + + def __init__(self, percentile=(0.1, 99.9)): + if isinstance(percentile, (int, float)): + if not 0 <= percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + self.percentile = (percentile, 100 - percentile) + else: + if not all(0 <= p <= 100 for p in percentile): + raise ValueError("percentiles must be between 0 and 100") + if len(percentile) != 2: + raise ValueError("percentile must be a float or tuple of 2 floats") + if percentile[0] >= percentile[1]: + raise ValueError("first percentile must be less than second") + self.percentile = percentile + + def fit(self, X, y=None): + """Fit the bounding box applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + # Calculate bounds + self.min_ = np.percentile(X, self.percentile[0], axis=0) + self.max_ = np.percentile(X, self.percentile[1], axis=0) + + return self + + def transform(self, X): + """Calculate the number of features outside their bounds for each sample. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + violations : ndarray of shape (n_samples, 1) + Number of features outside their bounds for each sample. + Zero indicates all features within bounds. + """ + check_is_fitted(self) + X = check_array(X) + + violations = np.sum((X < self.min_) | (X > self.max_), axis=1) + return violations.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + violations = self.transform(X).ravel() + return np.where(violations == 0, 1, -1) diff --git a/scikit_mol/applicability/convex_hull.py b/scikit_mol/applicability/convex_hull.py new file mode 100644 index 0000000..4f46eb9 --- /dev/null +++ b/scikit_mol/applicability/convex_hull.py @@ -0,0 +1,113 @@ +""" +Convex hull applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from scipy import optimize +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class ConvexHullApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain defined as the convex hull of the training data. + + The convex hull approach determines if a point belongs to the convex hull of the + training set by checking if it can be represented as a convex combination of + training points. + + The method is based on the `highs` solver from the `scipy.optimize` module, but is still + slow at inference time. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + points_ : ndarray of shape (n_features + 1, n_samples) + Transformed training points used for convex hull calculations. + """ + + def fit(self, X, y=None): + """Fit the convex hull applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + # Add ones column and transpose for convex hull calculations + self.points_ = np.r_[X.T, np.ones((1, X.shape[0]))].astype(np.float32) + + return self + + def transform(self, X): + """Calculate distance from convex hull for each sample. + + A distance of 0 indicates the sample lies within the convex hull. + Positive values indicate distance outside the hull. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + distances : ndarray of shape (n_samples, 1) + Distance from convex hull. Zero for points inside the hull, + positive for points outside. + """ + check_is_fitted(self) + X = check_array(X) + + # Calculate distances + if X.ndim == 1: + X = X.reshape(1, -1) + + distances = [] + for sample in X: + # Append 1 to sample vector + sample_ext = np.r_[sample, 1].astype(np.float16) + + # Try to solve the linear programming problem + result = optimize.linprog( + np.ones(self.points_.shape[1], dtype=np.float32), + A_eq=self.points_, + b_eq=sample_ext, + method="highs", + ) + # Distance is positive if no solution found, 0 if solution exists + distances.append(0.0 if result.success else 1.0) + + return np.array(distances).reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores == 0, 1, -1) diff --git a/scikit_mol/applicability/hotelling.py b/scikit_mol/applicability/hotelling.py new file mode 100644 index 0000000..1494d48 --- /dev/null +++ b/scikit_mol/applicability/hotelling.py @@ -0,0 +1,169 @@ +""" +Hotelling T² applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from scipy.stats import f as f_dist +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class HotellingT2ApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on Hotelling's T² statistic. + + Uses Hotelling's T² statistic to define an elliptical confidence region + around the training data. The threshold can be set using either the + F-distribution (statistical approach) or adjusted using a validation set. + + Lower volume protrusion scores indicate samples closer to the training + data center. By default, the threshold is set using the F-distribution + with a significance level of 0.05 (95% confidence). When using fit_threshold, + a target_percentile of 95 means that 95% of the validation samples with + the lowest protrusion scores will be considered inside the domain. + + Parameters + ---------- + significance : float, default=0.05 + Significance level for F-distribution threshold. + Only used if fit_threshold is not called. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + t2_ : ndarray of shape (n_features,) + Hotelling T² ellipse parameters. + threshold_ : float + Current threshold for volume protrusions. + + Examples + -------- + >>> from scikit_mol.applicability import HotellingT2ApplicabilityDomain + >>> ad = HotellingT2ApplicabilityDomain() + >>> # Using F-distribution threshold (default) + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + >>> + >>> # Adjusting threshold using validation set + >>> ad.fit_threshold(X_val, target_percentile=95) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] Hotelling, H. (1931). The generalization of Student's ratio. + The Annals of Mathematical Statistics, 2(3), 360-378. + """ + + def __init__(self, significance=0.05): + if not 0 < significance < 1: + raise ValueError("significance must be between 0 and 1") + self.significance = significance + + def fit(self, X, y=None): + """Fit the Hotelling T² applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + n_samples = X.shape[0] + + # Determine the Hotelling T² ellipse + self.t2_ = np.sqrt((1 / n_samples) * (X**2).sum(axis=0)) + + # Set initial threshold using F-distribution + f_stat = ( + (n_samples - 1) + / n_samples + * self.n_features_in_ + * (n_samples**2 - 1) + / (n_samples * (n_samples - self.n_features_in_)) + ) + f_stat *= f_dist.ppf( + 1 - self.significance, self.n_features_in_, n_samples - self.n_features_in_ + ) + self.threshold_ = f_stat + + return self + + def fit_threshold(self, X, target_percentile=95): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + target_percentile : float, default=95 + Target percentile of samples to include within domain. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X) + + if not 0 <= target_percentile <= 100: + raise ValueError("target_percentile must be between 0 and 100") + + # Calculate volume protrusions for validation set + scores = self.transform(X).ravel() + + # Set threshold to achieve desired percentile (lower scores = inside domain) + self.threshold_ = np.percentile(scores, 100 - target_percentile) + + return self + + def transform(self, X): + """Calculate volume protrusion scores for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + The volume protrusion scores. Higher values indicate samples + further from the training data center. + """ + check_is_fitted(self) + X = check_array(X) + + # Calculate volume protrusions + protrusions = (X**2 / self.t2_**2).sum(axis=1) + return protrusions.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/isolation_forest.py b/scikit_mol/applicability/isolation_forest.py new file mode 100644 index 0000000..3ef23d3 --- /dev/null +++ b/scikit_mol/applicability/isolation_forest.py @@ -0,0 +1,154 @@ +""" +Isolation Forest applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.ensemble import IsolationForest +from sklearn.utils.validation import check_array, check_is_fitted + + +class IsolationForestApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on Isolation Forest. + + Uses Isolation Forest to identify outliers based on the isolation depth + of samples in random decision trees. + + Parameters + ---------- + n_estimators : int, default=100 + Number of trees in the forest. + contamination : float, default=0.01 + Expected proportion of outliers in the training data. + random_state : int or RandomState, default=None + Controls the randomness of the forest. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + iforest_ : IsolationForest + Fitted isolation forest model. + + Examples + -------- + >>> from scikit_mol.applicability import IsolationForestApplicabilityDomain + >>> ad = IsolationForestApplicabilityDomain(contamination=0.1) + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] Liu, F. T., Ting, K. M., & Zhou, Z. H. (2008). Isolation forest. + In 2008 Eighth IEEE International Conference on Data Mining (pp. 413-422). + """ + + def __init__(self, n_estimators=100, contamination=0.01, random_state=None): + if not 0 < contamination < 1: + raise ValueError("contamination must be between 0 and 1") + + self.n_estimators = n_estimators + self.contamination = contamination + self.random_state = random_state + + def fit(self, X, y=None): + """Fit the isolation forest applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + self.iforest_ = IsolationForest( + n_estimators=self.n_estimators, + contamination=self.contamination, + random_state=self.random_state, + ) + self.iforest_.fit(X) + + self.fit_threshold(X) + + return self + + def transform(self, X): + """Calculate anomaly scores for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + The anomaly scores of the samples. + The lower the score, the more abnormal the sample. + """ + check_is_fitted(self) + X = check_array(X) + + scores = self.iforest_.score_samples(X) + return scores.reshape(-1, 1) + + def fit_threshold(self, X, target_percentile=95): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + target_percentile : float, default=95 + Target percentile of samples to include within domain. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X) + + if not 0 <= target_percentile <= 100: + raise ValueError("target_percentile must be between 0 and 100") + + # Get decision function scores + scores = self.iforest_.score_samples(X) + + # Set threshold to achieve desired percentile + self.threshold_ = np.percentile(scores, 100 - target_percentile) + + return self + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + if hasattr(self, "threshold_"): + return np.where(scores > self.threshold_, 1, -1) + return self.iforest_.predict(X) diff --git a/scikit_mol/applicability/kernel_density.py b/scikit_mol/applicability/kernel_density.py new file mode 100644 index 0000000..40125f1 --- /dev/null +++ b/scikit_mol/applicability/kernel_density.py @@ -0,0 +1,143 @@ +""" +Kernel Density applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.neighbors import KernelDensity +from sklearn.utils.validation import check_array, check_is_fitted + + +class KernelDensityApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on kernel density estimation. + + Uses kernel density estimation to model the distribution of the training data. + Samples with density below a threshold (determined by percentile of training + data densities) are considered outside the domain. + + Parameters + ---------- + bandwidth : float, default=1.0 + The bandwidth of the kernel. + kernel : str, default='gaussian' + The kernel to use. Options: ['gaussian', 'tophat', 'epanechnikov', + 'exponential', 'linear', 'cosine']. + percentile : float, default=1.0 + The percentile of training set densities to use as threshold. + Must be between 0 and 100. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + kde_ : KernelDensity + Fitted kernel density estimator. + threshold_ : float + Density threshold for domain membership. + + Examples + -------- + >>> from scikit_mol.applicability import KernelDensityApplicabilityDomain + >>> ad = KernelDensityApplicabilityDomain(bandwidth=1.0) + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + """ + + def __init__(self, bandwidth=1.0, kernel="gaussian", percentile=1.0): + if not 0 <= percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + + self.bandwidth = bandwidth + self.kernel = kernel + self.percentile = percentile + + def fit(self, X, y=None): + """Fit the kernel density applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + # Fit KDE + self.kde_ = KernelDensity(bandwidth=self.bandwidth, kernel=self.kernel) + self.kde_.fit(X) + + # Set initial threshold based on training data + self.fit_threshold(X) + + return self + + def transform(self, X): + """Calculate log density scores for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + The log density scores of the samples. Higher scores indicate samples + more similar to the training data. + """ + check_is_fitted(self) + X = check_array(X) + + scores = self.kde_.score_samples(X) + return scores.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores >= self.threshold_, 1, -1) + + def fit_threshold(self, X): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X) + + # Calculate density threshold from provided data + densities = self.kde_.score_samples(X) + self.threshold_ = np.percentile(densities, self.percentile) + + return self diff --git a/scikit_mol/applicability/knn.py b/scikit_mol/applicability/knn.py new file mode 100644 index 0000000..ebfd2d5 --- /dev/null +++ b/scikit_mol/applicability/knn.py @@ -0,0 +1,150 @@ +""" +K-Nearest Neighbors applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.neighbors import NearestNeighbors +from sklearn.utils.validation import check_array, check_is_fitted + + +class KNNApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain defined using K-nearest neighbors. + + Parameters + ---------- + n_neighbors : int, default=5 + Number of neighbors to use for distance calculation. + percentile : float, default=99 + Percentile of training set distances to use as threshold. + Samples with distances above this percentile are considered outside + the applicability domain. The fit_threshold method can be used to update + the threshold using new data without refitting the model (e.g. validation data). + metric : str, default='euclidean' + Distance metric to use for nearest neighbor calculation. + Any metric supported by sklearn.neighbors.NearestNeighbors can be used. + n_jobs : int, default=None + Number of parallel jobs to run for neighbors search. + None means 1 unless in a joblib.parallel_backend context. + -1 means using all processors. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + threshold_ : float + Distance threshold for the applicability domain. + nn_ : NearestNeighbors + Fitted nearest neighbors model. + """ + + def __init__(self, n_neighbors=5, percentile=95, metric="euclidean", n_jobs=None): + self.n_neighbors = n_neighbors + self.percentile = percentile + self.metric = metric + self.n_jobs = n_jobs + + def fit(self, X, y=None): + """Fit the KNN applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + if not 0 <= self.percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + + X = check_array(X, accept_sparse=True) + + self.n_features_in_ = X.shape[1] + + # Fit nearest neighbors model + self.nn_ = NearestNeighbors( + n_neighbors=self.n_neighbors + 1, # +1 because point is its own neighbor + metric=self.metric, + n_jobs=self.n_jobs, + ) + self.nn_.fit(X) + + # Set initial threshold based on training data + self.fit_threshold(X) + + return self + + def fit_threshold(self, X): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X, accept_sparse=True) + + # Calculate distances to k nearest neighbors + distances, _ = self.nn_.kneighbors(X) + mean_distances = distances[:, 1:].mean(axis=1) + + # Set threshold based on distance distribution + self.threshold_ = np.percentile(mean_distances, self.percentile) + + return self + + def transform(self, X): + """Calculate mean distance to k nearest neighbors in training set. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + distances : ndarray of shape (n_samples, 1) + Mean distance to k nearest neighbors. Higher values indicate samples + further from the training set. + """ + check_is_fitted(self) + X = check_array(X, accept_sparse=True) + + # Calculate distances to k nearest neighbors + distances, _ = self.nn_.kneighbors(X) + mean_distances = distances.mean(axis=1) + + return mean_distances.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/leverage.py b/scikit_mol/applicability/leverage.py new file mode 100644 index 0000000..3d8f309 --- /dev/null +++ b/scikit_mol/applicability/leverage.py @@ -0,0 +1,103 @@ +""" +Leverage-based applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) as described in the README.md file. +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class LeverageApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain defined using the leverage approach. + + The leverage approach measures how far a sample is from the center of the + feature space using the diagonal elements of the hat matrix H = X(X'X)^(-1)X'. + + Parameters + ---------- + threshold_factor : float, default=3 + Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n + where p is the number of features and n is the number of samples. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + threshold_ : float + Calculated leverage threshold. + var_covar_ : ndarray of shape (n_features, n_features) + Variance-covariance matrix of the training data. + """ + + def __init__(self, threshold_factor=3): + self.threshold_factor = threshold_factor + + def fit(self, X, y=None): + """Fit the leverage applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + n_samples = X.shape[0] + + # Calculate variance-covariance matrix + self.var_covar_ = np.linalg.inv(X.T.dot(X)) + + # Calculate threshold + self.threshold_ = self.threshold_factor * (self.n_features_in_ + 1) / n_samples + + return self + + def transform(self, X): + """Calculate leverage values for X. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + h : ndarray of shape (n_samples, 1) + The leverage values. Higher values indicate samples further from + the center of the training data. + """ + check_is_fitted(self) + X = check_array(X) + + # Calculate leverage values + h = np.sum(X.dot(self.var_covar_) * X, axis=1) + return h.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores < self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/local_outlier.py b/scikit_mol/applicability/local_outlier.py new file mode 100644 index 0000000..3fd181f --- /dev/null +++ b/scikit_mol/applicability/local_outlier.py @@ -0,0 +1,119 @@ +""" +Local Outlier Factor applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.neighbors import LocalOutlierFactor +from sklearn.utils.validation import check_array, check_is_fitted + + +class LocalOutlierFactorApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on Local Outlier Factor (LOF). + + LOF measures the local deviation of density of a sample with respect to its + neighbors, identifying samples that have substantially lower density than + their neighbors. + + Parameters + ---------- + n_neighbors : int, default=20 + Number of neighbors to use for LOF calculation. + contamination : float, default=0.1 + Expected proportion of outliers in the data set. + metric : str, default='euclidean' + Metric to use for distance computation. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + lof_ : LocalOutlierFactor + Fitted LOF estimator. + + Examples + -------- + >>> from scikit_mol.applicability import LocalOutlierFactorApplicabilityDomain + >>> ad = LocalOutlierFactorApplicabilityDomain() + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] Breunig et al. (2000). LOF: Identifying Density-Based Local Outliers. + In: Proc. 2000 ACM SIGMOD Int. Conf. Manag. Data, ACM, pp. 93-104. + """ + + def __init__(self, n_neighbors=20, contamination=0.1, metric="euclidean"): + self.n_neighbors = n_neighbors + self.contamination = contamination + self.metric = metric + + def fit(self, X, y=None): + """Fit the LOF applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + self.lof_ = LocalOutlierFactor( + n_neighbors=self.n_neighbors, + metric=self.metric, + contamination=self.contamination, + novelty=True, + ) + self.lof_.fit(X) + + return self + + def transform(self, X): + """Calculate LOF scores for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + The LOF scores of the samples. Higher scores indicate samples + that are more likely to be outliers. + """ + check_is_fitted(self) + X = check_array(X) + + # Get negative LOF scores (higher means more likely to be inlier) + scores = -self.lof_.score_samples(X) + return scores.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + return self.lof_.predict(X) diff --git a/scikit_mol/applicability/mahalanobis.py b/scikit_mol/applicability/mahalanobis.py new file mode 100644 index 0000000..4be40de --- /dev/null +++ b/scikit_mol/applicability/mahalanobis.py @@ -0,0 +1,158 @@ +""" +Mahalanobis distance applicability domain. +""" + +import numpy as np +from scipy import stats +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class MahalanobisApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on Mahalanobis distance. + + Uses Mahalanobis distance to measure how many standard deviations a sample + is from the training set mean, taking into account the covariance structure + of the data. For multivariate normal data, the squared Mahalanobis distances + follow a chi-square distribution. + + Parameters + ---------- + percentile : float, default=95.0 + Percentile for the confidence region (0-100). + Default 95.0 corresponds to ~2 standard deviations. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + mean_ : ndarray of shape (n_features,) + Mean of training data. + covariance_ : ndarray of shape (n_features, n_features) + Covariance matrix of training data. + inv_covariance_ : ndarray of shape (n_features, n_features) + Inverse covariance matrix. + threshold_ : float + Current threshold for Mahalanobis distances. + + Examples + -------- + >>> from scikit_mol.applicability import MahalanobisApplicabilityDomain + >>> ad = MahalanobisApplicabilityDomain(percentile=95) + >>> ad.fit(X_train) + >>> # Using chi-square threshold (default) + >>> predictions = ad.predict(X_test) + >>> + >>> # Adjusting threshold using validation set + >>> ad.fit_threshold(X_val, target_percentile=95) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] De Maesschalck, R., Jouan-Rimbaud, D., & Massart, D. L. (2000). + The Mahalanobis distance. Chemometrics and intelligent laboratory + systems, 50(1), 1-18. + """ + + def __init__(self, percentile=95.0): + if not 0 <= percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + self.percentile = percentile + + def fit(self, X, y=None): + """Fit the Mahalanobis distance applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + # Compute mean and covariance + self.mean_ = np.mean(X, axis=0) + self.covariance_ = np.cov(X, rowvar=False) + self.inv_covariance_ = np.linalg.inv(self.covariance_) + + # Set initial threshold using chi-square distribution + self.threshold_ = stats.chi2.ppf(self.percentile / 100, df=self.n_features_in_) + + return self + + def fit_threshold(self, X, target_percentile=95): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + target_percentile : float, default=95 + Target percentile of samples to include within domain. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X) + + if not 0 <= target_percentile <= 100: + raise ValueError("target_percentile must be between 0 and 100") + + # Calculate distances for validation set + scores = self.transform(X).ravel() + + # Set threshold to achieve desired percentile (lower distances = inside domain) + self.threshold_ = np.percentile(scores, 100 - target_percentile) + + return self + + def transform(self, X): + """Calculate Mahalanobis distances for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + distances : ndarray of shape (n_samples, 1) + The Mahalanobis distances. Higher values indicate samples + further from the training data center. + """ + check_is_fitted(self) + X = check_array(X) + + # Center the data + X_centered = X - self.mean_ + + # Calculate Mahalanobis distances + distances = np.sum(X_centered @ self.inv_covariance_ * X_centered, axis=1) + return distances.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/standardization.py b/scikit_mol/applicability/standardization.py new file mode 100644 index 0000000..c1e1414 --- /dev/null +++ b/scikit_mol/applicability/standardization.py @@ -0,0 +1,165 @@ +""" +Standardization approach applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. +""" + +import numpy as np +from scipy import stats +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.preprocessing import StandardScaler +from sklearn.utils.validation import check_array, check_is_fitted + + +class StandardizationApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain based on standardized feature values. + + Samples are considered within the domain if their standardized features + have a mean + z * std <= threshold, or if their maximum standardized + value <= threshold, where z corresponds to the specified percentile + assuming a normal distribution. + + Parameters + ---------- + percentile : float, default=95.0 + Percentile for the confidence interval (0-100). + Default 95.0 corresponds to ~2 standard deviations. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + scaler_ : StandardScaler + Fitted standard scaler. + threshold_ : float + Current threshold for standardized values. + + Examples + -------- + >>> from scikit_mol.applicability import StandardizationApplicabilityDomain + >>> ad = StandardizationApplicabilityDomain(percentile=95) + >>> ad.fit(X_train) + >>> # Optionally adjust threshold using validation set + >>> ad.fit_threshold(X_val, target_percentile=95) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] Roy, K., Kar, S., & Ambure, P. (2015). On a simple approach for + determining applicability domain of QSAR models. Chemometrics and + Intelligent Laboratory Systems, 145, 22-29. + """ + + def __init__(self, percentile=95.0): + if not 0 <= percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + self.percentile = percentile + + def fit(self, X, y=None): + """Fit the standardization applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + + self.scaler_ = StandardScaler() + self.scaler_.fit(X) + + # Convert percentile to z-score for initial threshold + self.threshold_ = stats.norm.ppf(self.percentile / 100) + + return self + + def fit_threshold(self, X, target_percentile=95): + """Update the threshold using new data without refitting the model. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Data to compute threshold from. + target_percentile : float, default=95 + Target percentile of samples to include within domain. + + Returns + ------- + self : object + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X) + + if not 0 <= target_percentile <= 100: + raise ValueError("target_percentile must be between 0 and 100") + + # Calculate scores for the provided data + scores = self.transform(X).ravel() + + # Set threshold to achieve desired percentile + self.threshold_ = np.percentile(scores, target_percentile) + + return self + + def transform(self, X): + """Calculate standardized feature statistics for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + The maximum of: + 1. Maximum absolute standardized value + 2. Mean + z * std of standardized values + where z corresponds to the specified percentile. + Higher values indicate samples further from the training data. + """ + check_is_fitted(self) + X = check_array(X) + + # Standardize features + X_std = self.scaler_.transform(X) + + # Calculate statistics + max_vals = np.max(np.abs(X_std), axis=1) + means = np.mean(X_std, axis=1) + stds = np.std(X_std, axis=1) + z_score = -stats.norm.ppf(self.percentile / 100) # negative for lower tail + mean_std = means + z_score * stds + + # Return maximum of the two criteria + scores = np.maximum(max_vals, mean_std) + return scores.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/topkat.py b/scikit_mol/applicability/topkat.py new file mode 100644 index 0000000..1392c65 --- /dev/null +++ b/scikit_mol/applicability/topkat.py @@ -0,0 +1,148 @@ +""" +TOPKAT's Optimal Prediction Space (OPS) applicability domain. + +This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) +Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) +See LICENSE.MIT in this directory for the original MIT license. + +""" + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_array, check_is_fitted + + +class TopkatApplicabilityDomain(BaseEstimator, TransformerMixin): + """Applicability domain defined using TOPKAT's Optimal Prediction Space (OPS). + + The method transforms the input space (P-space) to a normalized space (S-space), + then projects it to the Optimal Prediction Space using eigendecomposition. + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + X_min_ : ndarray of shape (n_features,) + Minimum values of training features. + X_max_ : ndarray of shape (n_features,) + Maximum values of training features. + eigen_val_ : ndarray of shape (n_features + 1,) + Eigenvalues of the S-space transformation. + eigen_vec_ : ndarray of shape (n_features + 1, n_features + 1) + Eigenvectors of the S-space transformation. + + Examples + -------- + >>> from scikit_mol.applicability import TopkatApplicabilityDomain + >>> ad = TopkatApplicabilityDomain() + >>> ad.fit(X_train) + >>> predictions = ad.predict(X_test) + + References + ---------- + .. [1] Gombar, Vijay K. (1996). Method and apparatus for validation of model-based + predictions (US Patent No. 6-036-349) USPTO. + """ + + def fit(self, X, y=None): + """Fit the TOPKAT applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Ignored + Not used, present for API consistency. + + Returns + ------- + self : object + Returns the instance itself. + """ + X = check_array(X) + self.n_features_in_ = X.shape[1] + n_samples = X.shape[0] + + # Store scaling factors + self.X_min_ = X.min(axis=0) + self.X_max_ = X.max(axis=0) + + # Transform P-space to S-space + denom = np.where( + (self.X_max_ - self.X_min_) != 0, (self.X_max_ - self.X_min_), 1 + ) + S = (2 * X - self.X_max_ - self.X_min_) / denom + + # Add column of ones + S = np.c_[np.ones(n_samples), S] + + # Calculate eigendecomposition + self.eigen_val_, self.eigen_vec_ = np.linalg.eigh(S.T.dot(S)) + + # Ensure real values (numerical stability) + self.eigen_val_ = np.real(self.eigen_val_) + self.eigen_vec_ = np.real(self.eigen_vec_) + + return self + + def transform(self, X): + """Calculate OPS distance scores for samples. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The data to transform. + + Returns + ------- + distances : ndarray of shape (n_samples, 1) + OPS distance scores. Higher values indicate samples further + from the training data. + """ + check_is_fitted(self) + X = check_array(X) + + # Transform to S-space + denom = np.where( + (self.X_max_ - self.X_min_) != 0, (self.X_max_ - self.X_min_), 1 + ) + S = (2 * X - self.X_max_ - self.X_min_) / denom + + # Add column of ones + if X.ndim == 1: + S = np.r_[1, S].reshape(1, -1) + else: + S = np.c_[np.ones(X.shape[0]), S] + + # Project to OPS + OPS = S.dot(self.eigen_vec_) + + # Calculate OPS distances - matching MLChemAD's approach + denom = np.divide( + np.ones_like(self.eigen_val_, dtype=float), + self.eigen_val_, + out=np.zeros_like(self.eigen_val_), + where=self.eigen_val_ != 0, + ) + distances = (OPS * OPS).dot(denom) + + return distances.reshape(-1, 1) + + def predict(self, X): + """Predict whether samples are within the applicability domain. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to predict. + + Returns + ------- + y_pred : ndarray of shape (n_samples,) + Returns 1 for samples inside the domain and -1 for samples outside + (following scikit-learn's convention for outlier detection). + """ + scores = self.transform(X).ravel() + threshold = 5 * (self.n_features_in_ + 1) / (2 * self.n_features_in_) + return np.where(scores < threshold, 1, -1) diff --git a/scikit_mol/applicability.py b/scikit_mol/applicability_old.py similarity index 97% rename from scikit_mol/applicability.py rename to scikit_mol/applicability_old.py index 8cfda1b..e7d8ecf 100644 --- a/scikit_mol/applicability.py +++ b/scikit_mol/applicability_old.py @@ -1,11 +1,8 @@ import numpy as np +from scipy import linalg, stats from scipy.sparse import csr_matrix -from sklearn.neighbors import NearestNeighbors from sklearn.base import BaseEstimator, TransformerMixin, check_array, check_is_fitted -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_is_fitted, check_array -import numpy as np -from scipy import linalg +from sklearn.neighbors import NearestNeighbors class NearestNeighborsDistance(BaseEstimator, TransformerMixin): @@ -231,7 +228,9 @@ def fit(self, X, y=None): # Set threshold based on strategy if self.threshold_strategy == "chi2": - self.threshold_ = chi2.ppf(self.threshold_quantile, df=self.n_features_in_) + self.threshold_ = stats.chi2.ppf( + self.threshold_quantile, df=self.n_features_in_ + ) elif self.threshold_strategy == "empirical": self.threshold_ = np.quantile(train_distances, self.threshold_quantile) elif self.threshold_strategy is None: From 2f3abfd96f001855e5ccbad64239704b1aa639df Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 09:39:11 +0100 Subject: [PATCH 04/24] Updated README with a reference --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 34920b4..7769378 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Scikit-Mol has been featured in blog-posts or used in research, some examples wh - [WAE-DTI: Ensemble-based architecture for drug–target interaction prediction using descriptors and embeddings](https://www.sciencedirect.com/science/article/pii/S2352914824001618) - [Data Driven Estimation of Molecular Log-Likelihood using Fingerprint Key Counting](https://chemrxiv.org/engage/chemrxiv/article-details/661402ee21291e5d1d646651) - [AUTONOMOUS DRUG DISCOVERY](https://www.proquest.com/openview/3e830e36bc618f263905a99e787c66c6/1?pq-origsite=gscholar&cbl=18750&diss=y) +- [DrugGym: A testbed for the economics of autonomous drug discovery](https://www.biorxiv.org/content/10.1101/2024.05.28.596296v1.abstract) ## Roadmap and Contributing From 198cfba1a62f84ecc65825aac7a8b38f0cc12682 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 10:36:15 +0100 Subject: [PATCH 05/24] Developed a base_class for making AD estimators consistent --- scikit_mol/applicability/base.py | 253 +++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 scikit_mol/applicability/base.py diff --git a/scikit_mol/applicability/base.py b/scikit_mol/applicability/base.py new file mode 100644 index 0000000..9e52ff2 --- /dev/null +++ b/scikit_mol/applicability/base.py @@ -0,0 +1,253 @@ +"""Base class for applicability domain estimators.""" + +from abc import ABC, abstractmethod +from typing import Any, ClassVar, Optional, Union + +import numpy as np +import pandas as pd +from numpy.typing import ArrayLike, NDArray +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils import check_array +from sklearn.utils._set_output import _SetOutputMixin, _wrap_method_output +from sklearn.utils.validation import check_is_fitted + + +class _ADOutputMixin(_SetOutputMixin): + """Extends sklearn's _SetOutputMixin to handle predict and score_transform methods.""" + + def __init_subclass__(cls, **kwargs): + # First handle transform/fit_transform via parent + super().__init_subclass__(auto_wrap_output_keys=("transform",), **kwargs) + + # Add our additional methods + for method in ["predict", "score_transform"]: + if method not in cls.__dict__: + continue + wrapped_method = _wrap_method_output(getattr(cls, method), "transform") + setattr(cls, method, wrapped_method) + + +class BaseApplicabilityDomain(BaseEstimator, TransformerMixin, _ADOutputMixin, ABC): + """Base class for applicability domain estimators. + + Parameters + ---------- + percentile : float or None, default=None + Percentile of samples to consider within domain (0-100). + If None: + - For methods with statistical thresholds: use statistical method + - For percentile-only methods: use 99.0 (include 99% of training samples) + + Notes + ----- + Subclasses must define _scoring_convention as either: + - 'high_outside': Higher scores indicate samples outside domain (e.g., distances) + - 'high_inside': Higher scores indicate samples inside domain (e.g., likelihoods) + + The raw scores from transform() should maintain their natural interpretation, + while predict() will handle the conversion to ensure consistent output + (1 = inside domain, -1 = outside domain). + + Attributes + ---------- + n_features_in_ : int + Number of features seen during fit. + threshold_ : float + Current threshold for domain membership. + """ + + _supports_threshold_fitting: ClassVar[bool] = True + _scoring_convention: ClassVar[str] # Must be set by subclasses + + def __init__( + self, percentile: Optional[float] = None, feature_prefix: str = "AD_estimator" + ) -> None: + if not hasattr(self, "_scoring_convention"): + raise TypeError( + f"Class {self.__class__.__name__} must define _scoring_convention " + "as either 'high_outside' or 'high_inside'" + ) + if self._scoring_convention not in ["high_outside", "high_inside"]: + raise ValueError( + f"Invalid _scoring_convention '{self._scoring_convention}'. " + "Must be either 'high_outside' or 'high_inside'" + ) + if percentile is not None and not 0 <= percentile <= 100: + raise ValueError("percentile must be between 0 and 100") + self.percentile = percentile + self.feature_prefix = feature_prefix + self._check_params = { + "estimator": self, + "accept_sparse": False, + "dtype": None, + "force_all_finite": True, + "ensure_2d": True, + } + + @abstractmethod + def fit(self, X: ArrayLike, y: Optional[Any] = None) -> "BaseApplicabilityDomain": + """Fit the applicability domain estimator. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Training data. + y : Any, optional (default=None) + Not used, present for API consistency. + + Returns + ------- + self : BaseApplicabilityDomain + Returns the instance itself. + """ + raise NotImplementedError("Subclasses should implement fit") + + def fit_threshold( + self, X: ArrayLike, target_percentile: Optional[float] = None + ) -> "BaseApplicabilityDomain": + """Update threshold estimation using new data. + + Parameters + ---------- + X : array-like + Data to compute threshold from. + target_percentile : float, optional (default=None) + If provided: Use this percentile and update self.percentile + If None: Use current self.percentile setting + - For methods with statistical thresholds: use statistical method if percentile=None + - For percentile-only methods: use 99.0 if percentile=None + + Returns + ------- + self : BaseApplicabilityDomain + Returns the instance itself. + """ + check_is_fitted(self) + X = check_array(X, **self._check_params) + + if target_percentile is not None: + if not 0 <= target_percentile <= 100: + raise ValueError("target_percentile must be between 0 and 100") + self.percentile = target_percentile + + # Use statistical threshold if available and percentile is None + if self.percentile is None: + if hasattr(self, "_set_statistical_threshold"): + self._set_statistical_threshold(X) + else: + # Use 99th percentile for methods without statistical thresholds + scores = self.transform(X).ravel() + if self._scoring_convention == "high_outside": + self.threshold_ = np.percentile( + scores, 99.0 + ) # Only 1% above threshold (outside) + else: # high_inside + self.threshold_ = np.percentile( + scores, 1.0 + ) # Only 1% below threshold (outside) + else: + scores = self.transform(X).ravel() + if self._scoring_convention == "high_outside": + self.threshold_ = np.percentile( + scores, self.percentile + ) # percentile% below = inside + else: # high_inside + self.threshold_ = np.percentile( + scores, 100 - self.percentile + ) # percentile% above = inside + + return self + + def transform( + self, X: Union[ArrayLike, pd.DataFrame], y: Optional[Any] = None + ) -> Union[NDArray[np.float64], pd.DataFrame]: + """Calculate applicability domain scores. + + Parameters + ---------- + X : array-like or pandas DataFrame + The data to transform. + + Returns + ------- + scores : ndarray or pandas DataFrame + Method-specific scores. Interpretation depends on _scoring_convention: + - 'high_outside': Higher scores indicate samples further from training data + - 'high_inside': Higher scores indicate samples closer to training data + Shape (n_samples, 1). + """ + check_is_fitted(self) + X = check_array(X, **self._check_params) + + # Calculate scores + scores = self._transform(X) + + return scores + + @abstractmethod + def _transform(self, X: NDArray) -> NDArray[np.float64]: + """Implementation of the transform method. + + Parameters + ---------- + X : ndarray of shape (n_samples, n_features) + Validated input data. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + Method-specific scores. + """ + raise NotImplementedError("Subclasses should implement _transform") + + def predict( + self, X: Union[ArrayLike, pd.DataFrame] + ) -> Union[NDArray[np.int_], pd.DataFrame]: + """Predict whether samples are within the applicability domain.""" + + check_is_fitted(self) + X = check_array(X, **self._check_params) + + # Calculate predictions + scores = self._transform(X).ravel() + if self._scoring_convention == "high_outside": + predictions = np.where(scores <= self.threshold_, 1, -1) + else: # high_inside + predictions = np.where(scores >= self.threshold_, 1, -1) + + return predictions + + def score_transform(self, X: ArrayLike) -> NDArray[np.float64]: + """Transform raw scores to [0,1] range using sigmoid. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The samples to transform. + + Returns + ------- + scores : ndarray of shape (n_samples, 1) + Transformed scores in [0,1] range. Higher values indicate + samples more likely to be within domain, regardless of + the method's raw score convention. + """ + + check_is_fitted(self) + scores = self.transform( + X + ) # May be pandas dataframe returned if that is set as output transform. + scores = check_array(scores, **self._check_params).ravel() + + # TODO: the sharpness ought to somehow be fitted to the range of the raw_scores + if self._scoring_convention == "high_outside": + # Flip sign for sigmoid so higher output = more likely inside + return (1 / (1 + np.exp(scores - self.threshold_))).reshape(-1, 1) + else: # high_inside + # No sign flip needed + return (1 / (1 + np.exp(self.threshold_ - scores))).reshape(-1, 1) + + def get_feature_names_out(self) -> NDArray[np.str_]: + """Get feature name for output column.""" + + return np.array([f"{self.feature_prefix}"]) From d5d84ad7520b73283763539fa97f2ecb8ed7c236 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 14:04:02 +0100 Subject: [PATCH 06/24] Made first transition for the kNN implementation. Currently only supports distance based methods, and only bit fingerprints for jaccard/tanimoto distances. --- scikit_mol/applicability/base.py | 93 +++++++++-------- scikit_mol/applicability/knn.py | 165 +++++++++++++++++-------------- tests/__init__.py | 0 tests/applicability/__init__.py | 0 tests/applicability/conftest.py | 49 +++++++++ tests/applicability/test_base.py | 127 ++++++++++++++++++++++++ tests/applicability/test_knn.py | 25 +++++ 7 files changed, 337 insertions(+), 122 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/applicability/__init__.py create mode 100644 tests/applicability/conftest.py create mode 100644 tests/applicability/test_base.py create mode 100644 tests/applicability/test_knn.py diff --git a/scikit_mol/applicability/base.py b/scikit_mol/applicability/base.py index 9e52ff2..cf7d994 100644 --- a/scikit_mol/applicability/base.py +++ b/scikit_mol/applicability/base.py @@ -27,6 +27,24 @@ def __init_subclass__(cls, **kwargs): setattr(cls, method, wrapped_method) +def _safe_flatten(X: Union[ArrayLike, pd.DataFrame]) -> NDArray[np.float64]: + """Safely flatten numpy arrays or pandas DataFrames to 1D array. + + Parameters + ---------- + X : array-like or DataFrame of shape (n_samples, n_features) + Input data to flatten + + Returns + ------- + flattened : ndarray of shape (n_samples,) + Flattened 1D array + """ + if hasattr(X, "to_numpy"): # pandas DataFrame + return X.to_numpy().ravel() + return np.asarray(X).ravel() + + class BaseApplicabilityDomain(BaseEstimator, TransformerMixin, _ADOutputMixin, ABC): """Base class for applicability domain estimators. @@ -103,25 +121,11 @@ def fit(self, X: ArrayLike, y: Optional[Any] = None) -> "BaseApplicabilityDomain raise NotImplementedError("Subclasses should implement fit") def fit_threshold( - self, X: ArrayLike, target_percentile: Optional[float] = None + self, + X: Union[ArrayLike, pd.DataFrame], + target_percentile: Optional[float] = None, ) -> "BaseApplicabilityDomain": - """Update threshold estimation using new data. - - Parameters - ---------- - X : array-like - Data to compute threshold from. - target_percentile : float, optional (default=None) - If provided: Use this percentile and update self.percentile - If None: Use current self.percentile setting - - For methods with statistical thresholds: use statistical method if percentile=None - - For percentile-only methods: use 99.0 if percentile=None - - Returns - ------- - self : BaseApplicabilityDomain - Returns the instance itself. - """ + """Update threshold estimation using new data.""" check_is_fitted(self) X = check_array(X, **self._check_params) @@ -131,30 +135,24 @@ def fit_threshold( self.percentile = target_percentile # Use statistical threshold if available and percentile is None + if self.percentile is None and hasattr(self, "_set_statistical_threshold"): + self._set_statistical_threshold(X) + return self + + # Otherwise use percentile-based threshold + scores = _safe_flatten(self.transform(X)) + if self.percentile is None: - if hasattr(self, "_set_statistical_threshold"): - self._set_statistical_threshold(X) - else: - # Use 99th percentile for methods without statistical thresholds - scores = self.transform(X).ravel() - if self._scoring_convention == "high_outside": - self.threshold_ = np.percentile( - scores, 99.0 - ) # Only 1% above threshold (outside) - else: # high_inside - self.threshold_ = np.percentile( - scores, 1.0 - ) # Only 1% below threshold (outside) + # Default percentile for methods without statistical thresholds + if self._scoring_convention == "high_outside": + self.threshold_ = np.percentile(scores, 99.0) + else: # high_inside + self.threshold_ = np.percentile(scores, 1.0) else: - scores = self.transform(X).ravel() if self._scoring_convention == "high_outside": - self.threshold_ = np.percentile( - scores, self.percentile - ) # percentile% below = inside + self.threshold_ = np.percentile(scores, self.percentile) else: # high_inside - self.threshold_ = np.percentile( - scores, 100 - self.percentile - ) # percentile% above = inside + self.threshold_ = np.percentile(scores, 100 - self.percentile) return self @@ -204,40 +202,37 @@ def predict( self, X: Union[ArrayLike, pd.DataFrame] ) -> Union[NDArray[np.int_], pd.DataFrame]: """Predict whether samples are within the applicability domain.""" - check_is_fitted(self) X = check_array(X, **self._check_params) # Calculate predictions - scores = self._transform(X).ravel() + scores = _safe_flatten(self.transform(X)) if self._scoring_convention == "high_outside": predictions = np.where(scores <= self.threshold_, 1, -1) else: # high_inside predictions = np.where(scores >= self.threshold_, 1, -1) - return predictions + return predictions.ravel() - def score_transform(self, X: ArrayLike) -> NDArray[np.float64]: + def score_transform( + self, X: Union[ArrayLike, pd.DataFrame] + ) -> Union[NDArray[np.float64], pd.DataFrame]: """Transform raw scores to [0,1] range using sigmoid. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : array-like or DataFrame of shape (n_samples, n_features) The samples to transform. Returns ------- - scores : ndarray of shape (n_samples, 1) + scores : ndarray or DataFrame of shape (n_samples, 1) Transformed scores in [0,1] range. Higher values indicate samples more likely to be within domain, regardless of the method's raw score convention. """ - check_is_fitted(self) - scores = self.transform( - X - ) # May be pandas dataframe returned if that is set as output transform. - scores = check_array(scores, **self._check_params).ravel() + scores = _safe_flatten(self.transform(X)) # TODO: the sharpness ought to somehow be fitted to the range of the raw_scores if self._scoring_convention == "high_outside": diff --git a/scikit_mol/applicability/knn.py b/scikit_mol/applicability/knn.py index ebfd2d5..749980e 100644 --- a/scikit_mol/applicability/knn.py +++ b/scikit_mol/applicability/knn.py @@ -7,49 +7,116 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Callable, ClassVar, Optional, Union + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin +from numpy.typing import ArrayLike from sklearn.neighbors import NearestNeighbors -from sklearn.utils.validation import check_array, check_is_fitted + +from .base import BaseApplicabilityDomain -class KNNApplicabilityDomain(BaseEstimator, TransformerMixin): +class KNNApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain defined using K-nearest neighbors. + Determines domain membership based on the mean distance to k nearest neighbors + in the training set. Higher distances indicate samples further from the + training distribution. + Parameters ---------- n_neighbors : int, default=5 Number of neighbors to use for distance calculation. - percentile : float, default=99 - Percentile of training set distances to use as threshold. - Samples with distances above this percentile are considered outside - the applicability domain. The fit_threshold method can be used to update - the threshold using new data without refitting the model (e.g. validation data). - metric : str, default='euclidean' - Distance metric to use for nearest neighbor calculation. - Any metric supported by sklearn.neighbors.NearestNeighbors can be used. + percentile : float or None, default=None + Percentile of training set distances to use as threshold (0-100). + If None, uses 99.0 (include 99% of training samples). + distance_metric : str or callable, default='euclidean' + Distance metric to use. Options: + - 'euclidean': Euclidean distance (default) + - 'manhattan': Manhattan distance + - 'cosine': Cosine distance + - 'tanimoto': Tanimoto distance for binary fingerprints (same as 'jaccard') + - 'jaccard': Jaccard distance for binary fingerprints + - callable: Custom distance metric function(X, Y) -> array-like + Any distance metric supported by sklearn.neighbors.NearestNeighbors can also be used. + Note: Only distance metrics are supported (higher values = more distant) currently. n_jobs : int, default=None Number of parallel jobs to run for neighbors search. None means 1 unless in a joblib.parallel_backend context. -1 means using all processors. + feature_prefix : str, default='KNN' + Prefix for feature names in output. + + Notes + ----- + For binary fingerprints, the Tanimoto distance is equivalent to the Jaccard distance. + Both 'tanimoto' and 'jaccard' options use scipy's implementation of the Jaccard + distance metric. Attributes ---------- n_features_in_ : int Number of features seen during fit. threshold_ : float - Distance threshold for the applicability domain. + Distance threshold for domain membership. nn_ : NearestNeighbors Fitted nearest neighbors model. + + Examples + -------- + >>> import numpy as np + >>> from scikit_mol.applicability import KNNApplicabilityDomain + >>> + >>> # Generate example data + >>> rng = np.random.RandomState(0) + >>> X_train = rng.normal(0, 1, (100, 5)) + >>> X_test = rng.normal(0, 2, (20, 5)) # More spread out than training + >>> + >>> # Fit AD model + >>> ad = KNNApplicabilityDomain(n_neighbors=5, percentile=95) + >>> ad.fit(X_train) + >>> + >>> # Get raw distance scores (higher = more distant) + >>> distances = ad.transform(X_test) + >>> + >>> # Get domain membership predictions + >>> predictions = ad.predict(X_test) # 1 = inside, -1 = outside + >>> + >>> # Get probability-like scores + >>> scores = ad.score_transform(X_test) # Higher = more likely inside """ - def __init__(self, n_neighbors=5, percentile=95, metric="euclidean", n_jobs=None): + _scoring_convention: ClassVar[str] = ( + "high_outside" # Higher distance = outside domain + ) + + def __init__( + self, + n_neighbors: int = 5, + percentile: Optional[float] = None, + distance_metric: Union[str, Callable] = "euclidean", + n_jobs: Optional[int] = None, + feature_prefix: str = "KNN", + ) -> None: + super().__init__(percentile=percentile, feature_prefix=feature_prefix) self.n_neighbors = n_neighbors - self.percentile = percentile - self.metric = metric + self.distance_metric = distance_metric self.n_jobs = n_jobs - def fit(self, X, y=None): + @property + def distance_metric(self) -> Union[Callable, str]: + return self._distance_metric + + @distance_metric.setter + def distance_metric(self, value: Union[str, Callable]) -> None: + if not isinstance(value, (str, Callable)): + raise ValueError("distance_metric must be a string or callable") + if value == "tanimoto": + self._distance_metric = "jaccard" # Use scipy's jaccard metric + else: + self._distance_metric = value + + def fit(self, X: ArrayLike, y=None) -> "KNNApplicabilityDomain": """Fit the KNN applicability domain. Parameters @@ -61,20 +128,19 @@ def fit(self, X, y=None): Returns ------- - self : object + self : KNNApplicabilityDomain Returns the instance itself. """ - if not 0 <= self.percentile <= 100: - raise ValueError("percentile must be between 0 and 100") - - X = check_array(X, accept_sparse=True) + if not isinstance(self.n_neighbors, int) or self.n_neighbors < 1: + raise ValueError("n_neighbors must be a positive integer") + X = self._validate_data(X) self.n_features_in_ = X.shape[1] # Fit nearest neighbors model self.nn_ = NearestNeighbors( n_neighbors=self.n_neighbors + 1, # +1 because point is its own neighbor - metric=self.metric, + metric=self.distance_metric, n_jobs=self.n_jobs, ) self.nn_.fit(X) @@ -84,38 +150,13 @@ def fit(self, X, y=None): return self - def fit_threshold(self, X): - """Update the threshold using new data without refitting the model. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - - Returns - ------- - self : object - Returns the instance itself. - """ - check_is_fitted(self) - X = check_array(X, accept_sparse=True) - - # Calculate distances to k nearest neighbors - distances, _ = self.nn_.kneighbors(X) - mean_distances = distances[:, 1:].mean(axis=1) - - # Set threshold based on distance distribution - self.threshold_ = np.percentile(mean_distances, self.percentile) - - return self - - def transform(self, X): + def _transform(self, X: np.ndarray) -> np.ndarray: """Calculate mean distance to k nearest neighbors in training set. Parameters ---------- - X : array-like of shape (n_samples, n_features) - The data to transform. + X : ndarray of shape (n_samples, n_features) + Validated input data. Returns ------- @@ -123,28 +164,6 @@ def transform(self, X): Mean distance to k nearest neighbors. Higher values indicate samples further from the training set. """ - check_is_fitted(self) - X = check_array(X, accept_sparse=True) - - # Calculate distances to k nearest neighbors distances, _ = self.nn_.kneighbors(X) - mean_distances = distances.mean(axis=1) - + mean_distances = distances[:, 1:].mean(axis=1) # Skip first (self) neighbor return mean_distances.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - return np.where(scores <= self.threshold_, 1, -1) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/applicability/__init__.py b/tests/applicability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/applicability/conftest.py b/tests/applicability/conftest.py new file mode 100644 index 0000000..f762a8e --- /dev/null +++ b/tests/applicability/conftest.py @@ -0,0 +1,49 @@ +import numpy as np +import pytest +from sklearn.decomposition import PCA +from sklearn.preprocessing import StandardScaler + +from scikit_mol.applicability import KNNApplicabilityDomain +from scikit_mol.fingerprints import MorganFingerprintTransformer + +from ..fixtures import mols_list + + +@pytest.fixture( + params=[ + (KNNApplicabilityDomain, dict(n_neighbors=3)), + # Add other AD estimators here as (class, params) tuples + ] +) +def ad_estimator(request): + """Fixture providing fresh AD estimator instances.""" + estimator_class, params = request.param + return estimator_class(**params) + + +@pytest.fixture +def reduced_fingerprints(mols_list): + """Create dimensionality-reduced fingerprints for AD testing.""" + # Generate larger fingerprints first + fps = MorganFingerprintTransformer(fpSize=1024).fit_transform(mols_list) + # Reduce dimensionality while preserving ~90% variance + pca = PCA(n_components=0.9) + return StandardScaler().fit_transform(pca.fit_transform(fps)) + + +@pytest.fixture +def binary_fingerprints(mols_list): + """Binary fingerprints for testing e.g. Tanimoto distance.""" + return MorganFingerprintTransformer(fpSize=1024).fit_transform(mols_list) + + +@pytest.fixture +def ad_test_data(): + """Simple 2D data with clear in/out domain regions.""" + rng = np.random.RandomState(42) # Fixed seed for reproducibility + X_train = rng.uniform(0, 1, (20, 2)) + X_test_in = rng.uniform(0.25, 0.75, (5, 2)) + X_test_out = rng.uniform(2, 3, (5, 2)) + X_test = np.vstack([X_test_in, X_test_out]) + y_test = np.array([1] * 5 + [-1] * 5) + return X_train, X_test, y_test diff --git a/tests/applicability/test_base.py b/tests/applicability/test_base.py new file mode 100644 index 0000000..dd0df67 --- /dev/null +++ b/tests/applicability/test_base.py @@ -0,0 +1,127 @@ +"""Common tests for all applicability domain estimators.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal, assert_array_equal +from sklearn.utils.estimator_checks import check_estimator + +# def test_estimator_api(ad_estimator): +# """Test scikit-learn API compatibility.""" +# check_estimator(ad_estimator) + + +def test_basic_functionality(ad_estimator, reduced_fingerprints): + """Test basic fit/transform on reduced fingerprints.""" + ad_estimator.fit(reduced_fingerprints) + scores = ad_estimator.transform(reduced_fingerprints) + assert scores.shape == (len(reduced_fingerprints), 1) + assert np.isfinite(scores).all() + + +def test_predict_functionality(ad_estimator, ad_test_data): + """Test predict method returns expected values.""" + X_train, X_test, expected = ad_test_data + + # Fit and predict + ad_estimator.fit(X_train) + predictions = ad_estimator.predict(X_test) + + # Check output format + assert predictions.shape == (len(X_test),) # Should be 1D + assert set(np.unique(predictions)) <= {-1, 1} # Only -1 and 1 allowed + + # Check predictions make sense (in/out of domain) + accuracy = np.mean(predictions == expected) + assert accuracy >= 0.8 # Allow some misclassification + + +def test_score_transform(ad_estimator, ad_test_data): + """Test score_transform returns valid probability-like scores.""" + X_train, X_test, expected = ad_test_data + + # Fit and get scores + ad_estimator.fit(X_train) + scores = ad_estimator.score_transform(X_test) + + # Check output format + assert scores.shape == (len(X_test), 1) + assert np.all((0 <= scores) & (scores <= 1)) # Scores in [0,1] + + # Check scores correlate with domain membership + in_domain = expected == 1 + mean_in = np.mean(scores[in_domain]) + mean_out = np.mean(scores[~in_domain]) + assert mean_in > mean_out # Inside domain should have higher scores + + +def test_threshold_setting(ad_estimator, reduced_fingerprints): + """Test threshold setting and percentile behavior.""" + # Test default threshold + ad_estimator.fit(reduced_fingerprints) + pred_default = ad_estimator.predict(reduced_fingerprints) + + # Test custom percentile + ad_estimator.percentile = 90 + ad_estimator.fit_threshold(reduced_fingerprints) + pred_90 = ad_estimator.predict(reduced_fingerprints) + + # More samples should be outside with stricter threshold + n_inside_default = np.sum(pred_default == 1) + n_inside_90 = np.sum(pred_90 == 1) + assert n_inside_90 <= n_inside_default + + +def test_feature_names(ad_estimator, reduced_fingerprints): + """Test feature names are properly handled.""" + ad_estimator.fit(reduced_fingerprints) + + # Check feature names exist and match prefix + feature_names = ad_estimator.get_feature_names_out() + assert len(feature_names) == 1 + assert feature_names[0].startswith(ad_estimator.feature_prefix) + + +def test_pandas_output(ad_estimator, reduced_fingerprints): + """Test pandas DataFrame output functionality.""" + ad_estimator.set_output(transform="pandas") + ad_estimator.fit(reduced_fingerprints) + + # Test transform output + scores_df = ad_estimator.transform(reduced_fingerprints) + assert hasattr(scores_df, "columns") + assert len(scores_df.columns) == 1 + assert scores_df.columns[0].startswith(ad_estimator.feature_prefix) + + # Test predict output + pred_df = ad_estimator.predict(reduced_fingerprints) + assert hasattr(pred_df, "columns") + assert len(pred_df.columns) == 1 + + +def test_input_validation(ad_estimator): + """Test input validation and error handling.""" + # Test fitting with invalid input + with pytest.raises(ValueError): + ad_estimator.fit([[]]) # Empty data + + with pytest.raises(ValueError): + ad_estimator.fit([[1], [2, 3]]) # Inconsistent dimensions + + # Test invalid percentile + with pytest.raises(ValueError): + ad_estimator.percentile = 101 + ad_estimator.fit([[1, 2]]) + + +def test_refit_consistency(ad_estimator, reduced_fingerprints): + """Test consistency when refitting with same data.""" + ad_estimator.fit(reduced_fingerprints) + scores1 = ad_estimator.transform(reduced_fingerprints) + + ad_estimator.fit(reduced_fingerprints) + scores2 = ad_estimator.transform(reduced_fingerprints) + + assert_array_almost_equal(scores1, scores2) + + +# ... other common tests ... diff --git a/tests/applicability/test_knn.py b/tests/applicability/test_knn.py new file mode 100644 index 0000000..3ff5463 --- /dev/null +++ b/tests/applicability/test_knn.py @@ -0,0 +1,25 @@ +"""Tests specific to KNN applicability domain.""" + +import numpy as np +import pytest + +from scikit_mol.applicability import KNNApplicabilityDomain +from scikit_mol.fingerprints import MorganFingerprintTransformer + + +@pytest.fixture +def binary_fingerprints(mols_list): + """Binary fingerprints for testing Tanimoto distance.""" + return MorganFingerprintTransformer(fpSize=1024).fit_transform(mols_list) + + +def test_knn_tanimoto(binary_fingerprints): + """Test KNN with Tanimoto distance on binary fingerprints.""" + ad = KNNApplicabilityDomain(n_neighbors=3, distance_metric="tanimoto") + ad.fit(binary_fingerprints) + scores = ad.transform(binary_fingerprints) + assert scores.shape == (len(binary_fingerprints), 1) + assert np.all((0 <= scores) & (scores <= 1)) # Tanimoto distances are [0,1] + + +# ... other KNN-specific tests ... From 51efb25b25440254fb4d4b894f5a0727ac0189bd Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 14:15:14 +0100 Subject: [PATCH 07/24] Added leverage to our tests. --- scikit_mol/applicability/leverage.py | 112 +++++++++++++++++---------- tests/applicability/conftest.py | 4 +- tests/applicability/test_leverage.py | 63 +++++++++++++++ 3 files changed, 134 insertions(+), 45 deletions(-) create mode 100644 tests/applicability/test_leverage.py diff --git a/scikit_mol/applicability/leverage.py b/scikit_mol/applicability/leverage.py index 3d8f309..fee276c 100644 --- a/scikit_mol/applicability/leverage.py +++ b/scikit_mol/applicability/leverage.py @@ -7,22 +7,30 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted +from numpy.typing import ArrayLike, NDArray +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class LeverageApplicabilityDomain(BaseEstimator, TransformerMixin): +class LeverageApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain defined using the leverage approach. The leverage approach measures how far a sample is from the center of the feature space using the diagonal elements of the hat matrix H = X(X'X)^(-1)X'. + Higher leverage values indicate samples further from the center of the training data. Parameters ---------- threshold_factor : float, default=3 Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n where p is the number of features and n is the number of samples. + percentile : float or None, default=None + If not None, overrides the statistical threshold with a percentile-based one. + See BaseApplicabilityDomain for details. Attributes ---------- @@ -32,12 +40,61 @@ class LeverageApplicabilityDomain(BaseEstimator, TransformerMixin): Calculated leverage threshold. var_covar_ : ndarray of shape (n_features, n_features) Variance-covariance matrix of the training data. + + Notes + ----- + The statistical threshold h* = 3 * (p+1)/n is a commonly used rule of thumb + in regression diagnostics, where p is the number of features and n is the + number of training samples. + + Input data should be scaled (e.g., using StandardScaler) to ensure all features + contribute equally. For high-dimensional data like fingerprints, dimensionality + reduction (e.g., PCA) is strongly recommended to avoid computational issues with + the variance-covariance matrix inversion. + + Examples + -------- + >>> from sklearn.pipeline import Pipeline + >>> from sklearn.preprocessing import StandardScaler + >>> from sklearn.decomposition import PCA + >>> from scikit_mol.applicability import LeverageApplicabilityDomain + >>> + >>> # Create pipeline with scaling and dimensionality reduction + >>> pipe = Pipeline([ + ... ('scaler', StandardScaler()), + ... ('pca', PCA(n_components=0.95)), # Keep 95% of variance + ... ('ad', LeverageApplicabilityDomain()) + ... ]) + >>> + >>> # Fit pipeline + >>> X_train = [[0, 1, 2], [1, 2, 3], [2, 3, 4]] # Example data + >>> pipe.fit(X_train) + >>> + >>> # Predict domain membership for new samples + >>> X_test = [[0, 1, 2], [10, 20, 30]] + >>> pipe.predict(X_test) # Returns [1, -1] (in/out of domain) """ - def __init__(self, threshold_factor=3): + _scoring_convention = "high_outside" + _supports_threshold_fitting = True + + def __init__( + self, + threshold_factor: float = 3, + percentile: Optional[float] = None, + feature_prefix: str = "Leverage", + ) -> None: + super().__init__(percentile=percentile, feature_prefix=feature_prefix) self.threshold_factor = threshold_factor - def fit(self, X, y=None): + def _set_statistical_threshold(self, X: NDArray) -> None: + """Set the statistical threshold h* = threshold_factor * (p+1)/n.""" + n_samples = X.shape[0] + self.threshold_ = self.threshold_factor * (self.n_features_in_ + 1) / n_samples + + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "LeverageApplicabilityDomain": """Fit the leverage applicability domain. Parameters @@ -49,55 +106,24 @@ def fit(self, X, y=None): Returns ------- - self : object + self : LeverageApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] - n_samples = X.shape[0] # Calculate variance-covariance matrix self.var_covar_ = np.linalg.inv(X.T.dot(X)) - # Calculate threshold - self.threshold_ = self.threshold_factor * (self.n_features_in_ + 1) / n_samples + # Set initial threshold + self._set_statistical_threshold(X) return self - def transform(self, X): - """Calculate leverage values for X. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The data to transform. + def _transform(self, X: NDArray) -> NDArray[np.float64]: + """Calculate leverage values. - Returns - ------- - h : ndarray of shape (n_samples, 1) - The leverage values. Higher values indicate samples further from - the center of the training data. + Higher values indicate samples further from the center of the training data. """ - check_is_fitted(self) - X = check_array(X) - - # Calculate leverage values h = np.sum(X.dot(self.var_covar_) * X, axis=1) return h.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - return np.where(scores < self.threshold_, 1, -1) diff --git a/tests/applicability/conftest.py b/tests/applicability/conftest.py index f762a8e..7047573 100644 --- a/tests/applicability/conftest.py +++ b/tests/applicability/conftest.py @@ -3,7 +3,7 @@ from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler -from scikit_mol.applicability import KNNApplicabilityDomain +from scikit_mol.applicability import KNNApplicabilityDomain, LeverageApplicabilityDomain from scikit_mol.fingerprints import MorganFingerprintTransformer from ..fixtures import mols_list @@ -12,7 +12,7 @@ @pytest.fixture( params=[ (KNNApplicabilityDomain, dict(n_neighbors=3)), - # Add other AD estimators here as (class, params) tuples + (LeverageApplicabilityDomain, dict(threshold_factor=3)), ] ) def ad_estimator(request): diff --git a/tests/applicability/test_leverage.py b/tests/applicability/test_leverage.py new file mode 100644 index 0000000..892582a --- /dev/null +++ b/tests/applicability/test_leverage.py @@ -0,0 +1,63 @@ +"""Tests specific to Leverage applicability domain.""" + +import numpy as np +import pytest +from sklearn.decomposition import PCA +from sklearn.exceptions import NotFittedError +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler + +from scikit_mol.applicability import LeverageApplicabilityDomain + + +def test_leverage_statistical_threshold(ad_test_data): + """Test the statistical threshold calculation.""" + X_train, _, _ = ad_test_data + ad = LeverageApplicabilityDomain(threshold_factor=3) + ad.fit(X_train) + + # Check threshold matches formula h* = 3 * (p+1)/n + n_samples, n_features = X_train.shape + expected_threshold = 3 * (n_features + 1) / n_samples + assert np.isclose(ad.threshold_, expected_threshold) + + +def test_leverage_pipeline(reduced_fingerprints): + """Test leverage works in pipeline with scaling and PCA.""" + pipe = Pipeline( + [ + ("scaler", StandardScaler()), + ("pca", PCA(n_components=0.95)), + ("ad", LeverageApplicabilityDomain()), + ] + ) + + # Should run without errors + pipe.fit(reduced_fingerprints) + scores = pipe.transform(reduced_fingerprints) + assert scores.shape == (len(reduced_fingerprints), 1) + + +def test_leverage_threshold_factor(): + """Test different threshold factors.""" + X = np.array([[1, 2], [3, 4], [5, 6]]) + + ad1 = LeverageApplicabilityDomain(threshold_factor=3) + ad2 = LeverageApplicabilityDomain(threshold_factor=2) + + ad1.fit(X) + ad2.fit(X) + + # Higher threshold factor should result in higher threshold + assert ad1.threshold_ > ad2.threshold_ + + +def test_leverage_var_covar_matrix(ad_test_data): + """Test the variance-covariance matrix calculation.""" + X_train, _, _ = ad_test_data + ad = LeverageApplicabilityDomain() + ad.fit(X_train) + + # Check matrix properties + assert ad.var_covar_.shape == (X_train.shape[1], X_train.shape[1]) + assert np.allclose(ad.var_covar_, ad.var_covar_.T) # Should be symmetric From f75bc800b0e9a7ea6e7ad4efe68d7d1bc6a37a46 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 14:58:09 +0100 Subject: [PATCH 08/24] Added more AD estimators as children of base_class --- scikit_mol/applicability/bounding_box.py | 96 ++++++----- scikit_mol/applicability/convex_hull.py | 64 +++++--- scikit_mol/applicability/hotelling.py | 134 ++++++--------- scikit_mol/applicability/isolation_forest.py | 161 +++++++++++-------- tests/applicability/conftest.py | 20 ++- tests/applicability/test_base.py | 16 +- tests/applicability/test_bounding_box.py | 71 ++++++++ tests/applicability/test_convex_hull.py | 81 ++++++++++ tests/applicability/test_hotelling.py | 87 ++++++++++ tests/applicability/test_isolation_forest.py | 25 +++ 10 files changed, 522 insertions(+), 233 deletions(-) create mode 100644 tests/applicability/test_bounding_box.py create mode 100644 tests/applicability/test_convex_hull.py create mode 100644 tests/applicability/test_hotelling.py create mode 100644 tests/applicability/test_isolation_forest.py diff --git a/scikit_mol/applicability/bounding_box.py b/scikit_mol/applicability/bounding_box.py index 4c946fe..6bd35b7 100644 --- a/scikit_mol/applicability/bounding_box.py +++ b/scikit_mol/applicability/bounding_box.py @@ -7,22 +7,28 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional, Tuple, Union + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted +from numpy.typing import ArrayLike, NDArray + +from .base import BaseApplicabilityDomain -class BoundingBoxApplicabilityDomain(BaseEstimator, TransformerMixin): +class BoundingBoxApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain defined by feature value ranges. Samples falling outside the allowed range for any feature are considered - outside the domain. + outside the domain. The range for each feature is defined by percentiles + of the training set distribution. Parameters ---------- percentile : float or tuple of float, default=(0.1, 99.9) Percentile(s) of the training set distribution used to define the bounding box. If float, uses (percentile, 100-percentile). + feature_prefix : str, default="BoundingBox" + Prefix for feature names in output. Attributes ---------- @@ -32,42 +38,49 @@ class BoundingBoxApplicabilityDomain(BaseEstimator, TransformerMixin): Minimum allowed value for each feature. max_ : ndarray of shape (n_features,) Maximum allowed value for each feature. + threshold_ : float + Current threshold for domain membership (always 0.5). + + Notes + ----- + The bounding box method is simple but effective, especially for chemical + descriptors with clear physical interpretations. For high-dimensional or + correlated features, other methods may be more appropriate. Examples -------- >>> from sklearn.pipeline import make_pipeline >>> from sklearn.preprocessing import StandardScaler - >>> from sklearn.decomposition import PCA >>> from scikit_mol.applicability import BoundingBoxApplicabilityDomain - - Basic usage: + >>> + >>> # Basic usage >>> ad = BoundingBoxApplicabilityDomain(percentile=1) >>> ad.fit(X_train) >>> predictions = ad.predict(X_test) - - With preprocessing: + >>> + >>> # With preprocessing >>> pipe = make_pipeline( ... StandardScaler(), ... BoundingBoxApplicabilityDomain(percentile=1) ... ) >>> pipe.fit(X_train) >>> predictions = pipe.predict(X_test) - - With PCA preprocessing: - >>> pipe = make_pipeline( - ... StandardScaler(), - ... PCA(n_components=0.9), - ... BoundingBoxApplicabilityDomain(percentile=1) - ... ) - >>> pipe.fit(X_train) - >>> predictions = pipe.predict(X_test) """ - def __init__(self, percentile=(0.1, 99.9)): + _scoring_convention = "high_outside" + _supports_threshold_fitting = False + + def __init__( + self, + percentile: Union[float, Tuple[float, float]] = (0.1, 99.9), + feature_prefix: str = "BoundingBox", + ) -> None: + super().__init__(percentile=None, feature_prefix=feature_prefix) + if isinstance(percentile, (int, float)): if not 0 <= percentile <= 100: raise ValueError("percentile must be between 0 and 100") - self.percentile = (percentile, 100 - percentile) + self.box_percentile = (percentile, 100 - percentile) else: if not all(0 <= p <= 100 for p in percentile): raise ValueError("percentiles must be between 0 and 100") @@ -75,9 +88,11 @@ def __init__(self, percentile=(0.1, 99.9)): raise ValueError("percentile must be a float or tuple of 2 floats") if percentile[0] >= percentile[1]: raise ValueError("first percentile must be less than second") - self.percentile = percentile + self.box_percentile = percentile - def fit(self, X, y=None): + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "BoundingBoxApplicabilityDomain": """Fit the bounding box applicability domain. Parameters @@ -89,24 +104,27 @@ def fit(self, X, y=None): Returns ------- - self : object + self : BoundingBoxApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = self._validate_data(X) self.n_features_in_ = X.shape[1] # Calculate bounds - self.min_ = np.percentile(X, self.percentile[0], axis=0) - self.max_ = np.percentile(X, self.percentile[1], axis=0) + self.min_ = np.percentile(X, self.box_percentile[0], axis=0) + self.max_ = np.percentile(X, self.box_percentile[1], axis=0) + + # Fixed threshold since we count violations + self.threshold_ = 0.5 return self - def transform(self, X): - """Calculate the number of features outside their bounds for each sample. + def _transform(self, X: NDArray) -> NDArray[np.float64]: + """Calculate the number of features outside their bounds. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -115,25 +133,5 @@ def transform(self, X): Number of features outside their bounds for each sample. Zero indicates all features within bounds. """ - check_is_fitted(self) - X = check_array(X) - violations = np.sum((X < self.min_) | (X > self.max_), axis=1) return violations.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - violations = self.transform(X).ravel() - return np.where(violations == 0, 1, -1) diff --git a/scikit_mol/applicability/convex_hull.py b/scikit_mol/applicability/convex_hull.py index 4f46eb9..0ec3eaa 100644 --- a/scikit_mol/applicability/convex_hull.py +++ b/scikit_mol/applicability/convex_hull.py @@ -7,21 +7,38 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np +from numpy.typing import ArrayLike, NDArray from scipy import optimize -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted + +from .base import BaseApplicabilityDomain -class ConvexHullApplicabilityDomain(BaseEstimator, TransformerMixin): +class ConvexHullApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain defined as the convex hull of the training data. The convex hull approach determines if a point belongs to the convex hull of the training set by checking if it can be represented as a convex combination of training points. - The method is based on the `highs` solver from the `scipy.optimize` module, but is still - slow at inference time. + Parameters + ---------- + percentile : float or None, default=None + Not used, present for API consistency. + feature_prefix : str, default="ConvexHull" + Prefix for feature names in output. + + Notes + ----- + The method is based on the `highs` solver from `scipy.optimize`. Note that this + method can be computationally expensive for high-dimensional data or large + training sets, as it requires solving a linear programming problem for each + test point. + + For high-dimensional data (e.g., fingerprints), consider using dimensionality + reduction before applying this method. Attributes ---------- @@ -29,9 +46,22 @@ class ConvexHullApplicabilityDomain(BaseEstimator, TransformerMixin): Number of features seen during fit. points_ : ndarray of shape (n_features + 1, n_samples) Transformed training points used for convex hull calculations. + threshold_ : float + Fixed at 0.5 since output is binary (inside/outside hull). """ - def fit(self, X, y=None): + _scoring_convention = "high_outside" + _supports_threshold_fitting = False + + def __init__( + self, percentile: Optional[float] = None, feature_prefix: str = "ConvexHull" + ) -> None: + super().__init__(percentile=None, feature_prefix=feature_prefix) + self.threshold_ = 0.5 # Fixed threshold since output is binary + + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "ConvexHullApplicabilityDomain": """Fit the convex hull applicability domain. Parameters @@ -43,10 +73,10 @@ def fit(self, X, y=None): Returns ------- - self : object + self : ConvexHullApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = self._validate_data(X) self.n_features_in_ = X.shape[1] # Add ones column and transpose for convex hull calculations @@ -54,15 +84,12 @@ def fit(self, X, y=None): return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate distance from convex hull for each sample. - A distance of 0 indicates the sample lies within the convex hull. - Positive values indicate distance outside the hull. - Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -71,17 +98,10 @@ def transform(self, X): Distance from convex hull. Zero for points inside the hull, positive for points outside. """ - check_is_fitted(self) - X = check_array(X) - - # Calculate distances - if X.ndim == 1: - X = X.reshape(1, -1) - distances = [] for sample in X: # Append 1 to sample vector - sample_ext = np.r_[sample, 1].astype(np.float16) + sample_ext = np.r_[sample, 1].astype(np.float32) # Try to solve the linear programming problem result = optimize.linprog( @@ -109,5 +129,5 @@ def predict(self, X): Returns 1 for samples inside the domain and -1 for samples outside (following scikit-learn's convention for outlier detection). """ - scores = self.transform(X).ravel() + scores = self._transform(X).ravel() return np.where(scores == 0, 1, -1) diff --git a/scikit_mol/applicability/hotelling.py b/scikit_mol/applicability/hotelling.py index 1494d48..00aafb2 100644 --- a/scikit_mol/applicability/hotelling.py +++ b/scikit_mol/applicability/hotelling.py @@ -7,30 +7,38 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np +from numpy.typing import ArrayLike, NDArray from scipy.stats import f as f_dist -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted +from sklearn.utils import check_array + +from .base import BaseApplicabilityDomain -class HotellingT2ApplicabilityDomain(BaseEstimator, TransformerMixin): +class HotellingT2ApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on Hotelling's T² statistic. Uses Hotelling's T² statistic to define an elliptical confidence region around the training data. The threshold can be set using either the F-distribution (statistical approach) or adjusted using a validation set. - Lower volume protrusion scores indicate samples closer to the training - data center. By default, the threshold is set using the F-distribution - with a significance level of 0.05 (95% confidence). When using fit_threshold, - a target_percentile of 95 means that 95% of the validation samples with - the lowest protrusion scores will be considered inside the domain. - Parameters ---------- significance : float, default=0.05 Significance level for F-distribution threshold. - Only used if fit_threshold is not called. + percentile : float or None, default=None + If not None, overrides significance-based threshold. + Must be between 0 and 100. + feature_prefix : str, default="HotellingT2" + Prefix for feature names in output. + + Notes + ----- + Lower volume protrusion scores indicate samples closer to the training + data center. By default, the threshold is set using the F-distribution + with a significance level of 0.05 (95% confidence). Attributes ---------- @@ -41,52 +49,29 @@ class HotellingT2ApplicabilityDomain(BaseEstimator, TransformerMixin): threshold_ : float Current threshold for volume protrusions. - Examples - -------- - >>> from scikit_mol.applicability import HotellingT2ApplicabilityDomain - >>> ad = HotellingT2ApplicabilityDomain() - >>> # Using F-distribution threshold (default) - >>> ad.fit(X_train) - >>> predictions = ad.predict(X_test) - >>> - >>> # Adjusting threshold using validation set - >>> ad.fit_threshold(X_val, target_percentile=95) - >>> predictions = ad.predict(X_test) - References ---------- .. [1] Hotelling, H. (1931). The generalization of Student's ratio. The Annals of Mathematical Statistics, 2(3), 360-378. """ - def __init__(self, significance=0.05): + _scoring_convention = "high_outside" + _supports_threshold_fitting = True + + def __init__( + self, + significance: float = 0.05, + percentile: Optional[float] = None, + feature_prefix: str = "HotellingT2", + ) -> None: if not 0 < significance < 1: raise ValueError("significance must be between 0 and 1") + super().__init__(percentile=percentile, feature_prefix=feature_prefix) self.significance = significance - def fit(self, X, y=None): - """Fit the Hotelling T² applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Training data. - y : Ignored - Not used, present for API consistency. - - Returns - ------- - self : object - Returns the instance itself. - """ - X = check_array(X) - self.n_features_in_ = X.shape[1] + def _set_statistical_threshold(self, X: NDArray) -> None: + """Set threshold using F-distribution.""" n_samples = X.shape[0] - - # Determine the Hotelling T² ellipse - self.t2_ = np.sqrt((1 / n_samples) * (X**2).sum(axis=0)) - - # Set initial threshold using F-distribution f_stat = ( (n_samples - 1) / n_samples @@ -99,43 +84,43 @@ def fit(self, X, y=None): ) self.threshold_ = f_stat - return self - - def fit_threshold(self, X, target_percentile=95): - """Update the threshold using new data without refitting the model. + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "HotellingT2ApplicabilityDomain": + """Fit the Hotelling T² applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - target_percentile : float, default=95 - Target percentile of samples to include within domain. + Training data. + y : Ignored + Not used, present for API consistency. Returns ------- - self : object + self : HotellingT2ApplicabilityDomain Returns the instance itself. """ - check_is_fitted(self) - X = check_array(X) - - if not 0 <= target_percentile <= 100: - raise ValueError("target_percentile must be between 0 and 100") + X = check_array(X, **self._check_params) + self.n_features_in_ = X.shape[1] - # Calculate volume protrusions for validation set - scores = self.transform(X).ravel() + # Determine the Hotelling T² ellipse + self.t2_ = np.sqrt((1 / X.shape[0]) * (X**2).sum(axis=0)) - # Set threshold to achieve desired percentile (lower scores = inside domain) - self.threshold_ = np.percentile(scores, 100 - target_percentile) + # Set initial threshold + if self.percentile is not None: + self.fit_threshold(X) + else: + self._set_statistical_threshold(X) return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate volume protrusion scores for samples. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -144,26 +129,5 @@ def transform(self, X): The volume protrusion scores. Higher values indicate samples further from the training data center. """ - check_is_fitted(self) - X = check_array(X) - - # Calculate volume protrusions protrusions = (X**2 / self.t2_**2).sum(axis=1) return protrusions.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/isolation_forest.py b/scikit_mol/applicability/isolation_forest.py index 3ef23d3..ab1a3fa 100644 --- a/scikit_mol/applicability/isolation_forest.py +++ b/scikit_mol/applicability/isolation_forest.py @@ -7,13 +7,17 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin +from numpy.typing import ArrayLike, NDArray from sklearn.ensemble import IsolationForest -from sklearn.utils.validation import check_array, check_is_fitted +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class IsolationForestApplicabilityDomain(BaseEstimator, TransformerMixin): +class IsolationForestApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on Isolation Forest. Uses Isolation Forest to identify outliers based on the isolation depth @@ -25,8 +29,13 @@ class IsolationForestApplicabilityDomain(BaseEstimator, TransformerMixin): Number of trees in the forest. contamination : float, default=0.01 Expected proportion of outliers in the training data. - random_state : int or RandomState, default=None + random_state : Optional[int], default=None Controls the randomness of the forest. + percentile : float or None, default=None + Percentile of training set scores to use as threshold (0-100). + If None, uses contamination-based threshold from IsolationForest. + feature_prefix : str, default="IsolationForest" + Prefix for feature names in output. Attributes ---------- @@ -34,13 +43,13 @@ class IsolationForestApplicabilityDomain(BaseEstimator, TransformerMixin): Number of features seen during fit. iforest_ : IsolationForest Fitted isolation forest model. + threshold_ : float + Current threshold for domain membership. - Examples - -------- - >>> from scikit_mol.applicability import IsolationForestApplicabilityDomain - >>> ad = IsolationForestApplicabilityDomain(contamination=0.1) - >>> ad.fit(X_train) - >>> predictions = ad.predict(X_test) + Notes + ----- + The scoring convention is 'high_inside' because higher scores from + IsolationForest indicate samples more similar to the training data. References ---------- @@ -48,15 +57,27 @@ class IsolationForestApplicabilityDomain(BaseEstimator, TransformerMixin): In 2008 Eighth IEEE International Conference on Data Mining (pp. 413-422). """ - def __init__(self, n_estimators=100, contamination=0.01, random_state=None): + _scoring_convention = "high_inside" + _supports_threshold_fitting = True + + def __init__( + self, + n_estimators: int = 100, + contamination: float = 0.01, + random_state: Optional[int] = None, + percentile: Optional[float] = None, + feature_prefix: str = "IsolationForest", + ) -> None: if not 0 < contamination < 1: raise ValueError("contamination must be between 0 and 1") - + super().__init__(percentile=percentile, feature_prefix=feature_prefix) self.n_estimators = n_estimators self.contamination = contamination self.random_state = random_state - def fit(self, X, y=None): + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "IsolationForestApplicabilityDomain": """Fit the isolation forest applicability domain. Parameters @@ -68,10 +89,10 @@ def fit(self, X, y=None): Returns ------- - self : object + self : IsolationForestApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] self.iforest_ = IsolationForest( @@ -81,74 +102,76 @@ def fit(self, X, y=None): ) self.iforest_.fit(X) - self.fit_threshold(X) + # Set initial threshold + if self.percentile is not None: + self.fit_threshold(X) + else: + # Use IsolationForest's default threshold + self.threshold_ = self.iforest_.offset_ return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate anomaly scores for samples. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns ------- scores : ndarray of shape (n_samples, 1) The anomaly scores of the samples. - The lower the score, the more abnormal the sample. + Higher scores indicate samples more similar to training data. """ - check_is_fitted(self) - X = check_array(X) - scores = self.iforest_.score_samples(X) return scores.reshape(-1, 1) - def fit_threshold(self, X, target_percentile=95): - """Update the threshold using new data without refitting the model. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - target_percentile : float, default=95 - Target percentile of samples to include within domain. - - Returns - ------- - self : object - Returns the instance itself. - """ - check_is_fitted(self) - X = check_array(X) - - if not 0 <= target_percentile <= 100: - raise ValueError("target_percentile must be between 0 and 100") - - # Get decision function scores - scores = self.iforest_.score_samples(X) - - # Set threshold to achieve desired percentile - self.threshold_ = np.percentile(scores, 100 - target_percentile) - - return self - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - if hasattr(self, "threshold_"): - return np.where(scores > self.threshold_, 1, -1) - return self.iforest_.predict(X) + # def fit_threshold(self, X, target_percentile=95): + # """Update the threshold using new data without refitting the model. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # Data to compute threshold from. + # target_percentile : float, default=95 + # Target percentile of samples to include within domain. + + # Returns + # ------- + # self : object + # Returns the instance itself. + # """ + # check_is_fitted(self) + # X = check_array(X) + + # if not 0 <= target_percentile <= 100: + # raise ValueError("target_percentile must be between 0 and 100") + + # # Get decision function scores + # scores = self.iforest_.score_samples(X) + + # # Set threshold to achieve desired percentile + # self.threshold_ = np.percentile(scores, 100 - target_percentile) + + # return self + + # def predict(self, X): + # """Predict whether samples are within the applicability domain. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # The samples to predict. + + # Returns + # ------- + # y_pred : ndarray of shape (n_samples,) + # Returns 1 for samples inside the domain and -1 for samples outside + # (following scikit-learn's convention for outlier detection). + # """ + # scores = self._transform(X).ravel() + # if hasattr(self, "threshold_"): + # return np.where(scores > self.threshold_, 1, -1) + # return self.iforest_.predict(X) diff --git a/tests/applicability/conftest.py b/tests/applicability/conftest.py index 7047573..f822b1f 100644 --- a/tests/applicability/conftest.py +++ b/tests/applicability/conftest.py @@ -3,7 +3,14 @@ from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler -from scikit_mol.applicability import KNNApplicabilityDomain, LeverageApplicabilityDomain +from scikit_mol.applicability import ( + BoundingBoxApplicabilityDomain, + ConvexHullApplicabilityDomain, + HotellingT2ApplicabilityDomain, + IsolationForestApplicabilityDomain, + KNNApplicabilityDomain, + LeverageApplicabilityDomain, +) from scikit_mol.fingerprints import MorganFingerprintTransformer from ..fixtures import mols_list @@ -13,6 +20,17 @@ params=[ (KNNApplicabilityDomain, dict(n_neighbors=3)), (LeverageApplicabilityDomain, dict(threshold_factor=3)), + (BoundingBoxApplicabilityDomain, dict(percentile=(1, 99))), + (ConvexHullApplicabilityDomain, dict()), # No special parameters needed + (HotellingT2ApplicabilityDomain, dict(significance=0.05)), + ( + IsolationForestApplicabilityDomain, + dict( + n_estimators=100, + contamination=0.1, + random_state=42, # Add fixed random state + ), + ), ] ) def ad_estimator(request): diff --git a/tests/applicability/test_base.py b/tests/applicability/test_base.py index dd0df67..a7d7a58 100644 --- a/tests/applicability/test_base.py +++ b/tests/applicability/test_base.py @@ -54,8 +54,12 @@ def test_score_transform(ad_estimator, ad_test_data): assert mean_in > mean_out # Inside domain should have higher scores +@pytest.mark.threshold_fitting def test_threshold_setting(ad_estimator, reduced_fingerprints): """Test threshold setting and percentile behavior.""" + if not ad_estimator._supports_threshold_fitting: + pytest.skip("Estimator does not support threshold fitting") + # Test default threshold ad_estimator.fit(reduced_fingerprints) pred_default = ad_estimator.predict(reduced_fingerprints) @@ -107,10 +111,11 @@ def test_input_validation(ad_estimator): with pytest.raises(ValueError): ad_estimator.fit([[1], [2, 3]]) # Inconsistent dimensions - # Test invalid percentile - with pytest.raises(ValueError): - ad_estimator.percentile = 101 - ad_estimator.fit([[1, 2]]) + # Test invalid percentile only if threshold fitting is supported + if ad_estimator._supports_threshold_fitting: + with pytest.raises(ValueError): + ad_estimator.percentile = 101 + ad_estimator.fit([[1, 2]]) def test_refit_consistency(ad_estimator, reduced_fingerprints): @@ -122,6 +127,3 @@ def test_refit_consistency(ad_estimator, reduced_fingerprints): scores2 = ad_estimator.transform(reduced_fingerprints) assert_array_almost_equal(scores1, scores2) - - -# ... other common tests ... diff --git a/tests/applicability/test_bounding_box.py b/tests/applicability/test_bounding_box.py new file mode 100644 index 0000000..451ad43 --- /dev/null +++ b/tests/applicability/test_bounding_box.py @@ -0,0 +1,71 @@ +"""Tests specific to Bounding Box applicability domain.""" + +import numpy as np +import pytest +from sklearn.exceptions import NotFittedError +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler + +from scikit_mol.applicability import BoundingBoxApplicabilityDomain + + +def test_bounding_box_bounds(ad_test_data): + """Test the bounds calculation.""" + X_train, _, _ = ad_test_data + ad = BoundingBoxApplicabilityDomain(percentile=(1, 99)) + ad.fit(X_train) + + # Check bounds match numpy percentile + expected_min = np.percentile(X_train, 1, axis=0) + expected_max = np.percentile(X_train, 99, axis=0) + + assert np.allclose(ad.min_, expected_min) + assert np.allclose(ad.max_, expected_max) + + +def test_bounding_box_violations(): + """Test violation counting.""" + X_train = np.array([[1, 1], [2, 2], [3, 3]]) + X_test = np.array( + [ + [2, 2], # Inside bounds (0 violations) + [0, 2], # One violation + [0, 4], # Two violations + ] + ) + + ad = BoundingBoxApplicabilityDomain(percentile=(0, 100)) + ad.fit(X_train) + + scores = ad.transform(X_test) + assert scores[0, 0] == 0 # Inside bounds + assert scores[1, 0] == 1 # One violation + assert scores[2, 0] == 2 # Two violations + + +def test_bounding_box_percentile_validation(): + """Test percentile parameter validation.""" + # Invalid single percentile + with pytest.raises(ValueError): + BoundingBoxApplicabilityDomain(percentile=101) + + # Invalid tuple length + with pytest.raises(ValueError): + BoundingBoxApplicabilityDomain(percentile=(1, 2, 3)) + + # Invalid order + with pytest.raises(ValueError): + BoundingBoxApplicabilityDomain(percentile=(99, 1)) + + +def test_bounding_box_pipeline(): + """Test bounding box works in pipeline with scaling.""" + X = np.random.randn(10, 5) + pipe = Pipeline( + [("scaler", StandardScaler()), ("ad", BoundingBoxApplicabilityDomain())] + ) + + # Should run without errors + pipe.fit(X) + scores = pipe.transform(X) + assert scores.shape == (len(X), 1) diff --git a/tests/applicability/test_convex_hull.py b/tests/applicability/test_convex_hull.py new file mode 100644 index 0000000..53f430c --- /dev/null +++ b/tests/applicability/test_convex_hull.py @@ -0,0 +1,81 @@ +"""Tests specific to Convex Hull applicability domain.""" + +import numpy as np +import pytest +from sklearn.decomposition import PCA +from sklearn.exceptions import NotFittedError +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler + +from scikit_mol.applicability import ConvexHullApplicabilityDomain + + +def test_convex_hull_simple(): + """Test with simple 2D data where result is obvious.""" + # Create a triangle of points + X_train = np.array([[0, 0], [1, 0], [0, 1]]) + X_test = np.array( + [ + [0.5, 0.25], # Inside triangle + [2, 2], # Outside triangle + ] + ) + + ad = ConvexHullApplicabilityDomain() + ad.fit(X_train) + + scores = ad.transform(X_test) + assert scores[0, 0] == 0.0 # Inside point + assert scores[1, 0] == 1.0 # Outside point + + +def test_convex_hull_pipeline(): + """Test convex hull works in pipeline with dimensionality reduction.""" + pipe = Pipeline( + [ + ("scaler", StandardScaler()), + ("pca", PCA(n_components=2)), # Reduce to 2D for speed + ("ad", ConvexHullApplicabilityDomain()), + ] + ) + + # Generate random high-dimensional data + X = np.random.randn(10, 5) + + # Should run without errors + pipe.fit(X) + scores = pipe.transform(X) + assert scores.shape == (len(X), 1) + assert np.all((scores == 0) | (scores == 1)) # Binary output + + +def test_convex_hull_numerical_stability(): + """Test numerical stability with nearly colinear points.""" + X_train = np.array( + [ + [0, 0], + [1, 0], + [2, 1e-10], # Nearly colinear + ] + ) + X_test = np.array([[0.5, 0]]) + + ad = ConvexHullApplicabilityDomain() + ad.fit(X_train) + + # Should not raise and give consistent results + scores = ad.transform(X_test) + assert np.all(np.isfinite(scores)) + + +def test_convex_hull_single_point(): + """Test behavior with single point (degenerate hull).""" + X_train = np.array([[1, 1]]) + X_test = np.array([[1, 1], [2, 2]]) + + ad = ConvexHullApplicabilityDomain() + ad.fit(X_train) + + scores = ad.transform(X_test) + assert scores[0, 0] == 0.0 # Same point + assert scores[1, 0] == 1.0 # Different point diff --git a/tests/applicability/test_hotelling.py b/tests/applicability/test_hotelling.py new file mode 100644 index 0000000..c3045fd --- /dev/null +++ b/tests/applicability/test_hotelling.py @@ -0,0 +1,87 @@ +"""Tests specific to Hotelling T² applicability domain.""" + +import numpy as np +import pytest +from sklearn.exceptions import NotFittedError +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler + +from scikit_mol.applicability import HotellingT2ApplicabilityDomain + + +def test_hotelling_threshold(): + """Test F-distribution threshold calculation.""" + X = np.random.randn(100, 3) # 100 samples, 3 features + + ad = HotellingT2ApplicabilityDomain(significance=0.05) + ad.fit(X) + + # Threshold should be positive + assert ad.threshold_ > 0 + + # More stringent significance should give higher threshold + ad_strict = HotellingT2ApplicabilityDomain(significance=0.01) + ad_strict.fit(X) + assert ad_strict.threshold_ > ad.threshold_ + + +def test_hotelling_scores(): + """Test score calculation with known data.""" + # Create data with known center and spread + X_train = np.array([[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) + + X_test = np.array( + [ + [0, 0], # Center point + [2, 0], # Further out + [10, 10], # Far out + ] + ) + + ad = HotellingT2ApplicabilityDomain() + ad.fit(X_train) + + scores = ad.transform(X_test) + + # Scores should increase with distance from center + assert scores[0, 0] < scores[1, 0] < scores[2, 0] + + +def test_hotelling_significance_validation(): + """Test significance parameter validation.""" + with pytest.raises(ValueError): + HotellingT2ApplicabilityDomain(significance=0) + + with pytest.raises(ValueError): + HotellingT2ApplicabilityDomain(significance=1) + + with pytest.raises(ValueError): + HotellingT2ApplicabilityDomain(significance=-0.5) + + +def test_hotelling_pipeline(): + """Test Hotelling works in pipeline with scaling.""" + pipe = Pipeline( + [("scaler", StandardScaler()), ("ad", HotellingT2ApplicabilityDomain())] + ) + + X = np.random.randn(10, 5) + + # Should run without errors + pipe.fit(X) + scores = pipe.transform(X) + assert scores.shape == (len(X), 1) + assert np.all(scores >= 0) # Scores should be non-negative + + +def test_hotelling_threshold_fitting(): + """Test threshold fitting with percentile.""" + X = np.random.randn(100, 3) + + ad = HotellingT2ApplicabilityDomain(percentile=90) + ad.fit(X) + + # Get scores and check threshold matches 90th percentile + scores = ad.transform(X) + expected_threshold = np.percentile(scores, 90) + assert np.isclose(ad.threshold_, expected_threshold) diff --git a/tests/applicability/test_isolation_forest.py b/tests/applicability/test_isolation_forest.py new file mode 100644 index 0000000..f546b52 --- /dev/null +++ b/tests/applicability/test_isolation_forest.py @@ -0,0 +1,25 @@ +"""Tests specific to Isolation Forest applicability domain.""" + +import numpy as np + +from scikit_mol.applicability import IsolationForestApplicabilityDomain + + +def test_refit_consistency(): + """Test consistency when refitting with same data.""" + X = np.random.RandomState(42).normal(0, 1, (100, 2)) + + # Use fixed random state + ad = IsolationForestApplicabilityDomain( + n_estimators=100, contamination=0.1, random_state=42 + ) + + # First fit + ad.fit(X) + scores1 = ad.transform(X) + + # Second fit + ad.fit(X) + scores2 = ad.transform(X) + + assert np.allclose(scores1, scores2) From 9cafe9f3704476740b8641c9a949ebdebe170c51 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Mon, 10 Feb 2025 16:06:36 +0100 Subject: [PATCH 09/24] Moved rest of AD estimators. All test runs. --- scikit_mol/applicability/base.py | 9 +- scikit_mol/applicability/bounding_box.py | 6 +- scikit_mol/applicability/convex_hull.py | 38 ++-- scikit_mol/applicability/hotelling.py | 6 +- scikit_mol/applicability/isolation_forest.py | 8 +- scikit_mol/applicability/kernel_density.py | 125 +++++++------ scikit_mol/applicability/knn.py | 9 +- scikit_mol/applicability/leverage.py | 4 +- scikit_mol/applicability/local_outlier.py | 92 +++++---- scikit_mol/applicability/mahalanobis.py | 186 +++++++++---------- scikit_mol/applicability/standardization.py | 156 ++++++---------- scikit_mol/applicability/topkat.py | 91 +++++---- tests/applicability/conftest.py | 18 ++ tests/applicability/test_base.py | 6 +- tests/applicability/test_kernel_density.py | 44 +++++ tests/applicability/test_local_outlier.py | 64 +++++++ tests/applicability/test_mahalanobis.py | 66 +++++++ tests/applicability/test_standardization.py | 76 ++++++++ tests/applicability/test_topkat.py | 55 ++++++ tests/conftest.py | 19 +- tests/test_metrics.py | 91 +++++++++ 21 files changed, 790 insertions(+), 379 deletions(-) create mode 100644 tests/applicability/test_kernel_density.py create mode 100644 tests/applicability/test_local_outlier.py create mode 100644 tests/applicability/test_mahalanobis.py create mode 100644 tests/applicability/test_standardization.py create mode 100644 tests/applicability/test_topkat.py create mode 100644 tests/test_metrics.py diff --git a/scikit_mol/applicability/base.py b/scikit_mol/applicability/base.py index cf7d994..c8cbb62 100644 --- a/scikit_mol/applicability/base.py +++ b/scikit_mol/applicability/base.py @@ -55,6 +55,8 @@ class BaseApplicabilityDomain(BaseEstimator, TransformerMixin, _ADOutputMixin, A If None: - For methods with statistical thresholds: use statistical method - For percentile-only methods: use 99.0 (include 99% of training samples) + feature_name : str, default="AD_estimator" + Name for the output feature column. Notes ----- @@ -78,7 +80,7 @@ class BaseApplicabilityDomain(BaseEstimator, TransformerMixin, _ADOutputMixin, A _scoring_convention: ClassVar[str] # Must be set by subclasses def __init__( - self, percentile: Optional[float] = None, feature_prefix: str = "AD_estimator" + self, percentile: Optional[float] = None, feature_name: str = "AD_estimator" ) -> None: if not hasattr(self, "_scoring_convention"): raise TypeError( @@ -93,7 +95,7 @@ def __init__( if percentile is not None and not 0 <= percentile <= 100: raise ValueError("percentile must be between 0 and 100") self.percentile = percentile - self.feature_prefix = feature_prefix + self.feature_name = feature_name self._check_params = { "estimator": self, "accept_sparse": False, @@ -244,5 +246,4 @@ def score_transform( def get_feature_names_out(self) -> NDArray[np.str_]: """Get feature name for output column.""" - - return np.array([f"{self.feature_prefix}"]) + return np.array([f"{self.feature_name}"]) diff --git a/scikit_mol/applicability/bounding_box.py b/scikit_mol/applicability/bounding_box.py index 6bd35b7..9d8ec4a 100644 --- a/scikit_mol/applicability/bounding_box.py +++ b/scikit_mol/applicability/bounding_box.py @@ -27,7 +27,7 @@ class BoundingBoxApplicabilityDomain(BaseApplicabilityDomain): percentile : float or tuple of float, default=(0.1, 99.9) Percentile(s) of the training set distribution used to define the bounding box. If float, uses (percentile, 100-percentile). - feature_prefix : str, default="BoundingBox" + feature_name : str, default="BoundingBox" Prefix for feature names in output. Attributes @@ -73,9 +73,9 @@ class BoundingBoxApplicabilityDomain(BaseApplicabilityDomain): def __init__( self, percentile: Union[float, Tuple[float, float]] = (0.1, 99.9), - feature_prefix: str = "BoundingBox", + feature_name: str = "BoundingBox", ) -> None: - super().__init__(percentile=None, feature_prefix=feature_prefix) + super().__init__(percentile=None, feature_name=feature_name) if isinstance(percentile, (int, float)): if not 0 <= percentile <= 100: diff --git a/scikit_mol/applicability/convex_hull.py b/scikit_mol/applicability/convex_hull.py index 0ec3eaa..9b7a486 100644 --- a/scikit_mol/applicability/convex_hull.py +++ b/scikit_mol/applicability/convex_hull.py @@ -27,7 +27,7 @@ class ConvexHullApplicabilityDomain(BaseApplicabilityDomain): ---------- percentile : float or None, default=None Not used, present for API consistency. - feature_prefix : str, default="ConvexHull" + feature_name : str, default="ConvexHull" Prefix for feature names in output. Notes @@ -54,9 +54,9 @@ class ConvexHullApplicabilityDomain(BaseApplicabilityDomain): _supports_threshold_fitting = False def __init__( - self, percentile: Optional[float] = None, feature_prefix: str = "ConvexHull" + self, percentile: Optional[float] = None, feature_name: str = "ConvexHull" ) -> None: - super().__init__(percentile=None, feature_prefix=feature_prefix) + super().__init__(percentile=None, feature_name=feature_name) self.threshold_ = 0.5 # Fixed threshold since output is binary def fit( @@ -115,19 +115,19 @@ def _transform(self, X: NDArray) -> NDArray[np.float64]: return np.array(distances).reshape(-1, 1) - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self._transform(X).ravel() - return np.where(scores == 0, 1, -1) + # def predict(self, X): + # """Predict whether samples are within the applicability domain. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # The samples to predict. + + # Returns + # ------- + # y_pred : ndarray of shape (n_samples,) + # Returns 1 for samples inside the domain and -1 for samples outside + # (following scikit-learn's convention for outlier detection). + # """ + # scores = self._transform(X).ravel() + # return np.where(scores == 0, 1, -1) diff --git a/scikit_mol/applicability/hotelling.py b/scikit_mol/applicability/hotelling.py index 00aafb2..2f9b6b8 100644 --- a/scikit_mol/applicability/hotelling.py +++ b/scikit_mol/applicability/hotelling.py @@ -31,7 +31,7 @@ class HotellingT2ApplicabilityDomain(BaseApplicabilityDomain): percentile : float or None, default=None If not None, overrides significance-based threshold. Must be between 0 and 100. - feature_prefix : str, default="HotellingT2" + feature_name : str, default="HotellingT2" Prefix for feature names in output. Notes @@ -62,11 +62,11 @@ def __init__( self, significance: float = 0.05, percentile: Optional[float] = None, - feature_prefix: str = "HotellingT2", + feature_name: str = "HotellingT2", ) -> None: if not 0 < significance < 1: raise ValueError("significance must be between 0 and 1") - super().__init__(percentile=percentile, feature_prefix=feature_prefix) + super().__init__(percentile=percentile, feature_name=feature_name) self.significance = significance def _set_statistical_threshold(self, X: NDArray) -> None: diff --git a/scikit_mol/applicability/isolation_forest.py b/scikit_mol/applicability/isolation_forest.py index ab1a3fa..ee29659 100644 --- a/scikit_mol/applicability/isolation_forest.py +++ b/scikit_mol/applicability/isolation_forest.py @@ -34,8 +34,8 @@ class IsolationForestApplicabilityDomain(BaseApplicabilityDomain): percentile : float or None, default=None Percentile of training set scores to use as threshold (0-100). If None, uses contamination-based threshold from IsolationForest. - feature_prefix : str, default="IsolationForest" - Prefix for feature names in output. + feature_name : str, default="IsolationForest" + Name for feature names in output. Attributes ---------- @@ -66,11 +66,11 @@ def __init__( contamination: float = 0.01, random_state: Optional[int] = None, percentile: Optional[float] = None, - feature_prefix: str = "IsolationForest", + feature_name: str = "IsolationForest", ) -> None: if not 0 < contamination < 1: raise ValueError("contamination must be between 0 and 1") - super().__init__(percentile=percentile, feature_prefix=feature_prefix) + super().__init__(percentile=percentile, feature_name=feature_name) self.n_estimators = n_estimators self.contamination = contamination self.random_state = random_state diff --git a/scikit_mol/applicability/kernel_density.py b/scikit_mol/applicability/kernel_density.py index 40125f1..3f7281d 100644 --- a/scikit_mol/applicability/kernel_density.py +++ b/scikit_mol/applicability/kernel_density.py @@ -7,13 +7,17 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin +from numpy.typing import ArrayLike, NDArray from sklearn.neighbors import KernelDensity -from sklearn.utils.validation import check_array, check_is_fitted +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class KernelDensityApplicabilityDomain(BaseEstimator, TransformerMixin): +class KernelDensityApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on kernel density estimation. Uses kernel density estimation to model the distribution of the training data. @@ -27,9 +31,11 @@ class KernelDensityApplicabilityDomain(BaseEstimator, TransformerMixin): kernel : str, default='gaussian' The kernel to use. Options: ['gaussian', 'tophat', 'epanechnikov', 'exponential', 'linear', 'cosine']. - percentile : float, default=1.0 - The percentile of training set densities to use as threshold. - Must be between 0 and 100. + percentile : float or None, default=None + The percentile of training set densities to use as threshold (0-100). + If None, uses 1.0 (exclude bottom 1% of training samples). + feature_name : str, default="KernelDensity" + Name for the output feature column. Attributes ---------- @@ -40,6 +46,11 @@ class KernelDensityApplicabilityDomain(BaseEstimator, TransformerMixin): threshold_ : float Density threshold for domain membership. + Notes + ----- + The scoring convention is 'high_inside' because higher density scores + indicate samples more similar to the training data. + Examples -------- >>> from scikit_mol.applicability import KernelDensityApplicabilityDomain @@ -48,30 +59,37 @@ class KernelDensityApplicabilityDomain(BaseEstimator, TransformerMixin): >>> predictions = ad.predict(X_test) """ - def __init__(self, bandwidth=1.0, kernel="gaussian", percentile=1.0): - if not 0 <= percentile <= 100: - raise ValueError("percentile must be between 0 and 100") + _scoring_convention = "high_inside" + def __init__( + self, + bandwidth: float = 1.0, + kernel: str = "gaussian", + percentile: Optional[float] = None, + feature_name: str = "KernelDensity", + ) -> None: + super().__init__(percentile=percentile or 1.0, feature_name=feature_name) self.bandwidth = bandwidth self.kernel = kernel - self.percentile = percentile - def fit(self, X, y=None): + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "KernelDensityApplicabilityDomain": """Fit the kernel density applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) Training data. - y : Ignored + y : Any, optional (default=None) Not used, present for API consistency. Returns ------- - self : object + self : KernelDensityApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] # Fit KDE @@ -83,12 +101,12 @@ def fit(self, X, y=None): return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate log density scores for samples. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -97,47 +115,40 @@ def transform(self, X): The log density scores of the samples. Higher scores indicate samples more similar to the training data. """ - check_is_fitted(self) - X = check_array(X) - scores = self.kde_.score_samples(X) return scores.reshape(-1, 1) - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - return np.where(scores >= self.threshold_, 1, -1) - - def fit_threshold(self, X): - """Update the threshold using new data without refitting the model. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - - Returns - ------- - self : object - Returns the instance itself. - """ - check_is_fitted(self) - X = check_array(X) - - # Calculate density threshold from provided data - densities = self.kde_.score_samples(X) - self.threshold_ = np.percentile(densities, self.percentile) - - return self + # def predict(self, X): + # """Predict whether samples are within the applicability domain. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # The samples to predict. + + # Returns + # ------- + # y_pred : ndarray of shape (n_samples,) + # Returns 1 for samples inside the domain and -1 for samples outside + # (following scikit-learn's convention for outlier detection). + # """ + # scores = self._transform(X).ravel() + # return np.where(scores >= self.threshold_, 1, -1) + + # def fit_threshold(self, X): + # """Update the threshold using new data without refitting the model. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # Data to compute threshold from. + + # Returns + # ------- + # self : object + # Returns the instance itself. + # """ + # densities = self._transform(X).ravel() + # self.threshold_ = np.percentile(densities, self.percentile) + + # return self diff --git a/scikit_mol/applicability/knn.py b/scikit_mol/applicability/knn.py index 749980e..c9be686 100644 --- a/scikit_mol/applicability/knn.py +++ b/scikit_mol/applicability/knn.py @@ -12,6 +12,7 @@ import numpy as np from numpy.typing import ArrayLike from sklearn.neighbors import NearestNeighbors +from sklearn.utils import check_array from .base import BaseApplicabilityDomain @@ -44,7 +45,7 @@ class KNNApplicabilityDomain(BaseApplicabilityDomain): Number of parallel jobs to run for neighbors search. None means 1 unless in a joblib.parallel_backend context. -1 means using all processors. - feature_prefix : str, default='KNN' + feature_name : str, default='KNN' Prefix for feature names in output. Notes @@ -96,9 +97,9 @@ def __init__( percentile: Optional[float] = None, distance_metric: Union[str, Callable] = "euclidean", n_jobs: Optional[int] = None, - feature_prefix: str = "KNN", + feature_name: str = "KNN", ) -> None: - super().__init__(percentile=percentile, feature_prefix=feature_prefix) + super().__init__(percentile=percentile, feature_name=feature_name) self.n_neighbors = n_neighbors self.distance_metric = distance_metric self.n_jobs = n_jobs @@ -134,7 +135,7 @@ def fit(self, X: ArrayLike, y=None) -> "KNNApplicabilityDomain": if not isinstance(self.n_neighbors, int) or self.n_neighbors < 1: raise ValueError("n_neighbors must be a positive integer") - X = self._validate_data(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] # Fit nearest neighbors model diff --git a/scikit_mol/applicability/leverage.py b/scikit_mol/applicability/leverage.py index fee276c..d5a9e01 100644 --- a/scikit_mol/applicability/leverage.py +++ b/scikit_mol/applicability/leverage.py @@ -82,9 +82,9 @@ def __init__( self, threshold_factor: float = 3, percentile: Optional[float] = None, - feature_prefix: str = "Leverage", + feature_name: str = "Leverage", ) -> None: - super().__init__(percentile=percentile, feature_prefix=feature_prefix) + super().__init__(percentile=percentile, feature_name=feature_name) self.threshold_factor = threshold_factor def _set_statistical_threshold(self, X: NDArray) -> None: diff --git a/scikit_mol/applicability/local_outlier.py b/scikit_mol/applicability/local_outlier.py index 3fd181f..9366723 100644 --- a/scikit_mol/applicability/local_outlier.py +++ b/scikit_mol/applicability/local_outlier.py @@ -7,12 +7,17 @@ See LICENSE.MIT in this directory for the original MIT license. """ -from sklearn.base import BaseEstimator, TransformerMixin +from typing import Any, Optional + +import numpy as np +from numpy.typing import ArrayLike, NDArray from sklearn.neighbors import LocalOutlierFactor -from sklearn.utils.validation import check_array, check_is_fitted +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class LocalOutlierFactorApplicabilityDomain(BaseEstimator, TransformerMixin): +class LocalOutlierFactorApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on Local Outlier Factor (LOF). LOF measures the local deviation of density of a sample with respect to its @@ -27,6 +32,11 @@ class LocalOutlierFactorApplicabilityDomain(BaseEstimator, TransformerMixin): Expected proportion of outliers in the data set. metric : str, default='euclidean' Metric to use for distance computation. + percentile : float or None, default=None + Percentile of training set scores to use as threshold (0-100). + If None, uses contamination-based threshold from LOF. + feature_name : str, default="LOF" + Name for the output feature column. Attributes ---------- @@ -34,13 +44,13 @@ class LocalOutlierFactorApplicabilityDomain(BaseEstimator, TransformerMixin): Number of features seen during fit. lof_ : LocalOutlierFactor Fitted LOF estimator. + threshold_ : float + Current threshold for domain membership. - Examples - -------- - >>> from scikit_mol.applicability import LocalOutlierFactorApplicabilityDomain - >>> ad = LocalOutlierFactorApplicabilityDomain() - >>> ad.fit(X_train) - >>> predictions = ad.predict(X_test) + Notes + ----- + The scoring convention is 'high_outside' because higher LOF scores + indicate samples that are more likely to be outliers. References ---------- @@ -48,27 +58,39 @@ class LocalOutlierFactorApplicabilityDomain(BaseEstimator, TransformerMixin): In: Proc. 2000 ACM SIGMOD Int. Conf. Manag. Data, ACM, pp. 93-104. """ - def __init__(self, n_neighbors=20, contamination=0.1, metric="euclidean"): + _scoring_convention = "high_outside" + + def __init__( + self, + n_neighbors: int = 20, + contamination: float = 0.1, + metric: str = "euclidean", + percentile: Optional[float] = None, + feature_name: str = "LOF", + ) -> None: + super().__init__(percentile=percentile, feature_name=feature_name) self.n_neighbors = n_neighbors self.contamination = contamination self.metric = metric - def fit(self, X, y=None): + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "LocalOutlierFactorApplicabilityDomain": """Fit the LOF applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) Training data. - y : Ignored + y : Any, optional (default=None) Not used, present for API consistency. Returns ------- - self : object + self : LocalOutlierFactorApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] self.lof_ = LocalOutlierFactor( @@ -79,14 +101,17 @@ def fit(self, X, y=None): ) self.lof_.fit(X) + # Set initial threshold based on training data + self.fit_threshold(X) + return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate LOF scores for samples. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -95,25 +120,22 @@ def transform(self, X): The LOF scores of the samples. Higher scores indicate samples that are more likely to be outliers. """ - check_is_fitted(self) - X = check_array(X) - - # Get negative LOF scores (higher means more likely to be inlier) + # Get negative LOF scores (higher means more likely to be outlier) scores = -self.lof_.score_samples(X) return scores.reshape(-1, 1) - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - return self.lof_.predict(X) + # def predict(self, X): + # """Predict whether samples are within the applicability domain. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # The samples to predict. + + # Returns + # ------- + # y_pred : ndarray of shape (n_samples,) + # Returns 1 for samples inside the domain and -1 for samples outside + # (following scikit-learn's convention for outlier detection). + # """ + # return self.lof_.predict(X) diff --git a/scikit_mol/applicability/mahalanobis.py b/scikit_mol/applicability/mahalanobis.py index 4be40de..61cb493 100644 --- a/scikit_mol/applicability/mahalanobis.py +++ b/scikit_mol/applicability/mahalanobis.py @@ -2,13 +2,17 @@ Mahalanobis distance applicability domain. """ +from typing import Any, Optional + import numpy as np -from scipy import stats -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted +from numpy.typing import ArrayLike, NDArray +from scipy import linalg, stats +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class MahalanobisApplicabilityDomain(BaseEstimator, TransformerMixin): +class MahalanobisApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on Mahalanobis distance. Uses Mahalanobis distance to measure how many standard deviations a sample @@ -18,9 +22,11 @@ class MahalanobisApplicabilityDomain(BaseEstimator, TransformerMixin): Parameters ---------- - percentile : float, default=95.0 - Percentile for the confidence region (0-100). - Default 95.0 corresponds to ~2 standard deviations. + percentile : float or None, default=None + Percentile of training set scores to use as threshold (0-100). + If None, uses 95.0 (exclude top 5% of training samples). + feature_name : str, default="Mahalanobis" + Name for the output feature column. Attributes ---------- @@ -30,129 +36,107 @@ class MahalanobisApplicabilityDomain(BaseEstimator, TransformerMixin): Mean of training data. covariance_ : ndarray of shape (n_features, n_features) Covariance matrix of training data. - inv_covariance_ : ndarray of shape (n_features, n_features) - Inverse covariance matrix. threshold_ : float - Current threshold for Mahalanobis distances. - - Examples - -------- - >>> from scikit_mol.applicability import MahalanobisApplicabilityDomain - >>> ad = MahalanobisApplicabilityDomain(percentile=95) - >>> ad.fit(X_train) - >>> # Using chi-square threshold (default) - >>> predictions = ad.predict(X_test) - >>> - >>> # Adjusting threshold using validation set - >>> ad.fit_threshold(X_val, target_percentile=95) - >>> predictions = ad.predict(X_test) - - References - ---------- - .. [1] De Maesschalck, R., Jouan-Rimbaud, D., & Massart, D. L. (2000). - The Mahalanobis distance. Chemometrics and intelligent laboratory - systems, 50(1), 1-18. + Current threshold for domain membership. + + Notes + ----- + The scoring convention is 'high_outside' because higher Mahalanobis + distances indicate samples further from the training data mean. """ - def __init__(self, percentile=95.0): - if not 0 <= percentile <= 100: - raise ValueError("percentile must be between 0 and 100") - self.percentile = percentile + _scoring_convention = "high_outside" - def fit(self, X, y=None): - """Fit the Mahalanobis distance applicability domain. + def __init__( + self, + percentile: Optional[float] = None, + feature_name: str = "Mahalanobis", + ) -> None: + super().__init__(percentile=percentile or 95.0, feature_name=feature_name) - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Training data. - y : Ignored - Not used, present for API consistency. + def _set_statistical_threshold(self, X: NDArray) -> None: + """Set threshold based on chi-square distribution. - Returns - ------- - self : object - Returns the instance itself. + For multivariate normal data, squared Mahalanobis distances follow + a chi-square distribution with degrees of freedom equal to the + number of features. """ - X = check_array(X) - self.n_features_in_ = X.shape[1] - - # Compute mean and covariance - self.mean_ = np.mean(X, axis=0) - self.covariance_ = np.cov(X, rowvar=False) - self.inv_covariance_ = np.linalg.inv(self.covariance_) - - # Set initial threshold using chi-square distribution - self.threshold_ = stats.chi2.ppf(self.percentile / 100, df=self.n_features_in_) - - return self + df = self.n_features_in_ + self.threshold_ = np.sqrt(stats.chi2.ppf(0.95, df)) - def fit_threshold(self, X, target_percentile=95): - """Update the threshold using new data without refitting the model. + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "MahalanobisApplicabilityDomain": + """Fit the Mahalanobis distance applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - target_percentile : float, default=95 - Target percentile of samples to include within domain. + Training data. + y : Any, optional (default=None) + Not used, present for API consistency. Returns ------- - self : object + self : MahalanobisApplicabilityDomain Returns the instance itself. + + Raises + ------ + ValueError + If X has fewer samples than features, making covariance estimation unstable. """ - check_is_fitted(self) - X = check_array(X) + X = check_array(X, **self._check_params) + n_samples, n_features = X.shape + self.n_features_in_ = n_features - if not 0 <= target_percentile <= 100: - raise ValueError("target_percentile must be between 0 and 100") + if n_samples <= n_features: + raise ValueError( + f"n_samples ({n_samples}) must be greater than n_features ({n_features}) " + "for stable covariance estimation." + ) - # Calculate distances for validation set - scores = self.transform(X).ravel() + # Calculate mean and covariance + self.mean_ = np.mean(X, axis=0) + self.covariance_ = np.cov(X, rowvar=False, ddof=1) - # Set threshold to achieve desired percentile (lower distances = inside domain) - self.threshold_ = np.percentile(scores, 100 - target_percentile) + # Add small regularization to ensure positive definiteness + min_eig = np.min(linalg.eigvalsh(self.covariance_)) + if min_eig < 1e-6: + self.covariance_ += (abs(min_eig) + 1e-6) * np.eye(n_features) + + # Set initial threshold based on training data + self.fit_threshold(X) return self - def transform(self, X): - """Calculate Mahalanobis distances for samples. + def _transform(self, X: NDArray) -> NDArray[np.float64]: + """Calculate Mahalanobis distances. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns ------- distances : ndarray of shape (n_samples, 1) - The Mahalanobis distances. Higher values indicate samples - further from the training data center. - """ - check_is_fitted(self) - X = check_array(X) - - # Center the data - X_centered = X - self.mean_ - - # Calculate Mahalanobis distances - distances = np.sum(X_centered @ self.inv_covariance_ * X_centered, axis=1) - return distances.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). + The Mahalanobis distances of the samples. Higher distances indicate + samples further from the training data mean. """ - scores = self.transform(X).ravel() - return np.where(scores <= self.threshold_, 1, -1) + # Calculate Mahalanobis distances using stable computation + diff = X - self.mean_ + try: + # Try Cholesky decomposition first (more stable) + L = linalg.cholesky(self.covariance_, lower=True) + mahal_dist = np.sqrt( + np.sum(linalg.solve_triangular(L, diff.T, lower=True) ** 2, axis=0) + ) + except linalg.LinAlgError: + # Fallback to standard computation if Cholesky fails + inv_covariance = linalg.pinv( + self.covariance_ + ) # Use pseudo-inverse for stability + mahal_dist = np.sqrt(np.sum(diff @ inv_covariance * diff, axis=1)) + + return mahal_dist.reshape(-1, 1) diff --git a/scikit_mol/applicability/standardization.py b/scikit_mol/applicability/standardization.py index c1e1414..babeeba 100644 --- a/scikit_mol/applicability/standardization.py +++ b/scikit_mol/applicability/standardization.py @@ -7,26 +7,32 @@ See LICENSE.MIT in this directory for the original MIT license. """ +from typing import Any, Optional + import numpy as np +from numpy.typing import ArrayLike, NDArray from scipy import stats -from sklearn.base import BaseEstimator, TransformerMixin from sklearn.preprocessing import StandardScaler -from sklearn.utils.validation import check_array, check_is_fitted +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class StandardizationApplicabilityDomain(BaseEstimator, TransformerMixin): +class StandardizationApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain based on standardized feature values. Samples are considered within the domain if their standardized features - have a mean + z * std <= threshold, or if their maximum standardized - value <= threshold, where z corresponds to the specified percentile - assuming a normal distribution. + fall within a certain number of standard deviations from the mean. + The maximum absolute standardized value across all features is used + as the score. Parameters ---------- - percentile : float, default=95.0 - Percentile for the confidence interval (0-100). - Default 95.0 corresponds to ~2 standard deviations. + percentile : float or None, default=None + Percentile of training set scores to use as threshold (0-100). + If None, uses 95.0 (exclude top 5% of training samples). + feature_name : str, default="Standardization" + Name for the output feature column. Attributes ---------- @@ -35,131 +41,75 @@ class StandardizationApplicabilityDomain(BaseEstimator, TransformerMixin): scaler_ : StandardScaler Fitted standard scaler. threshold_ : float - Current threshold for standardized values. - - Examples - -------- - >>> from scikit_mol.applicability import StandardizationApplicabilityDomain - >>> ad = StandardizationApplicabilityDomain(percentile=95) - >>> ad.fit(X_train) - >>> # Optionally adjust threshold using validation set - >>> ad.fit_threshold(X_val, target_percentile=95) - >>> predictions = ad.predict(X_test) - - References - ---------- - .. [1] Roy, K., Kar, S., & Ambure, P. (2015). On a simple approach for - determining applicability domain of QSAR models. Chemometrics and - Intelligent Laboratory Systems, 145, 22-29. + Current threshold for domain membership. + + Notes + ----- + The scoring convention is 'high_outside' because higher standardized + values indicate samples further from the training data mean. """ - def __init__(self, percentile=95.0): - if not 0 <= percentile <= 100: - raise ValueError("percentile must be between 0 and 100") - self.percentile = percentile + _scoring_convention = "high_outside" + + def __init__( + self, + percentile: Optional[float] = None, + feature_name: str = "Standardization", + ) -> None: + super().__init__(percentile=percentile or 95.0, feature_name=feature_name) - def fit(self, X, y=None): + def _set_statistical_threshold(self, X: NDArray) -> None: + """Set threshold based on normal distribution. + + For normally distributed data, ~95% of values fall within + 2 standard deviations of the mean. + """ + self.threshold_ = stats.norm.ppf(0.975) # 2 standard deviations + + def fit( + self, X: ArrayLike, y: Optional[Any] = None + ) -> "StandardizationApplicabilityDomain": """Fit the standardization applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) Training data. - y : Ignored + y : Any, optional (default=None) Not used, present for API consistency. Returns ------- - self : object + self : StandardizationApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] + # Fit standard scaler self.scaler_ = StandardScaler() self.scaler_.fit(X) - # Convert percentile to z-score for initial threshold - self.threshold_ = stats.norm.ppf(self.percentile / 100) - - return self - - def fit_threshold(self, X, target_percentile=95): - """Update the threshold using new data without refitting the model. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Data to compute threshold from. - target_percentile : float, default=95 - Target percentile of samples to include within domain. - - Returns - ------- - self : object - Returns the instance itself. - """ - check_is_fitted(self) - X = check_array(X) - - if not 0 <= target_percentile <= 100: - raise ValueError("target_percentile must be between 0 and 100") - - # Calculate scores for the provided data - scores = self.transform(X).ravel() - - # Set threshold to achieve desired percentile - self.threshold_ = np.percentile(scores, target_percentile) + # Set initial threshold based on training data + self.fit_threshold(X) return self - def transform(self, X): - """Calculate standardized feature statistics for samples. + def _transform(self, X: NDArray) -> NDArray[np.float64]: + """Calculate maximum absolute standardized values. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns ------- scores : ndarray of shape (n_samples, 1) - The maximum of: - 1. Maximum absolute standardized value - 2. Mean + z * std of standardized values - where z corresponds to the specified percentile. - Higher values indicate samples further from the training data. + The maximum absolute standardized values. Higher values indicate + samples further from the training data mean. """ - check_is_fitted(self) - X = check_array(X) - - # Standardize features + # Calculate standardized values and take max absolute value per sample X_std = self.scaler_.transform(X) - - # Calculate statistics - max_vals = np.max(np.abs(X_std), axis=1) - means = np.mean(X_std, axis=1) - stds = np.std(X_std, axis=1) - z_score = -stats.norm.ppf(self.percentile / 100) # negative for lower tail - mean_std = means + z_score * stds - - # Return maximum of the two criteria - scores = np.maximum(max_vals, mean_std) + scores = np.max(np.abs(X_std), axis=1) return scores.reshape(-1, 1) - - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - return np.where(scores <= self.threshold_, 1, -1) diff --git a/scikit_mol/applicability/topkat.py b/scikit_mol/applicability/topkat.py index 1392c65..4849fc3 100644 --- a/scikit_mol/applicability/topkat.py +++ b/scikit_mol/applicability/topkat.py @@ -8,17 +8,28 @@ """ +from typing import Any, Optional + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.utils.validation import check_array, check_is_fitted +from numpy.typing import ArrayLike, NDArray +from sklearn.utils.validation import check_array + +from .base import BaseApplicabilityDomain -class TopkatApplicabilityDomain(BaseEstimator, TransformerMixin): +class TopkatApplicabilityDomain(BaseApplicabilityDomain): """Applicability domain defined using TOPKAT's Optimal Prediction Space (OPS). The method transforms the input space (P-space) to a normalized space (S-space), then projects it to the Optimal Prediction Space using eigendecomposition. + Parameters + ---------- + percentile : float or None, default=None + Not used, present for API consistency. + feature_name : str, default="TOPKAT" + Name for the output feature column. + Attributes ---------- n_features_in_ : int @@ -31,13 +42,13 @@ class TopkatApplicabilityDomain(BaseEstimator, TransformerMixin): Eigenvalues of the S-space transformation. eigen_vec_ : ndarray of shape (n_features + 1, n_features + 1) Eigenvectors of the S-space transformation. + threshold_ : float + Fixed threshold based on dimensionality. - Examples - -------- - >>> from scikit_mol.applicability import TopkatApplicabilityDomain - >>> ad = TopkatApplicabilityDomain() - >>> ad.fit(X_train) - >>> predictions = ad.predict(X_test) + Notes + ----- + The scoring convention is 'high_outside' because higher OPS distances + indicate samples further from the training data. References ---------- @@ -45,22 +56,32 @@ class TopkatApplicabilityDomain(BaseEstimator, TransformerMixin): predictions (US Patent No. 6-036-349) USPTO. """ - def fit(self, X, y=None): + _scoring_convention = "high_outside" + _supports_threshold_fitting = False + + def __init__( + self, + percentile: Optional[float] = None, + feature_name: str = "TOPKAT", + ) -> None: + super().__init__(percentile=None, feature_name=feature_name) + + def fit(self, X: ArrayLike, y: Optional[Any] = None) -> "TopkatApplicabilityDomain": """Fit the TOPKAT applicability domain. Parameters ---------- X : array-like of shape (n_samples, n_features) Training data. - y : Ignored + y : Any, optional (default=None) Not used, present for API consistency. Returns ------- - self : object + self : TopkatApplicabilityDomain Returns the instance itself. """ - X = check_array(X) + X = check_array(X, **self._check_params) self.n_features_in_ = X.shape[1] n_samples = X.shape[0] @@ -84,14 +105,17 @@ def fit(self, X, y=None): self.eigen_val_ = np.real(self.eigen_val_) self.eigen_vec_ = np.real(self.eigen_vec_) + # Set fixed threshold based on dimensionality + self.threshold_ = 5 * (self.n_features_in_ + 1) / (2 * self.n_features_in_) + return self - def transform(self, X): + def _transform(self, X: NDArray) -> NDArray[np.float64]: """Calculate OPS distance scores for samples. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : ndarray of shape (n_samples, n_features) The data to transform. Returns @@ -100,9 +124,6 @@ def transform(self, X): OPS distance scores. Higher values indicate samples further from the training data. """ - check_is_fitted(self) - X = check_array(X) - # Transform to S-space denom = np.where( (self.X_max_ - self.X_min_) != 0, (self.X_max_ - self.X_min_), 1 @@ -129,20 +150,20 @@ def transform(self, X): return distances.reshape(-1, 1) - def predict(self, X): - """Predict whether samples are within the applicability domain. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The samples to predict. - - Returns - ------- - y_pred : ndarray of shape (n_samples,) - Returns 1 for samples inside the domain and -1 for samples outside - (following scikit-learn's convention for outlier detection). - """ - scores = self.transform(X).ravel() - threshold = 5 * (self.n_features_in_ + 1) / (2 * self.n_features_in_) - return np.where(scores < threshold, 1, -1) + # def predict(self, X): + # """Predict whether samples are within the applicability domain. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # The samples to predict. + + # Returns + # ------- + # y_pred : ndarray of shape (n_samples,) + # Returns 1 for samples inside the domain and -1 for samples outside + # (following scikit-learn's convention for outlier detection). + # """ + # scores = self._transform(X).ravel() + # threshold = self.threshold_ + # return np.where(scores < threshold, 1, -1) diff --git a/tests/applicability/conftest.py b/tests/applicability/conftest.py index f822b1f..764e724 100644 --- a/tests/applicability/conftest.py +++ b/tests/applicability/conftest.py @@ -8,8 +8,13 @@ ConvexHullApplicabilityDomain, HotellingT2ApplicabilityDomain, IsolationForestApplicabilityDomain, + KernelDensityApplicabilityDomain, KNNApplicabilityDomain, LeverageApplicabilityDomain, + LocalOutlierFactorApplicabilityDomain, + MahalanobisApplicabilityDomain, + StandardizationApplicabilityDomain, + TopkatApplicabilityDomain, ) from scikit_mol.fingerprints import MorganFingerprintTransformer @@ -31,6 +36,19 @@ random_state=42, # Add fixed random state ), ), + ( + KernelDensityApplicabilityDomain, + dict(bandwidth=1.0, kernel="gaussian"), + ), + ( + LocalOutlierFactorApplicabilityDomain, + dict( + n_neighbors=3, contamination=0.1 + ), # Reduced from 20 to 3 for small test datasets + ), + (MahalanobisApplicabilityDomain, dict()), # No special parameters needed + (StandardizationApplicabilityDomain, dict()), # No special parameters needed + (TopkatApplicabilityDomain, dict()), # No special parameters needed ] ) def ad_estimator(request): diff --git a/tests/applicability/test_base.py b/tests/applicability/test_base.py index a7d7a58..56eba50 100644 --- a/tests/applicability/test_base.py +++ b/tests/applicability/test_base.py @@ -79,10 +79,10 @@ def test_feature_names(ad_estimator, reduced_fingerprints): """Test feature names are properly handled.""" ad_estimator.fit(reduced_fingerprints) - # Check feature names exist and match prefix + # Check feature names exist and match name feature_names = ad_estimator.get_feature_names_out() assert len(feature_names) == 1 - assert feature_names[0].startswith(ad_estimator.feature_prefix) + assert feature_names[0] == ad_estimator.feature_name def test_pandas_output(ad_estimator, reduced_fingerprints): @@ -94,7 +94,7 @@ def test_pandas_output(ad_estimator, reduced_fingerprints): scores_df = ad_estimator.transform(reduced_fingerprints) assert hasattr(scores_df, "columns") assert len(scores_df.columns) == 1 - assert scores_df.columns[0].startswith(ad_estimator.feature_prefix) + assert scores_df.columns[0] == ad_estimator.feature_name # Test predict output pred_df = ad_estimator.predict(reduced_fingerprints) diff --git a/tests/applicability/test_kernel_density.py b/tests/applicability/test_kernel_density.py new file mode 100644 index 0000000..2c178c4 --- /dev/null +++ b/tests/applicability/test_kernel_density.py @@ -0,0 +1,44 @@ +"""Tests for KernelDensityApplicabilityDomain.""" + +import pytest + +from scikit_mol.applicability import KernelDensityApplicabilityDomain + + +@pytest.fixture +def ad_estimator(): + """Fixture providing a KernelDensityApplicabilityDomain instance.""" + return KernelDensityApplicabilityDomain() + + +def test_kernel_parameter(): + """Test different kernel parameters.""" + kernels = ["gaussian", "tophat", "epanechnikov", "exponential", "linear", "cosine"] + # Create data with clear density gradient + X = [[0, 0], [0.1, 0.1], [0.2, 0.2], [2, 2]] + + for kernel in kernels: + ad = KernelDensityApplicabilityDomain(kernel=kernel) + ad.fit(X) + scores = ad.transform(X) + assert scores.shape == (4, 1) + # First point should have higher density than last point + assert scores[0, 0] > scores[-1, 0], f"Failed for kernel {kernel}" + + +def test_bandwidth_effect(): + """Test effect of bandwidth parameter on scores.""" + X = [[0, 0], [1, 1], [2, 2]] + test_point = [[10, 10]] # Far from training data + + # Larger bandwidth should give higher scores to outliers + ad_small = KernelDensityApplicabilityDomain(bandwidth=0.1) + ad_large = KernelDensityApplicabilityDomain(bandwidth=10.0) + + ad_small.fit(X) + ad_large.fit(X) + + score_small = ad_small.transform(test_point) + score_large = ad_large.transform(test_point) + + assert score_large[0, 0] > score_small[0, 0] diff --git a/tests/applicability/test_local_outlier.py b/tests/applicability/test_local_outlier.py new file mode 100644 index 0000000..c0b129f --- /dev/null +++ b/tests/applicability/test_local_outlier.py @@ -0,0 +1,64 @@ +"""Tests for LocalOutlierFactorApplicabilityDomain.""" + +import numpy as np +import pytest + +from scikit_mol.applicability import LocalOutlierFactorApplicabilityDomain + + +@pytest.fixture +def ad_estimator(): + """Fixture providing a LocalOutlierFactorApplicabilityDomain instance.""" + return LocalOutlierFactorApplicabilityDomain() + + +def test_n_neighbors_effect(): + """Test effect of n_neighbors parameter on scores.""" + # Create data with clear outlier + X = np.vstack([np.random.randn(50, 2), [[10, 10]]]) + outlier = np.array([[10, 10]]) + + # Compare different n_neighbors settings + ad_small = LocalOutlierFactorApplicabilityDomain(n_neighbors=5) + ad_large = LocalOutlierFactorApplicabilityDomain(n_neighbors=20) + + ad_small.fit(X) + ad_large.fit(X) + + score_small = ad_small.transform(outlier) + score_large = ad_large.transform(outlier) + + # Scores should be different but both should identify the point as an outlier + assert score_small != score_large + assert ad_small.predict(outlier) == -1 + assert ad_large.predict(outlier) == -1 + + +def test_metric_parameter(): + """Test different metric parameters.""" + metrics = ["euclidean", "manhattan", "cosine"] + X = np.random.randn(10, 2) + + for metric in metrics: + ad = LocalOutlierFactorApplicabilityDomain(metric=metric) + ad.fit(X) + scores = ad.transform(X) + assert scores.shape == (10, 1) + + +def test_contamination_effect(): + """Test effect of contamination parameter on predictions.""" + X = np.random.randn(100, 2) + + # Compare different contamination levels + ad_low = LocalOutlierFactorApplicabilityDomain(contamination=0.1) + ad_high = LocalOutlierFactorApplicabilityDomain(contamination=0.2) + + ad_low.fit(X) + ad_high.fit(X) + + pred_low = ad_low.predict(X) + pred_high = ad_high.predict(X) + + # Higher contamination should result in more outliers + assert np.sum(pred_high == -1) > np.sum(pred_low == -1) diff --git a/tests/applicability/test_mahalanobis.py b/tests/applicability/test_mahalanobis.py new file mode 100644 index 0000000..5d0c31c --- /dev/null +++ b/tests/applicability/test_mahalanobis.py @@ -0,0 +1,66 @@ +"""Tests for MahalanobisApplicabilityDomain.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal + +from scikit_mol.applicability import MahalanobisApplicabilityDomain + + +@pytest.fixture +def ad_estimator(): + """Fixture providing a MahalanobisApplicabilityDomain instance.""" + return MahalanobisApplicabilityDomain() + + +def test_statistical_threshold(): + """Test chi-square based statistical threshold.""" + # Create multivariate normal data + n_samples = 1000 + n_features = 3 + mean = np.zeros(n_features) + cov = np.eye(n_features) + X = np.random.multivariate_normal(mean, cov, n_samples) + + # Fit with statistical threshold + ad = MahalanobisApplicabilityDomain(percentile=None) + ad.fit(X) + + # For standard normal data, ~95% should be within threshold + predictions = ad.predict(X) + inside_ratio = np.mean(predictions == 1) + assert 0.93 <= inside_ratio <= 0.97 # Allow some variation + + +def test_mean_covariance(): + """Test mean and covariance computation.""" + X = np.array([[1, 2], [3, 4], [5, 6]]) + ad = MahalanobisApplicabilityDomain() + ad.fit(X) + + # Check mean computation + expected_mean = np.array([3, 4]) + assert_array_almost_equal(ad.mean_, expected_mean) + + # Check covariance computation + expected_cov = np.array([[4, 4], [4, 4]]) + assert_array_almost_equal(ad.covariance_, expected_cov) + + +def test_distance_properties(): + """Test properties of Mahalanobis distances.""" + # Create data with clear outlier + X = np.vstack([np.random.randn(50, 2), [[10, 10]]]) + outlier = np.array([[10, 10]]) + + ad = MahalanobisApplicabilityDomain() + ad.fit(X) + + # Distance to mean should be zero + mean_dist = ad.transform(ad.mean_.reshape(1, -1)) + assert_array_almost_equal(mean_dist, [[0]], decimal=10) + + # Outlier should have large distance and be predicted outside + outlier_dist = ad.transform(outlier) + assert outlier_dist[0, 0] > ad.threshold_ + assert ad.predict(outlier) == -1 diff --git a/tests/applicability/test_standardization.py b/tests/applicability/test_standardization.py new file mode 100644 index 0000000..fb9fb2a --- /dev/null +++ b/tests/applicability/test_standardization.py @@ -0,0 +1,76 @@ +"""Tests for StandardizationApplicabilityDomain.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal + +from scikit_mol.applicability import StandardizationApplicabilityDomain + + +@pytest.fixture +def ad_estimator(): + """Fixture providing a StandardizationApplicabilityDomain instance.""" + return StandardizationApplicabilityDomain() + + +def test_statistical_threshold(): + """Test normal distribution based statistical threshold.""" + # Create standard normal data + n_samples = 1000 + n_features = 3 + X = np.random.randn(n_samples, n_features) + + # Fit with statistical threshold + ad = StandardizationApplicabilityDomain(percentile=None) + ad.fit(X) + + # For standard normal data, ~95% should be within threshold + predictions = ad.predict(X) + inside_ratio = np.mean(predictions == 1) + assert 0.93 <= inside_ratio <= 0.97 # Allow some variation + + +def test_standardization(): + """Test standardization of features.""" + X = np.array([[1, 2], [3, 4], [5, 6]]) + ad = StandardizationApplicabilityDomain() + ad.fit(X) + + # Transform data + X_std = ad.scaler_.transform(X) + + # Check standardization properties + assert_array_almost_equal(np.mean(X_std, axis=0), [0, 0]) + assert_array_almost_equal(np.std(X_std, axis=0), [1, 1]) + + +def test_max_absolute_score(): + """Test that scores are maximum absolute standardized values.""" + # Create data with known standardized values + X = np.array([[0, 0], [1, 2], [3, -4]]) + ad = StandardizationApplicabilityDomain() + ad.fit(X) + + # Create test point with one extreme standardized value + X_test = np.array([[0, 10]]) # Second feature will be very large when standardized + scores = ad.transform(X_test) + + # Score should be the maximum absolute standardized value + X_std = ad.scaler_.transform(X_test) + expected_score = np.max(np.abs(X_std)) + assert_array_almost_equal(scores, [[expected_score]]) + + +def test_outlier_detection(): + """Test outlier detection on simple dataset.""" + # Create data with clear outlier + X = np.vstack([np.random.randn(50, 2), [[10, 10]]]) + outlier = np.array([[10, 10]]) + + ad = StandardizationApplicabilityDomain() + ad.fit(X) + + # Outlier should have high score and be predicted outside + outlier_score = ad.transform(outlier) + assert outlier_score[0, 0] > ad.threshold_ + assert ad.predict(outlier) == -1 diff --git a/tests/applicability/test_topkat.py b/tests/applicability/test_topkat.py new file mode 100644 index 0000000..4d3e792 --- /dev/null +++ b/tests/applicability/test_topkat.py @@ -0,0 +1,55 @@ +"""Tests for TopkatApplicabilityDomain.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal + +from scikit_mol.applicability import TopkatApplicabilityDomain + + +@pytest.fixture +def ad_estimator(): + """Fixture providing a TopkatApplicabilityDomain instance.""" + return TopkatApplicabilityDomain() + + +def test_ops_transformation(): + """Test OPS transformation and distance calculation.""" + # Create simple test data + X_train = np.array([[0, 0], [1, 1], [2, 2]]) + X_test = np.array([[0.5, 0.5], [10, 10]]) + + # Fit AD model + ad = TopkatApplicabilityDomain() + ad.fit(X_train) + + # Check distances + distances = ad.transform(X_test) + assert distances.shape == (2, 1) + assert distances[0] < distances[1] # Interpolated point should have lower distance + + +def test_fixed_threshold(): + """Test that threshold is based on dimensionality.""" + X = np.random.randn(10, 3) + ad = TopkatApplicabilityDomain() + ad.fit(X) + + # Check threshold formula + expected_threshold = 5 * (3 + 1) / (2 * 3) # n_features = 3 + assert_array_almost_equal(ad.threshold_, expected_threshold) + + +def test_eigendecomposition(): + """Test eigendecomposition properties.""" + X = np.random.randn(10, 2) + ad = TopkatApplicabilityDomain() + ad.fit(X) + + # Check eigenvalue/vector shapes + assert ad.eigen_val_.shape == (3,) # n_features + 1 + assert ad.eigen_vec_.shape == (3, 3) # (n_features + 1, n_features + 1) + + # Check eigenvalues are real and sorted + assert np.all(np.isreal(ad.eigen_val_)) + assert np.all(np.diff(ad.eigen_val_) >= 0) # Sorted in ascending order diff --git a/tests/conftest.py b/tests/conftest.py index 2359cd1..ee916ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ import hashlib import shutil -from pathlib import Path -from pathlib import PurePath +from pathlib import Path, PurePath from urllib.parse import urlsplit from urllib.request import urlopen @@ -10,6 +9,14 @@ import sklearn +# Register custom marks +def pytest_configure(config): + config.addinivalue_line( + "markers", + "threshold_fitting: mark tests that verify threshold fitting functionality", + ) + + TEST_DATA_URL = "https://ndownloader.figshare.com/files/25747817" TEST_DATA_MD5 = "1ec89bde544c3c4bc400d5b75315921e" @@ -32,9 +39,7 @@ def data_pth(tmp_path_factory) -> Path: if not data_fn.is_file(): # download svs from openslide test images - with urlopen(TEST_DATA_URL) as response, open( - data_fn, "wb" - ) as out_file: + with urlopen(TEST_DATA_URL) as response, open(data_fn, "wb") as out_file: shutil.copyfileobj(response, out_file) if md5(data_fn) != TEST_DATA_MD5: # pragma: no cover @@ -43,13 +48,15 @@ def data_pth(tmp_path_factory) -> Path: yield data_fn.absolute() + @pytest.fixture() def data(data_pth) -> pd.DataFrame: yield pd.read_csv(data_pth) + @pytest.fixture(scope="module") def pandas_output(): """Set sklearn to output pandas dataframes""" sklearn.set_config(transform_output="pandas") yield - sklearn.set_config(transform_output="default") \ No newline at end of file + sklearn.set_config(transform_output="default") diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..81b8773 --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,91 @@ +import numpy as np +import pytest + +from scikit_mol.metrics import tanimoto_distance + +from .applicability.conftest import binary_fingerprints + + +@pytest.fixture +def simple_fingerprints(): + """Create simple binary fingerprints for testing.""" + return np.array( + [ + [1, 1, 0, 0], # fp0: 2 bits set + [1, 0, 1, 0], # fp1: 2 bits set, 1 in common with fp1 + [0, 0, 1, 1], # fp2: 2 bits set, 1 in common with fp2, none with fp1 + [1, 1, 1, 1], # fp3: all bits set + [0, 0, 0, 0], # fp4: no bits set + ], + dtype=bool, + ) + + +def test_tanimoto_distance_basic(simple_fingerprints): + """Test basic properties of Tanimoto distance.""" + distances = tanimoto_distance(simple_fingerprints[0], simple_fingerprints[1]) + + # Check distance range [0,1] + assert 0 <= distances <= 1 + + # Check specific distances + # fp0 vs fp1: 1 bit in common, 3 in union -> distance = 2/3 + assert np.isclose(distances, 2 / 3) + # fp0 vs fp2: no bits in common, 4 in union -> distance = 1 + assert np.isclose( + tanimoto_distance(simple_fingerprints[0], simple_fingerprints[2]), 1.0 + ) + # fp0 vs fp3: 2 bits in common, 4 in union -> distance = 0.5 + assert np.isclose( + tanimoto_distance(simple_fingerprints[0], simple_fingerprints[3]), 0.5 + ) + # fp0 vs fp4: no bits in common, 2 in union -> distance = 1 + assert np.isclose( + tanimoto_distance(simple_fingerprints[0], simple_fingerprints[4]), 1.0 + ) + + +def test_tanimoto_distance_edge_cases(simple_fingerprints): + """Test edge cases for Tanimoto distance.""" + empty = simple_fingerprints[4] # Empty fingerprint + full = simple_fingerprints[3] # Full fingerprint + + # Two empty fingerprints (fp4) + dist = tanimoto_distance(empty, empty) + # No bits in common, 0 in union -> distance = 0/0 = 0 in our implementation. + assert np.isclose(dist, 0.0) + + # Empty vs full fingerprint (fp3) + dist = tanimoto_distance(empty, full) + assert np.isclose(dist, 1.0) # No overlap -> maximum distance + + +# TODO, can rdkit speed things up? But not working with np.arrays +# def test_tanimoto_implementations_equivalent(simple_fingerprints): +# """Test that both implementations give equivalent results.""" +# X = simple_fingerprints[:2] +# Y = simple_fingerprints[2:4] + +# dist1 = tanimoto_distance(X, Y) +# dist2 = tanimoto_distance_rdkit(X, Y) + +# assert np.allclose(dist1, dist2) + + +# def test_tanimoto_distance_rdkit_basic(binary_fingerprints): +# """Test basic properties of RDKit-based Tanimoto distance.""" +# # Get a subset of fingerprints for testing +# X = binary_fingerprints[:3] +# Y = binary_fingerprints[3:6] + +# distances = tanimoto_distance_rdkit(X, Y) + +# # Check output shape +# assert distances.shape == (3, 3) + +# # Check distance range [0,1] +# assert np.all((0 <= distances) & (distances <= 1)) + +# # Check self-distance is 0 for identical fingerprints +# self_distances = tanimoto_distance_rdkit(X, X) +# assert np.allclose(np.diag(self_distances), 0) From c2a9dc4a02969d1fdbfe60e2026c73c6e8971b3e Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Wed, 12 Feb 2025 09:58:28 +0100 Subject: [PATCH 10/24] WIP on the adapter. Not fully there yet as some complications in the parallel helper functions. --- notebooks/12_applicability_domain.py | 298 +++++++++++++++++++++++++ scikit_mol/adapters.py | 322 ++++++++++++++------------- tests/test_adapters.py | 145 ++++++++++++ 3 files changed, 607 insertions(+), 158 deletions(-) create mode 100644 notebooks/12_applicability_domain.py create mode 100644 tests/test_adapters.py diff --git a/notebooks/12_applicability_domain.py b/notebooks/12_applicability_domain.py new file mode 100644 index 0000000..7baa492 --- /dev/null +++ b/notebooks/12_applicability_domain.py @@ -0,0 +1,298 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_filter: -all +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.6 +# --- + +# %% +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Applicability Domain Estimation +# +# This notebook demonstrates how to use scikit-mol's applicability domain estimators to assess whether new compounds are within the domain of applicability of a trained model. +# +# We'll explore two different approaches: +# 1. Using Morgan binary fingerprints with a k-Nearest Neighbors based applicability domain +# 2. Using count-based Morgan fingerprints with dimensionality reduction and a leverage-based applicability domain +# +# First, let's import the necessary libraries and load our dataset: + +# %% +import numpy as np +import pandas as pd +from rdkit import Chem +from rdkit.Chem import PandasTools +import matplotlib.pyplot as plt +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestRegressor +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.decomposition import PCA + +from scikit_mol.conversions import SmilesToMolTransformer +from scikit_mol.fingerprints import MorganFingerprintTransformer +from scikit_mol.applicability import KNNApplicabilityDomain, LeverageApplicabilityDomain + +# %% [markdown] +# ## Load and Prepare Data + +# %% +# Load the dataset +csv_file = "../tests/data/SLC6A4_active_excapedb_subset.csv" +data = pd.read_csv(csv_file) + +# Add RDKit mol objects +PandasTools.AddMoleculeColumnToFrame(data, smilesCol="SMILES") +print(f"{data.ROMol.isna().sum()} out of {len(data)} SMILES failed in conversion") + +# Split into train/val/test +X = data.ROMol +y = data.pXC50 + +X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42) +X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=42) + +# %% [markdown] +# ## Example 1: k-NN Applicability Domain with Binary Morgan Fingerprints +# +# In this example, we'll use binary Morgan fingerprints and a k-NN based applicability domain with Tanimoto distance. +# This is particularly suitable for binary fingerprints as the Tanimoto coefficient is a natural similarity measure for them. + +# %% +# Create pipeline for binary fingerprints +binary_fp_pipe = Pipeline([ + ('fp', MorganFingerprintTransformer(fpSize=2048, radius=2)), + ('rf', RandomForestRegressor(n_estimators=100, random_state=42)) +]) + +# Train the model +binary_fp_pipe.fit(X_train, y_train) + +# Get predictions and errors +y_pred_test = binary_fp_pipe.predict(X_test) +abs_errors = np.abs(y_test - y_pred_test) + +# Create and fit k-NN AD estimator +knn_ad = KNNApplicabilityDomain(n_neighbors=3, distance_metric='tanimoto') +knn_ad.fit(binary_fp_pipe.named_steps['fp'].transform(X_train)) + +# Fit threshold using validation set +knn_ad.fit_threshold(binary_fp_pipe.named_steps['fp'].transform(X_val)) + +# Get AD scores for test set +knn_scores = knn_ad.transform(binary_fp_pipe.named_steps['fp'].transform(X_test)) + +# %% [markdown] +# Let's visualize the relationship between prediction errors and AD scores: + +# %% +plt.figure(figsize=(10, 6)) +plt.scatter(knn_scores, abs_errors, alpha=0.5) +plt.axvline(x=knn_ad.threshold_, color='r', linestyle='--', label='AD Threshold') +plt.xlabel('k-NN AD Score') +plt.ylabel('Absolute Prediction Error') +plt.title('Prediction Errors vs k-NN AD Scores') +plt.legend() +plt.show() + +# Calculate error statistics +in_domain = knn_ad.predict(binary_fp_pipe.named_steps['fp'].transform(X_test)) +errors_in = abs_errors[in_domain == 1] +errors_out = abs_errors[in_domain == -1] + +print(f"95th percentile of errors inside domain: {np.percentile(errors_in, 95):.2f}") +print(f"95th percentile of errors outside domain: {np.percentile(errors_out, 95):.2f}") +print(f"Fraction of samples outside domain: {(in_domain == -1).mean():.2f}") + +# %% [markdown] +# ## Example 2: Leverage-based AD with Count-based Morgan Fingerprints +# +# In this example, we'll use count-based Morgan fingerprints, reduce their dimensionality with PCA, +# and apply a leverage-based applicability domain estimator. + +# %% +# Create pipeline for count-based fingerprints with PCA +count_fp_pipe = Pipeline([ + ('fp', MorganFingerprintTransformer(fpSize=2048, radius=2, useCounts=True)), + ('pca', PCA(n_components=0.9)), # Keep 90% of variance + ('scaler', StandardScaler()), + ('rf', RandomForestRegressor(n_estimators=100, random_state=42)) +]) + +# Train the model +count_fp_pipe.fit(X_train, y_train) + +# Get predictions and errors +y_pred_test = count_fp_pipe.predict(X_test) +abs_errors = np.abs(y_test - y_pred_test) + +# Create and fit leverage AD estimator +leverage_ad = LeverageApplicabilityDomain() +X_train_transformed = count_fp_pipe.named_steps['scaler'].transform( + count_fp_pipe.named_steps['pca'].transform( + count_fp_pipe.named_steps['fp'].transform(X_train) + ) +) +leverage_ad.fit(X_train_transformed) + +# Fit threshold using validation set +X_val_transformed = count_fp_pipe.named_steps['scaler'].transform( + count_fp_pipe.named_steps['pca'].transform( + count_fp_pipe.named_steps['fp'].transform(X_val) + ) +) +leverage_ad.fit_threshold(X_val_transformed) + +# Get AD scores for test set +X_test_transformed = count_fp_pipe.named_steps['scaler'].transform( + count_fp_pipe.named_steps['pca'].transform( + count_fp_pipe.named_steps['fp'].transform(X_test) + ) +) +leverage_scores = leverage_ad.transform(X_test_transformed) + +# %% [markdown] +# Visualize the relationship between prediction errors and leverage scores: + +# %% +plt.figure(figsize=(10, 6)) +plt.scatter(leverage_scores, abs_errors, alpha=0.5) +plt.axvline(x=leverage_ad.threshold_, color='r', linestyle='--', label='AD Threshold') +plt.xlabel('Leverage AD Score') +plt.ylabel('Absolute Prediction Error') +plt.title('Prediction Errors vs Leverage Scores') +plt.legend() +plt.show() + +# Calculate error statistics +in_domain = leverage_ad.predict(X_test_transformed) +errors_in = abs_errors[in_domain == 1] +errors_out = abs_errors[in_domain == -1] + +print(f"95th percentile of errors inside domain: {np.percentile(errors_in, 95):.2f}") +print(f"95th percentile of errors outside domain: {np.percentile(errors_out, 95):.2f}") +print(f"Fraction of samples outside domain: {(in_domain == -1).mean():.2f}") + +# %% [markdown] +# ## Testing Famous Drugs +# +# Let's test some well-known drugs to see if they fall within our model's applicability domain: + +# %% +# Define famous drugs +famous_drugs = { + 'Aspirin': 'CC(=O)OC1=CC=CC=C1C(=O)O', + 'Viagra': 'CCc1nn(C)c2c(=O)[nH]c(nc12)c3cc(ccc3OCC)S(=O)(=O)N4CCN(C)CC4', + 'Heroin': 'CN1CC[C@]23[C@H]4Oc5c(O)ccc(CC1[C@H]2C=C[C@@H]4O3)c5', +} + +# Function to process a drug through both AD pipelines +def check_drug_applicability(smiles, name): + mol = Chem.MolFromSmiles(smiles) + + # k-NN AD + fp_binary = binary_fp_pipe.named_steps['fp'].transform([mol]) + knn_score = knn_ad.transform(fp_binary)[0][0] + knn_status = "Inside" if knn_ad.predict(fp_binary)[0] == 1 else "Outside" + + # Leverage AD + fp_count = count_fp_pipe.named_steps['fp'].transform([mol]) + fp_pca = count_fp_pipe.named_steps['pca'].transform(fp_count) + fp_scaled = count_fp_pipe.named_steps['scaler'].transform(fp_pca) + leverage_score = leverage_ad.transform(fp_scaled)[0][0] + leverage_status = "Inside" if leverage_ad.predict(fp_scaled)[0] == 1 else "Outside" + + return { + 'knn_score': knn_score, + 'knn_status': knn_status, + 'leverage_score': leverage_score, + 'leverage_status': leverage_status + } + +# Process each drug +results = [] +for name, smiles in famous_drugs.items(): + result = check_drug_applicability(smiles, name) + results.append({ + 'Drug': name, + 'k-NN Score': result['knn_score'], + 'k-NN Status': result['knn_status'], + 'Leverage Score': result['leverage_score'], + 'Leverage Status': result['leverage_status'] + }) + +# Display results +pd.DataFrame(results).set_index('Drug') + +# %% [markdown] +# Let's visualize where these drugs fall in our AD plots: + +# %% +# Plot for k-NN AD +plt.figure(figsize=(12, 5)) +plt.subplot(1, 2, 1) +plt.scatter(knn_scores, abs_errors, alpha=0.2, label='Test compounds') +plt.axvline(x=knn_ad.threshold_, color='r', linestyle='--', label='AD Threshold') + +for result in results: + plt.axvline(x=result['k-NN Score'], color='g', alpha=0.5, + label=f"{result['Drug']}") + +plt.xlabel('k-NN AD Score') +plt.ylabel('Absolute Prediction Error') +plt.title('k-NN AD Scores') +plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left') + +# Plot for Leverage AD +plt.subplot(1, 2, 2) +plt.scatter(leverage_scores, abs_errors, alpha=0.2, label='Test compounds') +plt.axvline(x=leverage_ad.threshold_, color='r', linestyle='--', label='AD Threshold') + +for result in results: + plt.axvline(x=result['Leverage Score'], color='g', alpha=0.5, + label=f"{result['Drug']}") + +plt.xlabel('Leverage AD Score') +plt.ylabel('Absolute Prediction Error') +plt.title('Leverage AD Scores') +plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left') + +plt.tight_layout() +plt.show() + +# %% [markdown] +# ## Conclusions +# +# This notebook demonstrated two different approaches to applicability domain estimation: +# +# 1. The k-NN based approach with binary fingerprints and Tanimoto distance provides a chemical similarity-based assessment +# of whether new compounds are similar enough to the training set. +# +# 2. The leverage-based approach with count-based fingerprints and dimensionality reduction focuses on the statistical +# novelty of compounds in the reduced feature space. +# +# The famous drugs we tested showed varying degrees of being within the applicability domain, which makes sense given +# that our training set is focused on SLC6A4 actives, while these drugs have different primary targets. +# +# The error analysis shows that compounds outside the applicability domain tend to have higher prediction errors, +# validating the usefulness of these approaches for identifying potentially unreliable predictions. diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index e5451f9..4bedd40 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -1,175 +1,181 @@ -import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin, OneToOneFeatureMixin -from sklearn.utils._pprint import _EstimatorPrettyPrinter -from sklearn.utils._param_validation import validate_params -from sklearn.utils.metaestimators import available_if -from sklearn.utils.validation import check_is_fitted, check_array -from sklearn.utils._estimator_html_repr import _VisualBlock -from sklearn.utils._set_output import _safe_set_output -from scipy.stats import chi2 - - -class EstimatorUnion(BaseEstimator, TransformerMixin): - def __init__(self, estimators): - self.estimators = estimators - - def fit(self, X, y=None): - self.fitted_estimators_ = [] - for name, estimator in self.estimators: - if hasattr(estimator, "fit"): - fitted_estimator = estimator.fit(X, y) - self.fitted_estimators_.append((name, fitted_estimator)) - else: - self.fitted_estimators_.append((name, estimator)) - return self - - def transform(self, X): - check_is_fitted(self) - results = [] - for name, estimator in self.fitted_estimators_: - if hasattr(estimator, "predict"): - results.append(estimator.predict(X)) - elif hasattr(estimator, "transform"): - results.append(estimator.transform(X)) - return np.column_stack(results) - - def predict(self, X): - return self.transform(X) +from typing import Dict, List, Optional, Tuple - @available_if(lambda self: hasattr(self, "fitted_estimators_")) - def get_feature_names_out(self, input_features=None): - feature_names = [] - for name, estimator in self.fitted_estimators_: - if hasattr(estimator, "get_feature_names_out"): - feature_names.extend(estimator.get_feature_names_out()) - else: - feature_names.append(name) - return np.array(feature_names) +import numpy as np +from numpy.typing import NDArray +from sklearn.base import BaseEstimator +from sklearn.pipeline import FeatureUnion + + +class EstimatorUnion(FeatureUnion): + """A more flexible version of FeatureUnion that supports various estimator types. + + This class extends scikit-learn's FeatureUnion to support estimators with different + method interfaces (predict, transform, etc.) and allows explicit method selection. + It maintains all functionality of FeatureUnion while adding flexible method resolution. + + Parameters + ---------- + estimators : list of (str, estimator) tuples + List of (name, estimator) tuples, where estimator is any scikit-learn + compatible estimator with at least one of the methods specified in + method_resolution_order. + method_resolution_order : tuple of str, default=("predict", "transform") + Ordered tuple of method names to try when getting output from estimators. + Methods are tried in order until a valid one is found. + selected_methods : dict or None, default=None + Optional mapping of estimator names to specific methods to use. Takes + precedence over method_resolution_order. + n_jobs : int or None, default=None + Number of jobs to run in parallel. None means 1. + transformer_weights : dict or None, default=None + Multiplicative weights for features per transformer. Keys are transformer + names, values are weights. + verbose : bool, default=False + If True, the time elapsed while fitting each transformer will be printed. + + Attributes + ---------- + transformers_ : list + List of fitted transformers. + + Notes + ----- + This class inherits from FeatureUnion and maintains all its functionality including + parallel processing, transformer weights, and metadata routing. The key extension is + the ability to handle estimators with different method interfaces through configurable + method resolution. + + See Also + -------- + sklearn.pipeline.FeatureUnion : The parent class providing base functionality. + """ - def set_output(self, *, transform=None): - """Set output container for all estimators. + def __init__( + self, + estimator_list: List[Tuple[str, BaseEstimator]], + *, + method_resolution_order: Tuple[str, ...] = ("predict", "transform"), + selected_methods: Optional[Dict[str, str]] = None, + n_jobs: Optional[int] = None, + transformer_weights: Optional[Dict[str, float]] = None, + verbose: bool = False, + ) -> None: + # Store all parameters as properties + self.estimator_list = estimator_list + self.method_resolution_order = method_resolution_order + self.selected_methods = selected_methods or {} + self.n_jobs = n_jobs + self.transformer_weights = transformer_weights + self.verbose = verbose + + @property + def estimator_list(self) -> List[Tuple[str, BaseEstimator]]: + """Get estimators (alias for transformer_list).""" + return self.transformer_list + + @estimator_list.setter + def estimator_list(self, estimator_list: List[Tuple[str, BaseEstimator]]) -> None: + """Set estimators (and internal transformer_list property).""" + self.transformer_list = estimator_list + + def _get_method_name(self, estimator_tuple: Tuple[str, BaseEstimator]) -> str: + """Get the appropriate method name for the estimator, raising clear errors if not found. Parameters ---------- - transform : {"default", "pandas"}, default=None - Configure output of `transform` and `fit_transform`. + estimator_tuple : tuple of (str, estimator) + Tuple containing the estimator name and instance. Returns ------- - self : estimator instance - Estimator instance. - """ - for _, estimator in self.estimators: - _safe_set_output(estimator, transform=transform) - return super().set_output(transform=transform) - - def __repr__(self): - class_name = self.__class__.__name__ - estimator_reprs = [] - for name, estimator in self.estimators: - estimator_repr = f"{name}={estimator.__repr__()}" - estimator_reprs.append(estimator_repr) - estimators_str = ",\n".join(estimator_reprs) - return f"{class_name}([\n{estimators_str}\n])" - - def _sk_visual_block_(self): - names, transformers = zip(*self.estimators) - return _VisualBlock("parallel", transformers, names=names) - - -class SigmoidThresholdTransformer(BaseEstimator, TransformerMixin): - def __init__(self, threshold, steepness=1, feature_name="Sigmoid_", prefix=True): - self.threshold = threshold - self.steepness = steepness - self.feature_name = feature_name - self.prefix = prefix - - def fit(self, X, y=None): - return self - - def transform(self, X): - return 1 / (1 + np.exp(self.steepness * (X - self.threshold))) - - def predict(self, X): - return self.transform(X) + str + Name of the method to use for this estimator. - @available_if(lambda self: hasattr(self, "fitted_estimators_")) - def get_feature_names_out(self, input_features=None): - check_is_fitted(self) - - if input_features is None: - if ( - hasattr(self, "feature_names_in_") - and self.feature_names_in_ is not None - ): - input_features = self.feature_names_in_ - else: - input_features = [f"x{i}" for i in range(self.n_features_in_)] - - if self.feature_name: - if self.prefix: - return np.array( - [f"{self.feature_name}{feature}" for feature in input_features] + Raises + ------ + ValueError + If no valid method is found for the estimator. + """ + name, estimator = estimator_tuple + + # Check explicit method if specified + if name in self.selected_methods: + method = self.selected_methods[name] + if not hasattr(estimator, method): + raise ValueError( + f"Estimator '{name}' ({type(estimator).__name__}) does not have " + f"explicitly selected method '{method}'. Consider changing selected_methods " + f"or using only method_resolution_order to specify valid methods." ) - else: - if len(input_features) > 1: - return np.array( - [f"{self.feature_name}{i}" for i in range(len(input_features))] - ) - else: - return np.array([self.feature_name]) - else: - return np.array(input_features) - - -class NullEstimator(BaseEstimator, TransformerMixin, OneToOneFeatureMixin): - def __init__( - self, - accept_sparse=False, - ): - self.accept_sparse = accept_sparse - - def fit(self, X, y=None): - # Check and store the input - self.X_ = check_array( - X, accept_sparse=self.accept_sparse, force_all_finite="allow-nan" - ) - self.n_features_in_ = self.X_.shape[1] - self.feature_names_in_ = getattr(X, "columns", None) - return self - - def transform(self, X): - check_is_fitted(self) - X = check_array( - X, accept_sparse=self.accept_sparse, force_all_finite="allow-nan" + return method + + # Try methods in resolution order + for method in self.method_resolution_order: + if hasattr(estimator, method): + return method + + raise ValueError( + f"Estimator '{name}' ({type(estimator).__name__}) does not have any of " + f"the methods: {', '.join(self.method_resolution_order)}. Consider using " + f"method_resolution_order or selected_methods to specify valid methods." ) - # Check that the input is of the same shape as the one passed during fit. - if X.shape[1] != self.n_features_in_: - raise ValueError( - f"Shape of input is different from what was seen in `fit`" - f" Expected {self.n_features_in_} features, got {X.shape[1]}" - ) - return X + def _get_estimator_output( + self, estimator_tuple: Tuple[str, BaseEstimator], X: NDArray + ) -> NDArray: + """Get output from estimator using appropriate method.""" + name, estimator = estimator_tuple + method = self._get_method_name(estimator_tuple) + output = getattr(estimator, method)(X) - def predict(self, X): - return self.transform(X) + # Ensure 2D output + if output.ndim == 1: + output = output.reshape(-1, 1) + return output + + def transform(self, X: NDArray) -> NDArray: + """Transform X using the selected method for each estimator. - @available_if(lambda self: hasattr(self, "fitted_estimators_")) - def get_feature_names_out(self, input_features=None): - check_is_fitted(self) + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Input data to be transformed. + + Returns + ------- + ndarray of shape (n_samples, sum_n_output_features) + Horizontally stacked results of all estimators. + sum_n_output_features is the sum of n_output_features for each + estimator. + """ + Xs = self._parallel_func(X, self._get_estimator_output) + if not Xs: + # All transformers are None + return np.zeros((X.shape[0], 0)) + + if self.transformer_weights is not None: + Xs = [ + (Xs[name] * self.transformer_weights[name]) + if name in self.transformer_weights + else Xs[name] + for name in self._iter() + ] + + return np.hstack(Xs) - # Do I need to heck that the size of input_features is correct? - # if len(input_features) != self.n_features_out_: - # raise ValueError(f"Expected {self.n_features_in_} features, got {len(input_features)}") + def predict(self, X: NDArray) -> NDArray: + """Predict using all estimators. - if input_features: - return input_features - else: - return np.array([f"x{i}" for i in range(self.n_features_in_)]) + Alias for transform to maintain predictor interface. - def _more_tags(self): - return { - "allow_nan": True, - "X_types": ["2darray"] + (["sparse"] if self.accept_sparse else []), - } + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + Input data to be predicted. + + Returns + ------- + ndarray of shape (n_samples, sum_n_output_features) + Horizontally stacked predictions of all estimators. + """ + return self.transform(X) diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..340c986 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,145 @@ +"""Tests for EstimatorUnion adapter.""" + +import numpy as np +import pandas as pd +import pytest +from sklearn.exceptions import NotFittedError +from sklearn.preprocessing import StandardScaler + +from scikit_mol.adapters import EstimatorUnion +from scikit_mol.applicability import ( + MahalanobisApplicabilityDomain, + StandardizationApplicabilityDomain, +) + +# Use existing fixtures +from .fixtures import ( + atompair_transformer, + mols_list, + morgan_transformer, + skip_pandas_output_test, +) + + +def test_estimator_union_basic(morgan_transformer, atompair_transformer): + """Test basic functionality of EstimatorUnion.""" + + union = EstimatorUnion( + [ + ("fp1", morgan_transformer), + ( + "fp2", + atompair_transformer, + ), # Different radius for different features + ] + ) + + # Test unfitted raises exception + with pytest.raises(NotFittedError): + union.transform(mols_list) + + # Test fit and transform + union.fit(mols_list) + features = union.transform(mols_list) + + # Check output shape + n_fp = morgan_transformer().fpSize + assert features.shape == (len(mols_list), n_fp * 2) + + +def test_estimator_union_with_ad(morgan_transformer): + """Test EstimatorUnion with applicability domain estimator.""" + union = EstimatorUnion( + [ + ("fp", morgan_transformer), + ("ad", MahalanobisApplicabilityDomain()), + ], + method_resolution_order=("transform", "transform_score"), + ) + + union.fit(mols_list) + features = union.transform(mols_list) + + # Check output shape (fingerprints + 1 AD score) + n_fp = morgan_transformer().fpSize + assert features.shape == (len(mols_list), n_fp + 1) + + +def test_estimator_union_mixed_methods(morgan_transformer): + """Test EstimatorUnion with different methods specified.""" + union = EstimatorUnion( + [ + ("scale", StandardScaler(), "transform"), + ("ad", StandardizationApplicabilityDomain(), "transform_score"), + ("fp", morgan_transformer, "transform"), + ] + ) + + # Create some numeric data for StandardScaler + X = morgan_transformer.fit_transform(mols_list) + union.fit(X) + features = union.transform(X) + + # Check output shape + assert features.shape[0] == len(X) + assert features.shape[1] == X.shape[1] * 2 + 1 # scaled + fp + 1 AD score + + +@skip_pandas_output_test +def test_estimator_union_pandas_output(pandas_output, morgan_transformer): + """Test pandas DataFrame output from EstimatorUnion.""" + union = EstimatorUnion( + [ + ("fp", morgan_transformer), + ("ad", MahalanobisApplicabilityDomain(), "transform_score"), + ] + ) + + union.fit(mols_list) + features = union.transform(mols_list) + + # Check output type and structure + assert isinstance(features, pd.DataFrame) + assert len(features) == len(mols_list) + + # Check column names + fp_cols = [f"fp_{i}" for i in range(morgan_transformer.fpSize)] + expected_cols = fp_cols + ["Mahalanobis"] + assert features.columns.tolist() == expected_cols + + +def test_estimator_union_invalid_method(morgan_transformer): + """Test EstimatorUnion with invalid method specification.""" + with pytest.raises(ValueError): + EstimatorUnion([("fp", morgan_transformer, "invalid_method")]) + + +def test_estimator_union_get_feature_names_out(morgan_transformer): + """Test feature names output from EstimatorUnion.""" + union = EstimatorUnion( + [ + ("fp", morgan_transformer), + ("ad", MahalanobisApplicabilityDomain(), "transform_score"), + ] + ) + + union.fit(mols_list) + feature_names = union.get_feature_names_out() + + # Check number and format of feature names + n_fp = morgan_transformer().fpSize + assert len(feature_names) == n_fp + 1 + assert all(name.startswith("fp_") for name in feature_names[:-1]) + assert feature_names[-1] == "Mahalanobis" + + +def test_estimator_union_partial_fit(morgan_transformer): + """Test EstimatorUnion with some estimators already fitted.""" + fp = morgan_transformer.fit(mols_list) + ad = MahalanobisApplicabilityDomain() + + union = EstimatorUnion([("fp", fp), ("ad", ad, "transform_score")]) + + # Should work since fp is already fitted + features = union.fit_transform(mols_list) + assert features.shape == (len(mols_list), fp.fpSize + 1) From f2ab158d22a07c19b7c86e50abfd13843f6e0b41 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Wed, 19 Feb 2025 20:00:23 +0100 Subject: [PATCH 11/24] work in progress on adapters --- scikit_mol/adapters.py | 262 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 244 insertions(+), 18 deletions(-) diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index 4bedd40..9830472 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -2,8 +2,17 @@ import numpy as np from numpy.typing import NDArray -from sklearn.base import BaseEstimator +from sklearn.base import BaseEstimator, TransformerMixin from sklearn.pipeline import FeatureUnion +from sklearn.utils import Bunch +from sklearn.utils.metadata_routing import ( + _raise_for_params, + _routing_enabled, + process_routing, +) +from sklearn.utils.parallel import Parallel as skParallel +from sklearn.utils.parallel import delayed +from sklearn.utils.validation import check_is_fitted class EstimatorUnion(FeatureUnion): @@ -133,35 +142,108 @@ def _get_estimator_output( output = output.reshape(-1, 1) return output - def transform(self, X: NDArray) -> NDArray: - """Transform X using the selected method for each estimator. + # def transform_old(self, X: NDArray) -> NDArray: + # """Transform X using the selected method for each estimator. + + # Parameters + # ---------- + # X : array-like of shape (n_samples, n_features) + # Input data to be transformed. + + # Returns + # ------- + # ndarray of shape (n_samples, sum_n_output_features) + # Horizontally stacked results of all estimators. + # sum_n_output_features is the sum of n_output_features for each + # estimator. + # """ + # Xs = self._parallel_func(X, self._get_estimator_output) + # if not Xs: + # # All transformers are None + # return np.zeros((X.shape[0], 0)) + + # if self.transformer_weights is not None: + # Xs = [ + # (Xs[name] * self.transformer_weights[name]) + # if name in self.transformer_weights + # else Xs[name] + # for name in self._iter() + # ] + + # return np.hstack(Xs) + def _validate_transformers(self): + names, transformers = zip(*self.transformer_list) + + # validate names + self._validate_names(names) + + # validate estimators + for t in transformers: + if t in ("drop", "passthrough"): + continue + # TODO, make a check that the methods in the method_resolution_order /method mappting are present + # if not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not hasattr( + # t, "transform" + # ): + # raise TypeError( + # "All estimators should implement fit and " + # "transform. '%s' (type %s) doesn't" % (t, type(t)) + # ) + + def transform(self, X, **params): + """Transform X separately by each transformer, concatenate results. Parameters ---------- - X : array-like of shape (n_samples, n_features) + X : iterable or array-like, depending on transformers Input data to be transformed. + **params : dict, default=None + + Parameters routed to the `transform` method of the sub-transformers via the + metadata routing API. See :ref:`Metadata Routing User Guide + ` for more details. + + .. versionadded:: 1.5 + Returns ------- - ndarray of shape (n_samples, sum_n_output_features) - Horizontally stacked results of all estimators. - sum_n_output_features is the sum of n_output_features for each - estimator. + X_t : array-like or sparse matrix of shape (n_samples, sum_n_components) + The `hstack` of results of transformers. `sum_n_components` is the + sum of `n_components` (output dimension) over transformers. """ - Xs = self._parallel_func(X, self._get_estimator_output) + _raise_for_params(params, self, "transform") + + if _routing_enabled(): + routed_params = process_routing(self, "transform", **params) + else: + # TODO(SLEP6): remove when metadata routing cannot be disabled. + routed_params = Bunch() + for name, _ in self.transformer_list: + routed_params[name] = Bunch(transform={}) + + # Build delayed jobs with custom methods + delayed_jobs = [] + for name, trans, weight in self._iter(): + method_name = self._get_method_name((name, trans)) + delayed_jobs.append( + delayed(_transform_one)( + trans, + X, + None, + weight, + params=routed_params[name], + method=method_name, + ) + ) + + Xs = skParallel(n_jobs=self.n_jobs)(delayed_jobs) + if not Xs: # All transformers are None return np.zeros((X.shape[0], 0)) - if self.transformer_weights is not None: - Xs = [ - (Xs[name] * self.transformer_weights[name]) - if name in self.transformer_weights - else Xs[name] - for name in self._iter() - ] - - return np.hstack(Xs) + return self._hstack(Xs) def predict(self, X: NDArray) -> NDArray: """Predict using all estimators. @@ -179,3 +261,147 @@ def predict(self, X: NDArray) -> NDArray: Horizontally stacked predictions of all estimators. """ return self.transform(X) + + +def _transform_one(transformer, X, y, weight, params=None, method="transform"): + """Call transform and apply weight to output. + + Parameters + ---------- + transformer : estimator + Estimator to be used for transformation. + + X : {array-like, sparse matrix} of shape (n_samples, n_features) + Input data to be transformed. + + y : ndarray of shape (n_samples,) + Ignored. + + weight : float + Weight to be applied to the output of the transformation. + + method : str + Method to use for transformation (e.g. "transform", "predict", "predict_proba"). + + params : dict + Parameters to be passed to the transformer's ``transform`` method. + + This should be of the form ``process_routing()["step_name"]``. + """ + res = getattr(transformer, method)(X, **params.transform) + # Ensure 2D output + if res.ndim == 1: + res = res.reshape(-1, 1) + # if we have a weight for this transformer, multiply output + if weight is None: + return res + return res * weight + + +# def _fit_transform_one( +# transformer, X, y, weight, message_clsname="", message=None, params=None, method="transform" +# ): +# """ +# Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned +# with the fitted transformer. If ``weight`` is not ``None``, the result will +# be multiplied by ``weight``. + +# ``params`` needs to be of the form ``process_routing()["step_name"]``. +# """ +# params = params or {} +# with _print_elapsed_time(message_clsname, message): +# if hasattr(transformer, "fit_transform"): +# res = transformer.fit_transform(X, y, **params.get("fit_transform", {})) +# else: +# res = transformer.fit(X, y, **params.get("fit", {})).transform( +# X, **params.get("transform", {}) +# ) + +# if weight is None: +# return res, transformer +# return res * weight, transformer + + +class PredictToTransformAdapter(TransformerMixin, BaseEstimator): + """Adapter that exposes an estimator's predict method as transform. + + Parameters + ---------- + estimator : BaseEstimator + Estimator with a predict method. + method : str, default="predict" + The method to use for transformation (e.g., "predict", "predict_proba"). + """ + + def __init__(self, estimator: BaseEstimator, method: str = "predict"): + self.estimator = estimator + self.method = method + + def fit(self, X, y=None): + self.estimator.fit(X, y) + return self + + def transform(self, X): + check_is_fitted(self) + return getattr(self.estimator, self.method)(X) + + def get_feature_names_out(self, input_features=None): + """Delegate feature names to wrapped estimator if available.""" + if hasattr(self.estimator, "get_feature_names_out"): + return self.estimator.get_feature_names_out(input_features) + return None + + def __sklearn_is_fitted__(self): + """Delegate fit check to wrapped estimator.""" + try: + check_is_fitted(self.estimator) + return True + except ValueError: + return False + + def _repr_html_(self): + """HTML representation for notebooks.""" + if hasattr(self.estimator, "_repr_html_"): + return f"
PredictToTransformAdapter using method '{self.method}' on:
{self.estimator._repr_html_()}
" + return f"
PredictToTransformAdapter(method='{self.method}', estimator={self.estimator})
" + + +class TransformToPredictAdapter(BaseEstimator): + """Adapter that exposes an estimator's transform method as predict. + + Parameters + ---------- + transformer : BaseEstimator + Estimator with a transform method. + """ + + def __init__(self, transformer: BaseEstimator): + self.transformer = transformer + + def fit(self, X, y=None): + self.transformer.fit(X, y) + return self + + def predict(self, X): + check_is_fitted(self) + return self.transformer.transform(X) + + def get_feature_names_out(self, input_features=None): + """Delegate feature names to wrapped transformer if available.""" + if hasattr(self.transformer, "get_feature_names_out"): + return self.transformer.get_feature_names_out(input_features) + return None + + def __sklearn_is_fitted__(self): + """Delegate fit check to wrapped transformer.""" + try: + check_is_fitted(self.transformer) + return True + except ValueError: + return False + + def _repr_html_(self): + """HTML representation for notebooks.""" + if hasattr(self.transformer, "_repr_html_"): + return f"
TransformToPredictAdapter on:
{self.transformer._repr_html_()}
" + return f"
TransformToPredictAdapter(transformer={self.transformer})
" From 42739365cb13be4e80314710101cd8caf798da94 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 22 Feb 2025 10:40:23 +0100 Subject: [PATCH 12/24] It seems to be getting there with the EstimatorUnion. Got feature_names to work. --- scikit_mol/adapters.py | 40 +++++++++++++++++++++++++++++++- scikit_mol/applicability/base.py | 11 +++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index 9830472..6ad54d8 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -16,7 +16,7 @@ class EstimatorUnion(FeatureUnion): - """A more flexible version of FeatureUnion that supports various estimator types. + """EXPERIMENTAL: more flexible version of FeatureUnion that supports various estimator types. This class extends scikit-learn's FeatureUnion to support estimators with different method interfaces (predict, transform, etc.) and allows explicit method selection. @@ -41,6 +41,8 @@ class EstimatorUnion(FeatureUnion): names, values are weights. verbose : bool, default=False If True, the time elapsed while fitting each transformer will be printed. + verbose_feature_names_out : bool, default=True + If True, the feature names out will be verbose. Attributes ---------- @@ -68,6 +70,7 @@ def __init__( n_jobs: Optional[int] = None, transformer_weights: Optional[Dict[str, float]] = None, verbose: bool = False, + verbose_feature_names_out: bool = True, ) -> None: # Store all parameters as properties self.estimator_list = estimator_list @@ -76,6 +79,7 @@ def __init__( self.n_jobs = n_jobs self.transformer_weights = transformer_weights self.verbose = verbose + self.verbose_feature_names_out = verbose_feature_names_out @property def estimator_list(self) -> List[Tuple[str, BaseEstimator]]: @@ -262,6 +266,40 @@ def predict(self, X: NDArray) -> NDArray: """ return self.transform(X) + def get_feature_names_out(self, input_features=None): + """Get output feature names for transformation. + + Parameters + ---------- + input_features : array-like of str or None, default=None + Input features. + + Returns + ------- + feature_names_out : ndarray of str objects + Transformed feature names. + """ + # List of tuples (name, feature_names_out) + transformer_with_feature_names_out = [] + for name, trans, _ in self._iter(): + if hasattr(trans, "predict") and not hasattr( + trans, "get_feature_names_out" + ): + # Assume predictors only return 1D output and thus we use their name as feature_name + feature_names_out = np.array([name]) + elif not hasattr(trans, "get_feature_names_out"): + raise AttributeError( + "Transformer %s (type %s) does not provide get_feature_names_out." + % (str(name), type(trans).__name__) + ) + else: + feature_names_out = trans.get_feature_names_out(input_features) + transformer_with_feature_names_out.append((name, feature_names_out)) + + return self._add_prefix_for_feature_names_out( + transformer_with_feature_names_out + ) + def _transform_one(transformer, X, y, weight, params=None, method="transform"): """Call transform and apply weight to output. diff --git a/scikit_mol/applicability/base.py b/scikit_mol/applicability/base.py index c8cbb62..813a8b1 100644 --- a/scikit_mol/applicability/base.py +++ b/scikit_mol/applicability/base.py @@ -203,7 +203,13 @@ def _transform(self, X: NDArray) -> NDArray[np.float64]: def predict( self, X: Union[ArrayLike, pd.DataFrame] ) -> Union[NDArray[np.int_], pd.DataFrame]: - """Predict whether samples are within the applicability domain.""" + """Predict whether samples are within the applicability domain. + + Returns + ------- + predictions : ndarray of shape (n_samples,) + Returns 1 for inside and -1 for outside. + """ check_is_fitted(self) X = check_array(X, **self._check_params) @@ -244,6 +250,7 @@ def score_transform( # No sign flip needed return (1 / (1 + np.exp(self.threshold_ - scores))).reshape(-1, 1) - def get_feature_names_out(self) -> NDArray[np.str_]: + def get_feature_names_out(self, input_features=None) -> NDArray[np.str_]: """Get feature name for output column.""" + # TODO: what is the mechanism around input_features? return np.array([f"{self.feature_name}"]) From 159432f93e9abe47106ccd846b4e90b84984d6ad Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 22 Feb 2025 13:41:22 +0100 Subject: [PATCH 13/24] predicttotransformwrapper seems to be working now. --- scikit_mol/adapters.py | 84 ++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index 6ad54d8..ff9c8b6 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -5,6 +5,7 @@ from sklearn.base import BaseEstimator, TransformerMixin from sklearn.pipeline import FeatureUnion from sklearn.utils import Bunch +from sklearn.utils._estimator_html_repr import _VisualBlock from sklearn.utils.metadata_routing import ( _raise_for_params, _routing_enabled, @@ -361,47 +362,66 @@ def _transform_one(transformer, X, y, weight, params=None, method="transform"): class PredictToTransformAdapter(TransformerMixin, BaseEstimator): - """Adapter that exposes an estimator's predict method as transform. - - Parameters - ---------- - estimator : BaseEstimator - Estimator with a predict method. - method : str, default="predict" - The method to use for transformation (e.g., "predict", "predict_proba"). - """ + """Adapter that exposes an estimator's predict method as transform.""" def __init__(self, estimator: BaseEstimator, method: str = "predict"): self.estimator = estimator self.method = method - def fit(self, X, y=None): - self.estimator.fit(X, y) - return self - def transform(self, X): check_is_fitted(self) - return getattr(self.estimator, self.method)(X) - - def get_feature_names_out(self, input_features=None): - """Delegate feature names to wrapped estimator if available.""" - if hasattr(self.estimator, "get_feature_names_out"): - return self.estimator.get_feature_names_out(input_features) - return None + prediction = getattr(self.estimator, self.method)(X) + if prediction.ndim == 1: + prediction = prediction.reshape(-1, 1) + return prediction + + def __getattr__(self, name): + """Delegate any unknown attributes/methods to wrapped estimator.""" + if hasattr(self.estimator, name): + attr = getattr(self.estimator, name) + # If it's a property, get its value + if isinstance(attr, property): + return attr.__get__(self.estimator) + return attr + raise AttributeError( + f"Neither {self.__class__.__name__} nor {self.estimator.__class__.__name__} " + f"has attribute '{name}'" + ) - def __sklearn_is_fitted__(self): - """Delegate fit check to wrapped estimator.""" - try: - check_is_fitted(self.estimator) - return True - except ValueError: - return False + def __dir__(self): + """List all attributes including those from wrapped estimator.""" + return list(set(super().__dir__() + dir(self.estimator))) - def _repr_html_(self): - """HTML representation for notebooks.""" - if hasattr(self.estimator, "_repr_html_"): - return f"
PredictToTransformAdapter using method '{self.method}' on:
{self.estimator._repr_html_()}
" - return f"
PredictToTransformAdapter(method='{self.method}', estimator={self.estimator})
" + @property + def __dict__(self): + """Include estimator's properties in the instance dict.""" + # Get our own dict + d = super().__dict__.copy() + + # Add estimator instance attributes and properties + estimator_dict = vars(self.estimator) + for name, value in estimator_dict.items(): + if not name.startswith("_"): # Skip private attributes + d[name] = value + + return d + + def _sk_visual_block_(self): + """Generate information about how to display the adapter.""" + return _VisualBlock( + "parallel", + [self.estimator], + names=None, + # [ + # f"{self.estimator.__class__.__name__}", + # ], + name_details=None, + # [ + # f"{self.method} from {self.estimator}", + # ], + name_caption=None, + dash_wrapped=False, + ) class TransformToPredictAdapter(BaseEstimator): From 927900822ce9bd9962551da394b88a441c2a140d Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 22 Feb 2025 14:06:50 +0100 Subject: [PATCH 14/24] Also got the fit_transform to work. Seemingly getting there. --- scikit_mol/adapters.py | 197 +++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 76 deletions(-) diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index ff9c8b6..39931c8 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -6,6 +6,7 @@ from sklearn.pipeline import FeatureUnion from sklearn.utils import Bunch from sklearn.utils._estimator_html_repr import _VisualBlock +from sklearn.utils._user_interface import _print_elapsed_time from sklearn.utils.metadata_routing import ( _raise_for_params, _routing_enabled, @@ -250,6 +251,61 @@ def transform(self, X, **params): return self._hstack(Xs) + def fit_transform(self, X, y=None, **params): + """Fit all transformers, transform the data and concatenate results. + + Parameters + ---------- + X : iterable or array-like, depending on transformers + Input data to be transformed. + + y : array-like of shape (n_samples, n_outputs), default=None + Targets for supervised learning. + + **params : dict, default=None + - If `enable_metadata_routing=False` (default): + Parameters directly passed to the `fit` methods of the + sub-transformers. + + - If `enable_metadata_routing=True`: + Parameters safely routed to the `fit` methods of the + sub-transformers. See :ref:`Metadata Routing User Guide + ` for more details. + + .. versionchanged:: 1.5 + `**params` can now be routed via metadata routing API. + + Returns + ------- + X_t : array-like or sparse matrix of \ + shape (n_samples, sum_n_components) + The `hstack` of results of transformers. `sum_n_components` is the + sum of `n_components` (output dimension) over transformers. + """ + if _routing_enabled(): + routed_params = process_routing(self, "fit_transform", **params) + else: + # TODO(SLEP6): remove when metadata routing cannot be disabled. + routed_params = Bunch() + for name, obj in self.transformer_list: + if hasattr(obj, "fit_transform"): + routed_params[name] = Bunch(fit_transform={}) + routed_params[name].fit_transform = params + else: + routed_params[name] = Bunch(fit={}) + routed_params[name] = Bunch(transform={}) + routed_params[name].fit = params + + results = self._parallel_func(X, y, _fit_transform_one, routed_params) + if not results: + # All transformers are None + return np.zeros((X.shape[0], 0)) + + Xs, transformers = zip(*results) + self._update_transformer_list(transformers) + + return self._hstack(Xs) + def predict(self, X: NDArray) -> NDArray: """Predict using all estimators. @@ -337,49 +393,57 @@ def _transform_one(transformer, X, y, weight, params=None, method="transform"): return res * weight -# def _fit_transform_one( -# transformer, X, y, weight, message_clsname="", message=None, params=None, method="transform" -# ): -# """ -# Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned -# with the fitted transformer. If ``weight`` is not ``None``, the result will -# be multiplied by ``weight``. +# Ouch, this seem to be a problem with the EstimatorUnion class. +def _fit_transform_one( + transformer, + X, + y, + weight, + message_clsname="", + message=None, + params=None, + method="transform", +): + """ + Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned + with the fitted transformer. If ``weight`` is not ``None``, the result will + be multiplied by ``weight``. -# ``params`` needs to be of the form ``process_routing()["step_name"]``. -# """ -# params = params or {} -# with _print_elapsed_time(message_clsname, message): -# if hasattr(transformer, "fit_transform"): -# res = transformer.fit_transform(X, y, **params.get("fit_transform", {})) -# else: -# res = transformer.fit(X, y, **params.get("fit", {})).transform( -# X, **params.get("transform", {}) -# ) + ``params`` needs to be of the form ``process_routing()["step_name"]``. + """ + params = params or {} + with _print_elapsed_time(message_clsname, message): + if hasattr(transformer, "fit_transform"): + res = transformer.fit_transform(X, y, **params.get("fit_transform", {})) + elif hasattr(transformer, "transform"): + res = transformer.fit(X, y, **params.get("fit", {})).transform( + X, **params.get("transform", {}) + ) + elif hasattr(transformer, "predict"): + transformer.fit(X, y, **params.get("fit", {})) + res = transformer.predict(X, **params.get("predict", {})) + if res.ndim == 1: + res = res.reshape(-1, 1) + else: + raise ValueError( + f"Transformer {transformer} does not have a fit_transform, fit or predict method." + ) -# if weight is None: -# return res, transformer -# return res * weight, transformer + if weight is None: + return res, transformer + return res * weight, transformer -class PredictToTransformAdapter(TransformerMixin, BaseEstimator): - """Adapter that exposes an estimator's predict method as transform.""" +class _BaseAdapter(BaseEstimator): + """EXPERIMENTAL: Base class for adapters that wrap estimators and modify their interface.""" - def __init__(self, estimator: BaseEstimator, method: str = "predict"): + def __init__(self, estimator: BaseEstimator): self.estimator = estimator - self.method = method - - def transform(self, X): - check_is_fitted(self) - prediction = getattr(self.estimator, self.method)(X) - if prediction.ndim == 1: - prediction = prediction.reshape(-1, 1) - return prediction def __getattr__(self, name): """Delegate any unknown attributes/methods to wrapped estimator.""" if hasattr(self.estimator, name): attr = getattr(self.estimator, name) - # If it's a property, get its value if isinstance(attr, property): return attr.__get__(self.estimator) return attr @@ -395,15 +459,11 @@ def __dir__(self): @property def __dict__(self): """Include estimator's properties in the instance dict.""" - # Get our own dict d = super().__dict__.copy() - - # Add estimator instance attributes and properties estimator_dict = vars(self.estimator) for name, value in estimator_dict.items(): - if not name.startswith("_"): # Skip private attributes + if not name.startswith("_"): d[name] = value - return d def _sk_visual_block_(self): @@ -412,54 +472,39 @@ def _sk_visual_block_(self): "parallel", [self.estimator], names=None, - # [ - # f"{self.estimator.__class__.__name__}", - # ], name_details=None, - # [ - # f"{self.method} from {self.estimator}", - # ], name_caption=None, dash_wrapped=False, ) -class TransformToPredictAdapter(BaseEstimator): - """Adapter that exposes an estimator's transform method as predict. +class PredictToTransformAdapter(_BaseAdapter, TransformerMixin): + """EXPERIMENTAL: Adapter that exposes an estimator's predict method as transform.""" - Parameters - ---------- - transformer : BaseEstimator - Estimator with a transform method. - """ + def __init__(self, estimator: BaseEstimator, method: str = "predict"): + super().__init__(estimator) + self.method = method - def __init__(self, transformer: BaseEstimator): - self.transformer = transformer + def transform(self, X): + check_is_fitted(self) + prediction = getattr(self.estimator, self.method)(X) + if prediction.ndim == 1: + prediction = prediction.reshape(-1, 1) + return prediction - def fit(self, X, y=None): - self.transformer.fit(X, y) - return self + +class TransformToPredictAdapter(_BaseAdapter, TransformerMixin): + """EXPERIMENTAL: Adapter that exposes an estimator's transform method as predict. + + 2D column vector output is flattened to 1D.""" + + def __init__(self, estimator: BaseEstimator, method: str = "transform"): + super().__init__(estimator) + self.method = method def predict(self, X): check_is_fitted(self) - return self.transformer.transform(X) - - def get_feature_names_out(self, input_features=None): - """Delegate feature names to wrapped transformer if available.""" - if hasattr(self.transformer, "get_feature_names_out"): - return self.transformer.get_feature_names_out(input_features) - return None - - def __sklearn_is_fitted__(self): - """Delegate fit check to wrapped transformer.""" - try: - check_is_fitted(self.transformer) - return True - except ValueError: - return False - - def _repr_html_(self): - """HTML representation for notebooks.""" - if hasattr(self.transformer, "_repr_html_"): - return f"
TransformToPredictAdapter on:
{self.transformer._repr_html_()}
" - return f"
TransformToPredictAdapter(transformer={self.transformer})
" + prediction = self.estimator.transform(X) + if prediction.shape[1] == 1: + prediction = prediction.flatten() + return prediction From d19322f6f57091fee3e25470b52ff9639cb038ea Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Mar 2025 09:43:48 +0100 Subject: [PATCH 15/24] Experimental adapters. WIP --- scikit_mol/adapters.py | 95 +++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py index 39931c8..5e3b2f0 100644 --- a/scikit_mol/adapters.py +++ b/scikit_mol/adapters.py @@ -18,7 +18,7 @@ class EstimatorUnion(FeatureUnion): - """EXPERIMENTAL: more flexible version of FeatureUnion that supports various estimator types. + """EXPERIMENTAL: A more flexible version of FeatureUnion that supports various estimator types. This class extends scikit-learn's FeatureUnion to support estimators with different method interfaces (predict, transform, etc.) and allows explicit method selection. @@ -148,35 +148,6 @@ def _get_estimator_output( output = output.reshape(-1, 1) return output - # def transform_old(self, X: NDArray) -> NDArray: - # """Transform X using the selected method for each estimator. - - # Parameters - # ---------- - # X : array-like of shape (n_samples, n_features) - # Input data to be transformed. - - # Returns - # ------- - # ndarray of shape (n_samples, sum_n_output_features) - # Horizontally stacked results of all estimators. - # sum_n_output_features is the sum of n_output_features for each - # estimator. - # """ - # Xs = self._parallel_func(X, self._get_estimator_output) - # if not Xs: - # # All transformers are None - # return np.zeros((X.shape[0], 0)) - - # if self.transformer_weights is not None: - # Xs = [ - # (Xs[name] * self.transformer_weights[name]) - # if name in self.transformer_weights - # else Xs[name] - # for name in self._iter() - # ] - - # return np.hstack(Xs) def _validate_transformers(self): names, transformers = zip(*self.transformer_list) @@ -233,7 +204,9 @@ def transform(self, X, **params): for name, trans, weight in self._iter(): method_name = self._get_method_name((name, trans)) delayed_jobs.append( - delayed(_transform_one)( + delayed( + _transform_one + )( # Seems like the only reason we modify this method from base class is to handle it with a custom function for parallel processing trans, X, None, @@ -393,7 +366,6 @@ def _transform_one(transformer, X, y, weight, params=None, method="transform"): return res * weight -# Ouch, this seem to be a problem with the EstimatorUnion class. def _fit_transform_one( transformer, X, @@ -419,6 +391,7 @@ def _fit_transform_one( res = transformer.fit(X, y, **params.get("fit", {})).transform( X, **params.get("transform", {}) ) + # Custom handling of methods that has predict but no fit_transform or transform elif hasattr(transformer, "predict"): transformer.fit(X, y, **params.get("fit", {})) res = transformer.predict(X, **params.get("predict", {})) @@ -437,8 +410,22 @@ def _fit_transform_one( class _BaseAdapter(BaseEstimator): """EXPERIMENTAL: Base class for adapters that wrap estimators and modify their interface.""" - def __init__(self, estimator: BaseEstimator): + def __init__( + self, estimator: BaseEstimator, _feature_names_out: Optional[List[str]] = None + ): + """Initialize the adapter with an estimator.""" self.estimator = estimator + self._feature_names_out = _feature_names_out + + def get_feature_names_out(self, input_features=None): + """Get output feature names for transformation. + + Parameters + ---------- + input_features : array-like of str or None, default=None + Input features.""" + + return ["tester"] def __getattr__(self, name): """Delegate any unknown attributes/methods to wrapped estimator.""" @@ -467,6 +454,7 @@ def __dict__(self): return d def _sk_visual_block_(self): + # TODO: this looks strange when putting the wrapped estimator into a pipeline """Generate information about how to display the adapter.""" return _VisualBlock( "parallel", @@ -482,11 +470,36 @@ class PredictToTransformAdapter(_BaseAdapter, TransformerMixin): """EXPERIMENTAL: Adapter that exposes an estimator's predict method as transform.""" def __init__(self, estimator: BaseEstimator, method: str = "predict"): + """Initialize the adapter with an estimator and a method to use. + + Parameters + ---------- + estimator : BaseEstimator + The estimator to wrap. + method : str, default="predict" + The method to use for transformation. + """ super().__init__(estimator) self.method = method def transform(self, X): + """Transform X using the wrapped estimator's specified method. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The input data to transform. check_is_fitted(self) + + Example + -------- + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn_mol.adapters import PredictToTransformAdapter + >>> estimator = LogisticRegression() + >>> adapter = PredictToTransformAdapter(estimator, method="predict"") + >>> adapter.fit(X, y) + >>> adapter.transform(X) + """ prediction = getattr(self.estimator, self.method)(X) if prediction.ndim == 1: prediction = prediction.reshape(-1, 1) @@ -503,6 +516,22 @@ def __init__(self, estimator: BaseEstimator, method: str = "transform"): self.method = method def predict(self, X): + """Predict using the wrapped estimator's specified method. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The input data to predict. + + Example + -------- + >>> from sklearn.preprocessing import StandardScaler + >>> from sklearn_mol.adapters import TransformToPredictAdapter + >>> estimator = StandardScaler() + >>> adapter = TransformToPredictAdapter(estimator, method="transform") + >>> adapter.fit(X, y) + >>> adapter.predict(X) + """ check_is_fitted(self) prediction = self.estimator.transform(X) if prediction.shape[1] == 1: From 8dfa31258ff65ecb91c1ee7829b2141ae965c808 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Mar 2025 10:01:31 +0100 Subject: [PATCH 16/24] Cleaning up. --- docs/notebooks/12_applicability_domain.ipynb | 1660 ++++++++++++++++++ scikit_mol/adapters.py | 539 ------ scikit_mol/applicability_old.py | 276 --- 3 files changed, 1660 insertions(+), 815 deletions(-) create mode 100644 docs/notebooks/12_applicability_domain.ipynb delete mode 100644 scikit_mol/adapters.py delete mode 100644 scikit_mol/applicability_old.py diff --git a/docs/notebooks/12_applicability_domain.ipynb b/docs/notebooks/12_applicability_domain.ipynb new file mode 100644 index 0000000..fcac2a4 --- /dev/null +++ b/docs/notebooks/12_applicability_domain.ipynb @@ -0,0 +1,1660 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ee549099", + "metadata": {}, + "source": [ + "# Applicability Domain Estimation\n", + "\n", + "This notebook demonstrates how to use scikit-mol's applicability domain estimators to assess whether new compounds are within the domain of applicability of a trained model.\n", + "\n", + "We'll explore two different approaches:\n", + "1. Using Morgan binary fingerprints with a k-Nearest Neighbors based applicability domain\n", + "2. Using count-based Morgan fingerprints with dimensionality reduction and a leverage-based applicability domain\n", + "\n", + "First, let's import the necessary libraries and load our dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "40500fae", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from rdkit import Chem\n", + "from rdkit.Chem import Draw\n", + "from rdkit.Chem import PandasTools\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.ensemble import RandomForestRegressor\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.decomposition import PCA\n", + "import pathlib\n", + "\n", + "\n", + "from scikit_mol.fingerprints import MorganFingerprintTransformer\n", + "from scikit_mol.applicability import KNNApplicabilityDomain, LeverageApplicabilityDomain" + ] + }, + { + "cell_type": "markdown", + "id": "e5d1277e", + "metadata": {}, + "source": [ + "## Load and Prepare Data" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "79d3b853", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 out of 7228 SMILES failed in conversion\n" + ] + } + ], + "source": [ + "full_set = True\n", + "\n", + "if full_set:\n", + " csv_file = \"SLC6A4_active_excape_export.csv\"\n", + " if not pathlib.Path(csv_file).exists():\n", + " import urllib.request\n", + "\n", + " url = \"https://ndownloader.figshare.com/files/25747817\"\n", + " urllib.request.urlretrieve(url, csv_file)\n", + "else:\n", + " csv_file = \"../tests/data/SLC6A4_active_excapedb_subset.csv\"\n", + "\n", + "data = pd.read_csv(csv_file)\n", + "\n", + "#Could also build a pipeline to convert the smiles to mols using SmilesToMolTransformer\n", + "PandasTools.AddMoleculeColumnToFrame(data, smilesCol=\"SMILES\")\n", + "print(f\"{data.ROMol.isna().sum()} out of {len(data)} SMILES failed in conversion\")\n", + "\n", + "# Split into train/val/test\n", + "X = data.ROMol\n", + "y = data.pXC50\n", + "\n", + "X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + "X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=42)" + ] + }, + { + "cell_type": "markdown", + "id": "e2896ad5", + "metadata": {}, + "source": [ + "## Example 1: k-NN Applicability Domain with Binary Morgan Fingerprints\n", + "\n", + "In this example, we'll use binary Morgan fingerprints and a k-NN based applicability domain with Tanimoto distance.\n", + "This is particularly suitable for binary fingerprints as the Tanimoto coefficient is a natural similarity measure for them." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9c89148b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n" + ] + }, + { + "data": { + "text/html": [ + "
Pipeline(steps=[('fp', MorganFingerprintTransformer()),\n",
+       "                ('rf', RandomForestRegressor(n_jobs=-1, random_state=61453))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "Pipeline(steps=[('fp', MorganFingerprintTransformer()),\n", + " ('rf', RandomForestRegressor(n_jobs=-1, random_state=61453))])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create pipeline for binary fingerprints\n", + "binary_fp_pipe = Pipeline([\n", + " ('fp', MorganFingerprintTransformer(fpSize=2048, radius=2)),\n", + " ('rf', RandomForestRegressor(n_estimators=100, random_state=0xf00d, n_jobs=-1))\n", + "])\n", + "\n", + "# Train the model\n", + "binary_fp_pipe.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ee7b2f64", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Predicted pXC50 vs Absolute Error')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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9n333Zeampohnz3yyCMBWLJkCW1tbaMe//zzz+fmm28uhwFKyOfz/PnPf+bDH/4wmqbR1dXFaaedRlNTE1/5yleora1l7dq1/PGPfxzXdSxevJiVK1dyxx13YJom733ve/ntb387wqiVcN555zFr1iyuvfZannnmGf73f/+X/v5+fvWrXwGwbNkyzj77bA4++GC+/e1vEwqFWLlyJU899VT5GIVCgRNPPJGVK1dyxRVXMHv2bO655x4+/OEPMzAwMOoLqVL8+te/5pJLLuHII48sv6Tmzp0LQGdnJ0cffXTZUDU1NfHXv/6Vj370o6TTaT7zmc9s9fiO44yaxIrFYkQikfLvY81ngH/+85/cfffdXHHFFTQ2NjJr1iyWLVvG2972NmpqavjSl76EYRjcdtttnHjiiTz++OMcddRRQ8532WWX0dTUxDe+8Q1yudz4bs52+as7CKXl9iOPPCK6u7vF+vXrxe9//3vR0NAgIpGI2LBhgxBCiIsuukgA4itf+cqQz//rX/8SgPjtb387ZPtDDz00ZHtXV5cwTVOcddZZwvf98n6lJefgpcejjz4qAPHoo48KIYRwXVfMnj1bzJw5U/T39w85z+BjjbXc3hFjHA1r1qwRwJD7JsTmpe1nP/vZUT93wQUXCEAkEgmxfv368vZUKiUA8e53v3uL5x2Miy++WHzrW98S9957r/jVr34l3vWudwlAnHfeeUP2O+igg8TJJ5884vPLli0TgLj11lvHPIfv+2LatGnife9735Dtd999twDEE088IYQQ4r777hs1lDNeXHHFFaKtra38Xfz9738XgHjppZeG7Fdabr/rXe8asv2yyy4TgHj55ZeFEELccMMNW12W33jjjQIQv/nNb8rbbNsWxxxzjIjH4yKdTpe3M2y5fdFFF4mZM2eOOGZpfIMx1nL7ox/9qJgyZYro6ekZsv0DH/iASCaTIp/Pjzl2IeRyGxj159prrx0y1tHmc+m6VFUVy5YtG7L9nHPOEaZpilWrVpW3bdq0SSQSCXHCCSeUt5VsyvHHHy9c193ieIdjUi+3Tz31VJqammhra+MDH/gA8Xic++67j2nTpg3Zb3jg/J577iGZTPL2t7+dnp6e8s9b3/pW4vE4jz76KACPPPIItm3zqU99asjSYzxvxpdeeok1a9bwmc98htra2iF/G3yssbAzxjgY55xzzpD7duSRR3LUUUfx4IMPjrp/Y2MjAAceeOCQZUk6nQYgkUiM+9w///nP+eY3v8l73/tePvjBD/KnP/2Jj33sY9x9991DlkWFQoFQKDTi8+FwuPz3saAoCueeey4PPvjgkCX9XXfdxbRp08pJn9J39cADD+A4zrivAaSXc9ddd3H++eeXv4uTTz6Z5uZmfvvb3476mcsvv3zI75/61KcAyve9NJ4//elP+L4/6jEefPBBWltbueCCC8rbDMPg05/+NNlslscff7yi66gEQgjuvfde3vnOdyKEGPKsnn766aRSKV588cWtHqfEUhj+M/iaShgrEbZw4UIOPPDA8u+e5/H3v/+dc845hzlz5pS3T5kyhQsvvJAnn3yy/LyW8LGPfQxN08Z7+cAkpQCVcPPNN/Pwww/z6KOP8uqrr7J69WpOP/30Ifvouj4itrBixQpSqRTNzc00NTUN+clms3R1dQHw5ptvArDPPvsM+XxTUxN1dXVbHFtp6T9//vxturadMcbBGP55gH333Ze1a9eO2P78889z8803M3/+fBYvXsxvfvOb8t9KS+FMJjPuc4+GUqbzkUceKW+LRCJYljVi31IMb/CybDScf/75FAoF/u///g+AbDbLgw8+yLnnnls2agsXLuR973sfV199NY2Njbz73e/mjjvuGPW8w/H3v/+d7u5ujjzySFauXMnKlStZs2YNJ510Er/73e9GNXLD7/vcuXNRVbV8388//3yOO+44LrnkElpaWvjABz7A3XffPeRYb775Jvvssw+qOnS6HnDAAeW/7yh0d3czMDDA7bffPuI5/chHPgJQfla3hMbGRk499dQRPzNnzhyy32jzuYTZs2ePGFs+n2e//fYbse8BBxyA7/sj4tjDjzEeTOqY5JFHHsnhhx++xX1CodCIh8f3/S2+3ZuamiZsjNuKyTpGz/P4+Mc/ztSpU3nqqac47bTT+PznP8/ZZ59NbW0tNTU1TJ06ddSkTyUoxRb7+vrK26ZMmcLGjRtH7Nve3g5snS5y9NFHM2vWLO6++24uvPBC/vznP1MoFDj//PPL+yiKwh/+8AeeeeYZ/vznP/O3v/2Niy++mOuvv55nnnmGeDw+5vFL39V555036t8ff/xxTjrppC2OcfgqIxKJ8MQTT/Doo4/yl7/8hYceeoi77rqLk08+mb///e8Vez1bO18JpSTW1lAy1v/93//NRRddNOo+Bx988LYNbhSMNp9L2NpLcjzYlmNMaiO5rZg7dy6PPPIIxx133BZvSukttmLFiiHuend394gM82jnAFi6dCmnnnrqmPuN9ZDujDEOxooVK0ZsW758+YjM5//+7//y0ksvcd9991FTU8Ott97K4Ycfzle+8hVuvfVWAM4++2xuv/12nn76aY455phxj2EwVq9eDQx9GRx66KE8+uijpNPpIcmbxYsXl/++NZx33nn8+Mc/Jp1Oc9dddzFr1iyOPvroEfsdffTRHH300Xz3u99l0aJF/Nd//Re///3vueSSS0Y9bi6X409/+hPnn38+73//+0f8/dOf/jS//e1vRxjJFStWDPFeVq5cie/7Q+67qqqccsopnHLKKfzoRz/ie9/7Hl/72td49NFHy97Wf/7zH3zfH2JAXn/9dYAR3thg1NXVMTAwMGL7aN7naM9qU1MTiUQCz/O2+JzvCjQ1NRGNRnnjjTdG/O31119HVdUxE32VYFIvt7cV5513Hp7ncc0114z4m+u65Yfm1FNPxTAMfvKTnyAG9UO78cYbt3qOww47jNmzZ3PjjTeOeAgHH6tEmxm+z84Y42Dcf//9Q7y0Z599lsWLF3PGGWeUt61fv55vfOMbvOtd7yoTig899FA+/elP89Of/rRsrL70pS8Ri8W45JJLRq2AWrVqVZkGlU6nRyxlRUBpAoaET97//vfjed4QErplWdxxxx0cddRR43rgzz//fCzL4pe//CUPPfTQCK+vv79/yH0sXWPpXGPhvvvuI5fLcfnll/P+979/xM/ZZ5/NvffeO+IYJQpYCT/5yU8Ayvd9sCc91njOPPNMOjo6uOuuu8r7uK7LT37yE+LxOAsXLhxz3HPnziWVSvGf//ynvK29vZ377rtvxL6xWGzEc6ppGu973/u49957R109dHd3j3nuHQ1N0zjttNP405/+NCRs1NnZyaJFizj++ONHMCW2CRWleXYSxiKTD8dFF10kYrHYqH+79NJLBSDOOOMMccMNN4ibbrpJXHnllWLq1KninnvuKe931VVXCUCceeaZ4qabbhIf/ehHxdSpU0VjY+MWs9tCyEy0YRhi5syZ4lvf+pa47bbbxGc/+1lx2mmnlfcpZVc/+MEPit/85jfid7/73Q4b42goZbcXLFggZs2aJb7//e+Lb3/726K+vl40NDQMIYO/+93vFrFYTLz55ptDjpFOp8W0adPEoYceWs4M/ulPfxLhcFjU1dWJK6+8Uvz0pz8VN998s/iv//ovYZqm+PjHP16+b62treKzn/2suPnmm8UPf/hDcdxxxwmgvM9gnHvuuULXdfHFL35R3HbbbeLYY48Vuq6Lxx9/fIvXORjz5s0TiURCAOKFF14Y8rcbbrhB7LPPPuJLX/qSuO2228QPf/hDsd9++4mamhqxevXqMY/5jne8QzQ0NIyZGf3zn/8sAHHvvfcKITZnjxcsWCDe+c53iptvvln893//twDEhRdeWP7clVdeKd7ylreIr3/96+KnP/2p+O53vyumTZsmpk+fLgYGBoQQQuTzeXHAAQcI0zTF5z//efGTn/xELFy4UABDSPtCjMxu9/T0iFgsJubMmSNuvPFG8b3vfU+0tbWJww47bER2+8wzzxSxWExcf/314ne/+5145plnhBBCdHR0iJkzZ4poNCquvPJKcdttt4lrr71WnHvuuaKurm4r34bMbh966KHi17/+9Yif++67r7zfluYzIC6//PIR25cuXSpisZiYNm2a+O53vyu+//3vizlz5ohQKFQevxDjtymjnrviT+wETISRFEKI22+/Xbz1rW8VkUhEJBIJsWDBAvGlL31piGHwPE9cffXVYsqUKSISiYgTTzxRLF26dEQ1y2hGUgghnnzySfH2t79dJBIJEYvFxMEHHyx+8pOflP/uuq741Kc+JZqamoSiKCMezIkc42goGcnrrrtOXH/99aKtrU2EQiHxtre9rUxDEWIzNeaHP/zhqMf5wx/+IADxox/9qLxt+fLl4mMf+5iYNWuWME1TJBIJcdxxx4mf/OQnolgsCiFkdc65554rZs2aJcLhsIhGo+Ktb32ruPXWW4dQmkooFAriC1/4gmhtbRWhUEgcccQR4qGHHtriNQ7H1772NQGIefPmjfjbiy++KC644AIxY8YMEQqFRHNzszj77LPF888/P+bxOjs7ha7r4oMf/OCY++TzeRGNRsV73vMeIcRmI/nqq6+K97///SKRSIi6ujpxxRVXDKkU+cc//iHe/e53i6lTpwrTNMXUqVPFBRdcIJYvXz5iDB/5yEdEY2OjME1TLFiwQNxxxx0jxjHcSAohaUrz588XpmmK/fbbT/zmN78ZlQL0+uuvixNOOEFEIpER9LLOzk5x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u99N9lCmLH/2Z39Gc3MzP/zhD6v3feELXyCRSPBv//ZvkziyqYOmafz85z+vmu4JIWhtbeVrX/sa//iP/whAJpOhubmZJ554gosuumgSRzt1UDPJI4Bnn32WFStW8Jd/+ZfMmDGDU089lX/913+d7GFNKVavXs26devYtGkTAO+88w6///3v+exnPzvJI5u6fPzxx3R0dHD22WdX76urq2PlypW88sorkziyqcUxJ3BxJPLRRx/x8MMPc91113HjjTfy+uuv8w//8A/Yts3ll18+2cObElx//fVks1mWLFmCYRgEQcAtt9zCJZdcMtlDm7J0dHQA0NzcPOj+5ubm6u8UKkgeEYRhyIoVK7j11lsBOPXUU3nvvfd45JFHVJCM+OlPf8qTTz7JU089xYknnsjbb7/NNddcQ2trqzpHioNCLbePAGbOnMnSpUsH3XfCCSewffv2SRrR1OPrX/86119/PRdddBEnn3wyX/ziF7n22murfu2K4bS0tADQ2dk56P7Ozs7q7xQqSB4RnHnmmXzwwQeD7tu0aRNz586dpBFNPYrF4jCBVsMwCMNwkkY09Zk/fz4tLS2sW7euel82m+XVV19l1apVkziyqYVabh8BXHvttaxevZpbb72Vv/qrv+K1117jscce47HHHpvsoU0ZPv/5z3PLLbcwZ84cTjzxRN566y3uuece/vZv/3ayhzap5PN5Pvzww+rtjz/+mLfffpvGxkbmzJnDNddcw3e/+10WLVrE/Pnzuemmm2htba1mwBWAUBwRPPfcc+Kkk04SsVhMLFmyRDz22GOTPaQpRTabFV/96lfFnDlzRDweF8cdd5z4p3/6J+E4zmQPbVL59a9/LYBhP5dffrkQQogwDMVNN90kmpubRSwWE2eddZb44IMPJnfQUwxVJ6lQKBSjoPYkFQqFYhRUkFQoFIpRUEFSoVAoRkEFSYVCoRgFFSQVCoViFFSQVCgUilFQQVKhUChGQQVJhUKhGAUVJBUKhWIUVJBUTBkKhQILFizguuuuG3T/1q1bqa2trQoNCyF47LHHWLlyJel0mvr6elasWMF9991HsVgE4IknnkDTtEE/8Xh80PMK5e+iGAMqSCqmDKlUih/96Ec88MADVasKIQRXXHEFZ555JldddRUAX/ziF7nmmms4//zz+fWvf83bb7/NTTfdxDPPPMOLL75Yfb7a2lra29urP9u2bRv0esrfRTEmJrVzXHHMsWbNGnH11VeLq6++WtTW1opp06aJb37zmyIMw+ox1157rViwYIHI5/Pi3nvvFfX19WLnzp1CCCHWrl0rAPH0008Pe+4wDEVfX58QQogf/ehHoq6ubp/jCMNQtLS0iLvuuqt6X19fn4jFYuLf//3fJ+ivVRwNqJmk4rDz4x//GNM0ee2117j//vu55557+MEPflD9/S233IJpmlx66aXceOONPPDAA8yaNQuAJ598ksWLF3P++ecPe15N06irq6vezufzzJ07l7a2Ns4//3w2bNhQ/Z3yd1GMFaUnqTjstLW1ce+996JpGosXL2b9+vXce++91eV0IpHg/vvv59xzz+Wzn/0sl156afWxmzdvZvHixft9jcWLF/P4449zyimnkMlkuPvuu1m9ejUbNmxg9uzZyt9FMWbUTFJx2DnjjDPQNK16e9WqVWzevJkgCKr3/fCHPySZTLJ+/XoymUz1fjFGZb9Vq1Zx2WWXsXz5ctasWcN//ud/Mn36dB599NGJ+0MUxwQqSCqmHGvXruX555/n5Zdfpqamhmuvvbb6u+OPP56NGzeO+zkty+LUU0+tqnQrfxfFWFFBUnHYefXVVwfd/sMf/sCiRYswDIPOzk6uvvpqvvvd77Js2TKeeOIJfvKTn/DLX/4SgIsvvphNmzbxzDPPDHteIcSgWedAgiBg/fr1zJw5E1D+LopxMNmZI8WxxZo1a0Q6nRbXXnut2Lhxo3jqqadEKpUSjzzyiBBCiD//8z8Xn/rUp0QQBNXHXH/99WL27Nmir69PhGEoLrzwQpFIJMQtt9wiXn/9dbF161bx3HPPiT/5kz8RP//5z4UQQnz7298WL7zwgtiyZYt44403xEUXXSTi8bjYsGFD9Xlvv/12UV9fL5555hnx7rvvivPPP1/Mnz9flEqlw3pOFFMbFSQVh5U1a9aIv//7vxdf+tKXRG1trWhoaBA33nijCMNQ/PjHPxbJZFJs3rx50GMcxxEnnXSSuOKKK4QQQgRBIB5++GHxyU9+UiSTSVFbWytOO+00cf/994tisSiEEOKaa64Rc+bMEbZti+bmZnHeeeeJN998c9DzKn8XxVhQHjeKw8qnP/1pli9fzn333TfZQ1EoxoTak1QoFIpRUEFSoVAoRkEttxUKhWIU1ExSoVAoRkEFSYVCoRgFFSQVCoViFFSQVCgUilFQQVKhUChGQQVJhUKhGAUVJBUKhWIUVJBUKBSKUfj/ASChX1HxJPjuAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# Get predictions and errors\n", + "y_pred_test = binary_fp_pipe.predict(X_test)\n", + "abs_errors = np.abs(y_test - y_pred_test)\n", + "\n", + "\n", + "fig = plt.figure(figsize=(3,3))\n", + "\n", + "plt.scatter(y_test, abs_errors, alpha=0.5)\n", + "plt.xlabel('pXC50')\n", + "plt.ylabel('Predicted Absolute Error')\n", + "plt.title('Predicted pXC50 vs Absolute Error')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9d2860b4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n" + ] + } + ], + "source": [ + "\n", + "# Create and fit k-NN AD estimator. Distance metrics follow the scikit-learn API, and the custom distance metric tanimoto popular in cheminformatics is available in scikit-mol.\n", + "knn_ad = KNNApplicabilityDomain(n_neighbors=3, distance_metric='tanimoto', n_jobs=-1)\n", + "knn_ad.fit(binary_fp_pipe.named_steps['fp'].transform(X_train))\n", + "\n", + "# Fit threshold using validation set\n", + "knn_ad.fit_threshold(binary_fp_pipe.named_steps['fp'].transform(X_val), target_percentile=95)\n", + "\n", + "# Get AD scores for test set\n", + "knn_scores = knn_ad.transform(binary_fp_pipe.named_steps['fp'].transform(X_test))" + ] + }, + { + "cell_type": "markdown", + "id": "22848529", + "metadata": {}, + "source": [ + "Let's visualize the relationship between prediction errors and AD scores, and calculate some statistics on compound errors within and outside the domain." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e8e2bb86", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX0AAAE8CAYAAAAsfWGYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAADb4klEQVR4nOydd5xcVfn/37dNn+01yWZTgQBJ6EhNgAACCkFBRL4/qoIIAoKoqF/B9sUCAoJfQOELFkAFxYpIaEF6D6RBejab7W36zC3n98edmexsndmdTTbkvl+vgeydW87cufOcc57zeZ5HEkIIHBwcHBz2CORd3QAHBwcHh52HY/QdHBwc9iAco+/g4OCwB+EYfQcHB4c9CMfoOzg4OOxBOEbfwcHBYQ/CMfoODg4OexCO0XdwcHDYg3CMvoODg8MehGP0JykzZszgwgsvzP79/PPPI0kSzz//fNGuIUkSN910U9HO5zCxZJ6Bxx57bFc3xWE3xjH6Q/Dggw8iSVL25fF42Guvvbjyyitpa2vb1c0riCeeeGLSGfabbrop5/4OfLW2tu7qJn4kydz32tpaYrHYoPdnzJjBJz7xiZxtme/k1ltvHbR/5nfy5ptv5t2GJ554AkmSmDJlCpZlDbnPjBkzsteVZZmysjLmz5/PpZdeymuvvZb3tSzL4je/+Q2HH344FRUVBINB9tprL84//3xeffXVvM/zUUPd1Q2YzHzve99j5syZJBIJXnzxRe6++26eeOIJVq5cic/n26ltOfbYY4nH47hcroKOe+KJJ/jFL34xpOGPx+Oo6q57BO6++24CgcCg7WVlZTu/MXsQ7e3t3H333Vx33XV5H/PTn/6Uyy+/fNzP/UMPPcSMGTPYvHkzzz77LEuWLBlyvwMOOCDbvnA4zJo1a3j00Uf51a9+xVe+8hV+9rOfjXqtq666il/84hecccYZnHfeeaiqygcffMC//vUvZs2axcc+9rFxfZbdFuEwiAceeEAA4o033sjZfu211wpAPPzww8MeG4lEitKGxsZGccEFF4z7PFdccYWYbF/zjTfeKADR0dFR8LHxeFyYpjnke+O996Zping8Pq5zTCTPPfecAMSjjz46puMz9/2AAw4QtbW1IhaL5bzf2NgoTjvttJxtmf0Bceutt+a8N9zvZDgikYjw+/3i5z//uTjwwAPFhRdeOOR+Q7VDCCFisZhYunSpAMT//u//jnit1tZWIUmS+MIXvjDoPcuyRFtbW15tLga6rotkMrnTrjcajnunAI4//ngANm3aBMCFF15IIBBgw4YNnHrqqQSDQc477zzAnlrefvvt7Lfffng8Hmpra7nsssvo6enJOacQgh/84AdMmzYNn8/Hcccdx6pVqwZdezif/muvvcapp55KeXk5fr+fBQsWcMcdd2Tb94tf/AIgx32SYSif/jvvvMMpp5xCSUkJgUCAE044YdBUODOtf+mll7j22muprq7G7/dz5pln0tHRUeBdHZ7MZ/7973/Pt7/9baZOnYrP5yMUCo1476PRKNdddx0NDQ243W723ntvbrnlFsSAhLKSJHHllVfy0EMPsd9+++F2u3nyyScB+P3vf8/BBx9MMBikpKSE+fPnZ+/rUOi6TkVFBRdddNGg90KhEB6Ph69+9avZbXfeeSf77bcfPp+P8vJyDjnkEB5++OGC71EymeQTn/gEpaWlvPzyy3kd853vfIe2tjbuvvvuvPY/6qijOP744/nJT35CPB4vuI0ZHn/8ceLxOGeffTaf/exn+fOf/0wikcj7eK/Xy29/+1sqKir44Q9/OOj77M+mTZsQQnDUUUcNek+SJGpqanK29fb28pWvfIUZM2bgdruZNm0a559/Pp2dndl92tvbueSSS6itrcXj8bBw4UJ+/etf55xn8+bNSJLELbfcwu23387s2bNxu92sXr0agLVr13LWWWdRUVGBx+PhkEMO4W9/+1vOOXRd57vf/S5z587F4/FQWVnJ0UcfzbJly/K+VyPhuHcKYMOGDQBUVlZmtxmGwcknn8zRRx/NLbfckp3+XnbZZTz44INcdNFFXHXVVWzatIm77rqLd955h5deeglN0wD7B/iDH/yAU089lVNPPZW3336bk046iVQqNWp7li1bxic+8Qnq6+u5+uqrqaurY82aNfzjH//g6quv5rLLLmP79u0sW7aM3/72t6Oeb9WqVRxzzDGUlJTwta99DU3TuPfee1m8eDHLly/n8MMPz9n/y1/+MuXl5dx4441s3ryZ22+/nSuvvJI//OEPed3P7u7uQdtUVR3k3vn+97+Py+Xiq1/9KslkMuviGureCyE4/fTTee6557jkkks44IAD+Pe//831119Pc3Mzt912W865n332Wf74xz9y5ZVXUlVVxYwZM1i2bBnnnnsuJ5xwAj/+8Y8BWLNmDS+99BJXX331kJ9F0zTOPPNM/vznP3PvvffmuOH+8pe/kEwm+exnPwvAr371K6666irOOussrr76ahKJBO+99x6vvfYan/vc5/K6d2C758444wzefPNNnn76aQ499NC8jjvmmGOyRvzyyy/H6/WOesxNN93Esccey9133821116bdxv789BDD3HcccdRV1fHZz/7Wb7xjW/w97//nbPPPjvvcwQCAc4880zuv/9+Vq9ezX777Tfkfo2NjQA8+uijnH322SO6pSKRCMcccwxr1qzh4osv5qCDDqKzs5O//e1vbNu2jaqqKuLxOIsXL2b9+vVceeWVzJw5k0cffZQLL7yQ3t7eQc/FAw88QCKR4NJLL8XtdlNRUcGqVas46qijmDp1Kt/4xjfw+/388Y9/ZOnSpfzpT3/izDPPBOx7ffPNN/P5z3+eww47jFAoxJtvvsnbb7/NiSeemPe9GpZdOs+YpGSmrU8//bTo6OgQTU1N4ve//72orKwUXq9XbNu2TQghxAUXXCAA8Y1vfCPn+P/85z8CEA899FDO9ieffDJne3t7u3C5XOK0004TlmVl9/vmN78pgBz3TmZq/9xzzwkhhDAMQ8ycOVM0NjaKnp6enOv0P9dI7h1A3Hjjjdm/ly5dKlwul9iwYUN22/bt20UwGBTHHnvsoPuzZMmSnGt95StfEYqiiN7e3iGvlyHjZhjqtffeew/6zLNmzRrkihju3v/lL38RgPjBD36Qs/2ss84SkiSJ9evX53x+WZbFqlWrcva9+uqrRUlJiTAMY8TPMZB///vfAhB///vfc7afeuqpYtasWdm/zzjjDLHffvsVdG4hct074XBYLFq0SFRVVYl33nknr+P7u9WWL18uAPGzn/0s+/5w7p0rrrhCCCHEcccdJ+rq6rLfRSHunba2NqGqqvjVr36V3XbkkUeKM844Y9C+w7l3Mtx2220CEH/9619HvOb5558vAFFeXi7OPPNMccstt4g1a9YM2u873/mOAMSf//znQe9lnu/bb79dAOJ3v/td9r1UKiWOOOIIEQgERCgUEkIIsWnTJgGIkpIS0d7ennOuE044QcyfP18kEomc8x955JFi7ty52W0LFy4c8fOPF8e9MwJLliyhurqahoYGPvvZzxIIBHj88ceZOnVqzn6XX355zt+PPvoopaWlnHjiiXR2dmZfBx98MIFAgOeeew6Ap59+mlQqxZe//OUct8s111wzatveeecdNm3axDXXXDNoZNz/XPlimiZPPfUUS5cuZdasWdnt9fX1fO5zn+PFF18kFArlHHPppZfmXOuYY47BNE22bNmS1zX/9Kc/sWzZspzXAw88MGi/Cy64YNjR6MB7/8QTT6AoCldddVXO9uuuuw4hBP/6179yti9atIh99903Z1tZWRnRaLTg6fTxxx9PVVVVzkynp6eHZcuWcc455+Scf9u2bbzxxhsFnT9DX18fJ510EmvXruX555/ngAMOKPgcxx57LMcdd1xBLpubbrqJ1tZW7rnnnoKv9/vf/x5Zlvn0pz+d3Xbuuefyr3/9a5DLczQyi//hcHjE/R544AHuuusuZs6cyeOPP85Xv/pV5s2bxwknnEBzc3N2vz/96U8sXLgwO9LuT+b5fuKJJ6irq+Pcc8/NvqdpGldddRWRSITly5fnHPfpT3+a6urq7N/d3d08++yzfOYznyEcDmdtQldXFyeffDLr1q3LtqmsrIxVq1axbt26gu5LvjhGfwR+8YtfsGzZMp577jlWr17Nxo0bOfnkk3P2UVWVadOm5Wxbt24dfX191NTUUF1dnfOKRCK0t7cDZI3j3Llzc46vrq6mvLx8xLZlXE3777//uD5jho6ODmKxGHvvvfeg9+bNm4dlWTQ1NeVsnz59es7fmTbn+yM+9thjWbJkSc7riCOOGLTfzJkzhzx+qHu/ZcsWpkyZQjAYHPQZMu+Pdu4vfelL7LXXXpxyyilMmzaNiy++OOvrHwlVVfn0pz/NX//6V5LJJAB//vOf0XU9x+h//etfJxAIcNhhhzF37lyuuOIKXnrppVHPn+Gaa67hjTfe4Omnnx7k3kilUrS2tua8TNMc8jyFGvGxdBQZfve733HYYYfR1dXF+vXrWb9+PQceeCCpVIpHH320oHNFIhGAQd/xQGRZ5oorruCtt96is7OTv/71r5xyyik8++yzWVcb2L+l0X5HW7ZsYe7cuchyrsnM97lav349Qgj++7//e5BNuPHGGwGyduF73/sevb297LXXXsyfP5/rr7+e9957b8T2FYJj9EfgsMMOY8mSJSxevJh58+YN+sIB3G73oO2WZVFTUzNoFJt5fe9739tZH2FCURRlyO2iyBU4hxvlD3Xvi3Humpoa3n33Xf72t79l1wdOOeUULrjgglHP99nPfpZwOJydUfzxj39kn332YeHChdl95s2bxwcffMDvf/97jj76aP70pz9x9NFHZ3/8o3HGGWcghOBHP/rRIK37yy+/TH19fc5rYGed4dhjj2Xx4sUFGfEbb7yR1tZW7r333rz2B3sQ9MYbb/Diiy8yd+7c7Ovoo48GbF9/IaxcuRKAOXPm5H1MZWUlp59+Ok888QSLFi3ixRdfzHtGOhYGPleZ7+mrX/3qsHYh83mOPfZYNmzYwP/93/+x//77c99993HQQQdx3333FaVtzkLuBDB79myefvppjjrqqBEXyTKLTevWrctxqXR0dIw6Wp49ezZg/wCG0zpD/q6e6upqfD4fH3zwwaD31q5diyzLNDQ05HWuXUljYyNPP/004XA4ZyS4du3a7Pv54HK5+OQnP8knP/lJLMviS1/6Evfeey///d//PaKxOfbYY6mvr+cPf/gDRx99NM8++yzf+ta3Bu3n9/s555xzOOecc0ilUnzqU5/ihz/8ITfccAMej2fEti1dupSTTjqJCy+8kGAwmKPCWbhw4SC3VF1d3bDnuummm1i8eHHeRnzRokUsXryYH//4x3znO9/J65iHHnoITdP47W9/O2ig8OKLL/Lzn/+crVu3Dpo5DkUkEuHxxx+noaEhO8oulEMOOYTly5fT0tJCY2Mjs2fPznYkw9HY2Mh7772HZVk5A418n6vM71vTtBF/rxkySrCLLrqISCTCsccey0033cTnP//5UY8dDWekPwF85jOfwTRNvv/97w96zzAMent7AXvNQNM07rzzzpzR8e233z7qNQ466CBmzpzJ7bffnj1fhv7n8vv9AIP2GYiiKJx00kn89a9/ZfPmzdntbW1tPPzwwxx99NGUlJSM2q5dzamnnoppmtx1110522+77TYkSeKUU04Z9RxdXV05f8uyzIIFCwCybpvhkGWZs846i7///e/89re/xTCMHNfOUOd3uVzsu+++CCHQdX3U9gGcf/75/PznP+eee+7h61//enZ7eXn5IJfZSJ1IfyOer3wy4xb65S9/mdf+Dz30EMcccwznnHMOZ511Vs7r+uuvB+CRRx4Z9TzxeJz/9//+H93d3XzrW98acUDT2tqalUn2J5VK8cwzzyDLcrbz/vSnP82KFSt4/PHHB+2f+S2deuqptLa25qzXGIbBnXfeSSAQYNGiRSO2vaamJtu5trS0DHq/v9R54PMRCASYM2fOqM9evjgj/Qlg0aJFXHbZZdx88828++67nHTSSWiaxrp163j00Ue54447OOuss6iuruarX/0qN998M5/4xCc49dRTeeedd/jXv/5FVVXViNeQZZm7776bT37ykxxwwAFcdNFF1NfXs3btWlatWsW///1vAA4++GDAjk48+eSTURQlx5/Znx/84AcsW7aMo48+mi996Uuoqsq9995LMpnkJz/5SXFvEvDYY48NGZF74oknUltbO6ZzfvKTn+S4447jW9/6Fps3b2bhwoU89dRT/PWvf+Waa67JzpBG4vOf/zzd3d0cf/zxTJs2jS1btnDnnXdywAEH5DW6POecc7jzzju58cYbmT9//qBjTjrpJOrq6jjqqKOora1lzZo13HXXXZx22mmj+qn7c+WVVxIKhfjWt75FaWkp3/zmN/M+tj833ngjxx13XN77L1q0iEWLFg1avByK1157LStzHIqpU6dy0EEH8dBDD+V0Xs3Nzfzud78D7NH96tWrefTRR2ltbeW6667jsssuG/G627Zt47DDDuP444/nhBNOoK6ujvb2dh555BFWrFjBNddck/2NXX/99Tz22GOcffbZXHzxxRx88MF0d3fzt7/9jXvuuYeFCxdy6aWXcu+993LhhRfy1ltvMWPGDB577DFeeuklbr/99ry+t1/84hccffTRzJ8/ny984QvMmjWLtrY2XnnlFbZt28aKFSsA2HfffVm8eDEHH3wwFRUVvPnmmzz22GPD3sOCmTBd0G5MvlK0Cy64QPj9/mHf/+UvfykOPvhg4fV6RTAYFPPnzxdf+9rXxPbt27P7mKYpvvvd74r6+nrh9XrF4sWLxcqVKwdF5A6UbGZ48cUXxYknniiCwaDw+/1iwYIF4s4778y+bxiG+PKXvyyqq6uFJEk58k0GSDaFEOLtt98WJ598sggEAsLn84njjjtOvPzyy3ndn+HaOJCRJJv9jx8pAnWkex8Oh8VXvvIVMWXKFKFpmpg7d6746U9/miMvzXz+jByxP4899pg46aSTRE1NjXC5XGL69OnisssuEy0tLSN+rgyWZYmGhoYhpaNCCHHvvfeKY489VlRWVgq32y1mz54trr/+etHX1zfieYe7H1/72tcEIO66664Rjx8pEnrRokUCGFGy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+OGH4qWXXhIHHniguPDCC4UQQrzxxhtCURTx8MMPi82bN4u3335b3HHHHTntKSkpETfddJP48MMPxa9//WshSZJ46qmnhBBCmKYpDjjgAHH00UeLN998U7z66qvi4IMPFov6fRc33nijWLhwYfbvH//4x6K8vFz86U9/EqtXrxaXXHKJCAaD4owzzhjynoz07BRi1yaN0TcMQzzyyCPC5XKJVatWDblPQ0ODuO2223K2fec73xELFiwY9ryJREL09fVlX01NTY7Rd9hl7O5G/8gjjxS33367EEIIXddFVVWVeO6557KHjGT016xZIwDxhz/8YdRLj2T0GxsbhZFujxBCnH322eKcc87J/t3Y2CiWLl2ac9wll1wiLr300pxt//nPf4QsyyIej4s//elPoqSkRIRCoWHbc/TRR+dsO/TQQ8XXv/51IYQQTz31lFAURWzdujX7/qpVqwQgXn/9dSHEYKNfX18vfvKTn2T/1nVdTJs2bcKN/i4XM7///vscccQRJBIJAoEAjz/+OPvuu++Q+7a2tlJbW5uzrba2ltbW1mHPf/PNN/Pd7363qG12cJgQIpHh3xsYgdnePvy+AwOpirQA+sEHH/D666/z+OOPA6CqKueccw73338/ixcvHvV4kXZdjDfIaL/99stx9dTX1/P+++/n7HPIIYfk/L1ixQree+89HnrooZz2ZNJinHjiiTQ2NjJr1iw+/vGP8/GPf5wzzzwzJ2p6wYIFOeesr6+nPf09rFmzhoaGhpx1h3333ZeysjLWrFnDoYcemnNsX18fLS0tHH744dltqqpyyCGHTLiLZ5cb/b333pt3332Xvr4+HnvsMS644AKWL18+rOEvlBtuuIFrr702+3emrJjDxOCkOx4HhRQpmah9R+D+++/HMAymTJmS3SaEwO12c9ddd1FaWjri8WvWrAFg5syZ42qHpmk5f0uShDVAUuof8JkjkQiXXXYZV1111aDzTZ8+HZfLxdtvv83zzz/PU089xXe+8x1uuukm3njjDcrKyvK+7u7ALjf6LpeLOXPmAHDwwQfzxhtvcMcdd3DvvfcO2reurm7QwkhbWxt1dXXDnt/tduN2u4vbaIch2d3THTsMQJZh3jwADMviN7/5DbfeeisnnXRSzm5Lly7lkUce4Ytf/OKwp7Isi5///OfMnDmTAw88cEKbPRQHHXQQq1ev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Xh751KDa61HGxo0bxfTp08Xpp58u/vZv/3bvDHYvoNrrYVmWOOGEE8Q555wjnnvuObFx40bx7LPPitdee20vj3xqUO31eOihh0QkEhEPPfSQ2Lhxo/jv//5vMW3aNHHVVVdN+VjDoB/gpJNOEpdffnnld8/zRFtbm7jpppvG3f/8888XH/rQh0ZsW7ZsmfiHf/iHKR3n3kK112M0XNcVqVRKPPjgg1M1xL2KXbkeruuKU089Vdxzzz3i4osvPqiCfrXX44c//KGYO3eusG17bw1xr6La63H55ZeL97///SO2XX311eK0006b0nEKIUSY3gFs22bFihUsX768sk1VVZYvX86LL7447jEvvvjiiP0BzjrrrAn3P5CwK9djNAqFAo7jUF9fP1XD3GvY1evxjW98g+bmZj772c/ujWHuNezK9fiv//ovTjnlFC6//HJaWlpYunQpN954I95BINe8K9fj1FNPZcWKFZUU0IYNG/jtb387aa/v3cEhJ7g2Hnp6evA8j5aWlhHbW1paWL169bjHdHR0jLt/R0fHlI1zb2FXrsdo/Ou//ittbW1jXowHInblejz33HPce++9vPbaa3thhHsXu3I9NmzYwB//+EcuvPBCfvvb37Ju3Touu+wyHMfhuuuu2xvDnjLsyvW44IIL6Onp4T3veQ9CCFzX5R//8R/5t3/7tykfbzjTD7HHcfPNN/PII4/w+OOPE41G9/Vw9jqy2SwXXXQRP/rRj2hsbNzXw9kv4Ps+zc3N/Od//ifHH388H//4x/nyl7/M3Xffva+Htk/w7LPPcuONN/KDH/yAV155hV/84hf85je/4frrr5/yzw5n+kBjYyOaptHZ2Tlie2dnJ62treMe09raWtX+BxJ25XqUcdttt3HzzTfzhz/8gaOOOmoqh7nXUO31WL9+PZs2beLcc8+tbPN9HwBd11mzZg3z5s2b2kFPIXbl/pg2bRqGYaBpWmXbkiVL6OjowLZtTNOc0jFPJXblenz1q1/loosu4nOf+xwARx55JPl8ns9//vN8+ctf3qkm/u4gnOkDpmly/PHH8/TTT1e2+b7P008/zSmnnDLuMaeccsqI/QGeeuqpCfc/kLAr1wPg1ltv5frrr+fJJ5/khBNO2BtD3Suo9nosXryYN998k9dee63y85GPfIT3ve99vPbaawe8a9uu3B+nnXYa69atq7z8ANauXcu0adMO6IAPu3Y9CoXCmMBefiGKqZZDm/JS8QGCRx55REQiEfHAAw+It99+W3z+858XtbW1oqOjQwghxEUXXSSuueaayv7PP/+80HVd3HbbbWLVqlXiuuuuO+gom9Vcj5tvvlmYpikee+wx0d7eXvnJZrP76ivsUVR7PUbjYGPvVHs9Nm/eLFKplLjiiivEmjVrxK9//WvR3NwsvvnNb+6rr7BHUe31uO6660QqlRI/+clPxIYNG8Tvf/97MW/ePHH++edP+VjDoD8Md9xxh5g1a5YwTVOcdNJJ4s9//nPlb2eeeaa4+OKLR+z/05/+VCxcuFCYpimOOOII8Zvf/GYvj3hqUc31OOywwwQw5ue6667b+wOfIlR7fwzHwRb0haj+erzwwgti2bJlIhKJiLlz54obbrhBuK67l0c9dajmejiOI772ta+JefPmiWg0KmbOnCkuu+wy0d/fP+XjDKWVQ4QIEeIQQpjTDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQDxEiRIhDCGHQD7Ff4r3vfS9XXnnlvh5GiBAHHcKgH+KgwSWXXIKiKNx8880jtv/yl79EUZTK788++yyKonDEEUeMMfGora3lgQce2Olnbd26FdM0Wbp06bh/VxSl8pNIJFiwYAGXXHIJK1as2Om5X3/9dT7ykY/Q3NxMNBpl9uzZfPzjH6erq2unx4YIsTOEQT/EQYVoNMott9xCf3//TvfdsGEDP/7xj3fpcx544AHOP/98MpkMf/nLX8bd5/7776e9vZ2VK1dy1113kcvlWLZs2Q4/s7u7mw984APU19fz3//936xatYr777+ftrY28vn8Lo11MnAcZ8rOHWL/Qhj0QxwQ+M1vfkM6neahhx7a4X7Lly+ntbWVm266aafn/Od//meuu+46LMuqaixCCO6//34uuugiLrjgAu69995x96utraW1tZXZs2fzwQ9+kMcee4wLL7yQK664YsKX0vPPP8/g4CD33HMPxx57LHPmzOF973sf3/3ud5kzZ05lv5UrV/LhD3+YmpoaUqkUp59+OuvXrwekrO83vvENZsyYQSQS4ZhjjuHJJ5+sHLtp0yYUReHRRx/lzDPPJBqNVq7rPffcw5IlS4hGoyxevJgf/OAHVV2bEPs/wqAfYr/Hww8/zCc+8QkeeughLrzwwh3uq2kaN954I3fccQdbt27d4b5XXnklrutyxx13VDWeZ555hkKhwPLly/nkJz/JI488MulZ+FVXXUU2m+Wpp54a9++tra24rsvjjz8+oa76tm3bOOOMM4hEIvzxj39kxYoVfOYzn8F1XQC+973v8e1vf5vbbruNN954g7POOouPfOQjvPPOOyPOc8011/DFL36RVatWcdZZZ/HQQw/x7//+79xwww2sWrWKG2+8ka9+9as8+OCDVVydEPs9plzHM0SIXcCZZ54pvvjFL4o777xTpNNp8eyzz+70mOHyxSeffLL4zGc+I4QQ4vHHHxfDb/VnnnlGAKK/v1/cfffdor6+XgwMDAghhEin0+L+++/f4edccMEF4sorr6z8fvTRR485BhCPP/74mGOLxaIAxC233DLh+f/t3/5N6Lou6uvrxdlnny1uvfXWii67EEJce+21Ys6cOcK27XGPb2trEzfccMOIbSeeeKK47LLLhBBCbNy4UQDi9ttvH7HPvHnzxMMPPzxi2/XXXy9OOeWUCcca4sBDONMPsd/iscce46qrruKpp57izDPPrGz/05/+RDKZrPyMl/K55ZZbePDBB1m1atUOP+Ozn/0sDQ0N3HLLLZMa08DAAL/4xS/45Cc/Wdn2yU9+csIUz2iIYPY+vLA8GjfccAMdHR3cfffdHHHEEdx9990VNy6A1157jdNPPx3DMMYcm8lk2L59O6eddtqI7aeddtqYazHc3Syfz7N+/Xo++9nPjri23/zmNytpoxAHB0KP3BD7LY499lheeeUV7rvvPk444YRKoDzhhBN47bXXKvu1tLSMOfaMM87grLPO4tprr+WSSy6Z8DN0XeeGG27gkksu4YorrtjpmB5++GFKpRLLli2rbBNC4Ps+a9euZeHChTs8vhx4h+fnx0NDQwMf+9jH+NjHPsaNN97Isccey2233caDDz5ILBbb6Tgng0QiUfnvXC4HwI9+9KMR3w0Y4Wsb4sBHONMPsd9i3rx5PPPMM/zqV7/in//5nyvbY7EY8+fPr/ykUqlxj7/55pt54oknePHFF3f4OR/72Mc44ogj+PrXv77TMd177738y7/8ywj/29dff53TTz+d++67b6fH33777dTU1LB8+fKd7luGaZrMmzevUjc46qij+NOf/jQu46ampoa2tjaef/75Eduff/55Dj/88Ak/o6Wlhba2NjZs2DDi2s6fP3+nL6gQBxj2dX4pRIjxUM7pCyHE6tWrRWtra+X3iTCeJeFFF10kotHohDn9Mp5++mmh67rQdX3CnP6rr74qALFq1aoxf/vBD34gWltbheM4QgiZ07///vtFe3u72LRpk/j9738v/u7v/k5omiYeeuihCb/DE088IS688ELxxBNPiDVr1ojVq1eLb33rW0LTNPHjH/9YCCFET0+PaGhoEB/96EfFyy+/LNauXSt+/OMfi9WrVwshhPjud78rampqxCOPPCJWr14t/vVf/1UYhiHWrl0rhBjK6b/66qsjPvtHP/qRiMVi4nvf+55Ys2aNeOONN8R9990nvv3tb0843hAHHsKgH2K/xPCgL4QQb7/9tmhubhZXX331hMeMF/Q3btwoTNPcadAXQogPfvCDlWA9Hq644gpx+OGHj/u39vZ2oaqq+NWvfiWEECN8gqPRqJg3b564+OKLxYoVKyb+0kKI9evXi0svvVQsXLhQxGIxUVtbK0488cQxY3r99dfFBz/4QRGPx0UqlRKnn366WL9+vRBCCM/zxNe+9jUxffp0YRiGOProo8Xvfve7EddkvKAvhBAPPfSQOOaYY4RpmqKurk6cccYZ4he/+MUOxxziwELokRsiRIgQhxDCnH6IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEIIg36IECFCHEL4/wG7t9Te+2WS8AAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "95th percentile of errors inside domain: 1.45\n", + "95th percentile of errors outside domain: 1.85\n", + "Fraction of samples outside domain: 0.04\n" + ] + } + ], + "source": [ + "plt.figure(figsize=(4, 3))\n", + "plt.scatter(knn_scores, abs_errors, alpha=0.5)\n", + "plt.axvline(x=knn_ad.threshold_, color='r', linestyle='--', label='AD Threshold')\n", + "plt.xlabel('k-NN AD Score')\n", + "plt.ylabel('Absolute Prediction Error')\n", + "plt.title('Prediction Errors vs k-NN AD Scores')\n", + "plt.legend()\n", + "plt.show()\n", + "\n", + "# Calculate error statistics\n", + "in_domain = knn_ad.predict(binary_fp_pipe.named_steps['fp'].transform(X_test))\n", + "errors_in = abs_errors[in_domain == 1]\n", + "errors_out = abs_errors[in_domain == -1]\n", + "\n", + "print(f\"95th percentile of errors inside domain: {np.percentile(errors_in, 95):.2f}\")\n", + "print(f\"95th percentile of errors outside domain: {np.percentile(errors_out, 95):.2f}\")\n", + "print(f\"Fraction of samples outside domain: {(in_domain == -1).mean():.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "10e69073", + "metadata": {}, + "source": [ + "There's some diffence in the errors distribution inside and outside the domain threshold, but maybe not as clear-cut as we could have wished for. The fraction of samples outside the domain in the test-set are close the 5% that corresponds to the threshold estimated from the validation set fractile of 95%." + ] + }, + { + "cell_type": "markdown", + "id": "09bdc3b2", + "metadata": {}, + "source": [ + "## Example 2: Leverage-based AD with Count-based Morgan Fingerprints\n", + "\n", + "In this example, we'll use count-based Morgan fingerprints, reduce their dimensionality with PCA,\n", + "and apply a leverage-based applicability domain estimator." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fe4a6819", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
Pipeline(steps=[('fp', MorganFingerprintTransformer(useCounts=True)),\n",
+       "                ('pca', PCA(n_components=0.9)), ('scaler', StandardScaler()),\n",
+       "                ('leverage', LeverageApplicabilityDomain())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "Pipeline(steps=[('fp', MorganFingerprintTransformer(useCounts=True)),\n", + " ('pca', PCA(n_components=0.9)), ('scaler', StandardScaler()),\n", + " ('leverage', LeverageApplicabilityDomain())])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create pipeline for count-based fingerprints AD estimation with PCA, scaling and leverage\n", + "count_fp_pipe = Pipeline([\n", + " ('fp', MorganFingerprintTransformer(fpSize=2048, radius=2, useCounts=True)),\n", + " ('pca', PCA(n_components=0.9)), # Keep 90% of variance\n", + " ('scaler', StandardScaler()),\n", + " ('leverage', LeverageApplicabilityDomain())\n", + "])\n", + "\n", + "# Train the model\n", + "count_fp_pipe.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "57d73a11", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'Series.swapaxes' is deprecated and will be removed in a future version. Please use 'Series.transpose' instead.\n", + " return bound(*args, **kwds)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "\n", + "X_val_transformed = count_fp_pipe[:-1].transform(X_val) #Index into pipeline to get all the pipeline up to thelast step before the AD estimator\n", + "count_fp_pipe.named_steps['leverage'].fit_threshold(X_val_transformed, target_percentile=95)\n", + "\n", + "\n", + "# Get AD scores for test set\n", + "X_test_transformed = count_fp_pipe[:-1].transform(X_test) #Index into pipeline to get the last step before the AD estimator \n", + "leverage_raw_scores = count_fp_pipe.named_steps['leverage'].transform(X_test_transformed)" + ] + }, + { + "cell_type": "markdown", + "id": "fd5c6718", + "metadata": {}, + "source": [ + "As before, let's visualize the relationship between prediction errors and leverage scores and look at the fractiles errors." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "41434c9d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "95th percentile of errors inside domain: 1.50\n", + "95th percentile of errors outside domain: 1.23\n", + "Fraction of samples outside domain: 0.05\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX0AAAE8CAYAAAAsfWGYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAACU8UlEQVR4nOydd3gc1dX/P1O2a9WsZstyrxgXMM00UwwGE8CEFuD3YjqEHgIkpGFCEhOqCfACSQAneQOh19CMqaaDjcG9d1WrbN+dcn9/jHatVbNWlizJns/z7KPdO+3OaPfMnXPP+R5JCCGwsbGxsdknkHu6AzY2NjY2ew7b6NvY2NjsQ9hG38bGxmYfwjb6NjY2NvsQttG3sbGx2Yewjb6NjY3NPoRt9G1sbGz2IWyjb2NjY7MPYRt9Gxsbm30I2+j3EoYMGcJFF12U+vzhhx8iSRIffvhhlx1DkiRmz57dZfuzsbHpe9hGH5g3bx6SJKVebrebUaNGce2111JZWdnT3cuIN998s9cZ9tmzZ6dd3+avioqKnu5ir2Tjxo1IksS9997b013p85imyT//+U8OPfRQ8vPz8fv9jBo1igsvvJAvvviip7u3R1F7ugO9id///vcMHTqUWCzGwoULefTRR3nzzTdZunQpXq93j/bl6KOPJhqN4nQ6M9ruzTff5JFHHmnV8EejUVS15/7ljz76KFlZWS3ac3Nz93xnbPYprr/+eh555BFOP/10LrjgAlRVZdWqVbz11lsMGzaMww47rKe7uMewjX4TTj75ZA466CAALrvsMvr168f999/Pq6++ynnnndfqNuFwGJ/P1+V9kWUZt9vdpfvs6v1lyllnnUVBQUFG28RiMZxOJ7Lc8qF0d6+9aZokEokevy57C0IIYrEYHo+np7uSRmVlJf/7v//L5Zdfzl//+te0ZXPnzqW6unqP9UXXdUzTzHgw15XY7p12OO644wDYsGEDABdddBFZWVmsW7eOGTNm4Pf7ueCCCwDLgMydO5dx48bhdrspLi7myiuvpK6uLm2fQgj+8Ic/MHDgQLxeL8ceeyzLli1rcey2fPpffvklM2bMIC8vD5/Px4QJE3jwwQdT/XvkkUcA0twnSVrz6S9evJiTTz6Z7OxssrKyOP7441s87ibdX59++ik33XQThYWF+Hw+zjjjjC79wSTP+T//+Q+/+c1vKC0txev1EggE2r324XCYn//855SVleFyuRg9ejT33nsvzQVkJUni2muv5d///jfjxo3D5XLx9ttvA/Cf//yHyZMn4/f7yc7OZvz48anr2hqappGfn8/FF1/cYlkgEMDtdnPzzTen2h566CHGjRuH1+slLy+Pgw46iKeffrorLhvxeJzbb7+dESNG4HK5KCsr49ZbbyUej6fW2X///Tn22GNbbGuaJqWlpZx11llpbR35Lg8ZMoQf/ehHvPPOOxx00EF4PB4ef/xxAJ566imOO+44ioqKcLlc7Lfffjz66KOtHn/27NkMGDAg9XtYvnx5izkugPr6em688cbU/3nEiBH8+c9/xjTNdq/Phg0bEEJwxBFHtFgmSRJFRUUtjvOzn/2MIUOG4HK5GDhwIBdeeCE1NTWpdaqqqrj00kspLi7G7XYzceJE/vGPf6Ttp6l7bu7cuQwfPhyXy8Xy5csBWLlyJWeddRb5+fm43W4OOuggXnvttbR9aJrGHXfcwciRI3G73fTr148jjzyS+fPnt3vO7WGP9Nth3bp1APTr1y/Vpus606dP58gjj+Tee+9NuX2uvPJK5s2bx8UXX8z111/Phg0bePjhh1m8eDGffvopDocDgN/97nf84Q9/YMaMGcyYMYNFixZx4oknkkgkdtmf+fPn86Mf/Yj+/ftzww03UFJSwooVK3jjjTe44YYbuPLKK9m+fTvz58/nX//61y73t2zZMo466iiys7O59dZbcTgcPP744xxzzDF89NFHHHrooWnrX3fddeTl5XH77bezceNG5s6dy7XXXsuzzz7boetZW1vbok1V1RbunTvvvBOn08nNN99MPB5PjYpau/ZCCE477TQ++OADLr30UiZNmsQ777zDLbfcwrZt23jggQfS9v3+++/z3HPPce2111JQUMCQIUOYP38+5513Hscffzx//vOfAVixYgWffvopN9xwQ6vn4nA4OOOMM3jppZd4/PHH00Zur7zyCvF4nJ/85CcA/O1vf+P666/nrLPO4oYbbiAWi/H999/z5Zdfcv7553fo2rWFaZqcdtppLFy4kCuuuIKxY8fyww8/8MADD7B69WpeeeUVAM4991xmz55NRUUFJSUlqe0XLlzI9u3bU32Fjn+XAVatWsV5553HlVdeyeWXX87o0aMBy5U3btw4TjvtNFRV5fXXX+fqq6/GNE2uueaa1Pa33XYbd999N6eeeirTp09nyZIlTJ8+nVgslnaekUiEqVOnsm3bNq688koGDRrEZ599xm233UZ5eTlz585t8xoNHjwYgOeff56zzz67XVdtKBTiqKOOYsWKFVxyySUceOCB1NTU8Nprr7F161YKCgqIRqMcc8wxrF27lmuvvZahQ4fy/PPPc9FFF1FfX9/iO/PUU08Ri8W44oorcLlc5Ofns2zZMo444ghKS0v55S9/ic/n47nnnmPmzJm8+OKLnHHGGYA1HzZnzhwuu+wyDjnkEAKBAN988w2LFi3ihBNOaPM82kXYiKeeekoA4r333hPV1dViy5Yt4j//+Y/o16+f8Hg8YuvWrUIIIWbNmiUA8ctf/jJt+08++UQA4t///nda+9tvv53WXlVVJZxOpzjllFOEaZqp9X71q18JQMyaNSvV9sEHHwhAfPDBB0IIIXRdF0OHDhWDBw8WdXV1acdpuq9rrrlGtPVvBcTtt9+e+jxz5kzhdDrFunXrUm3bt28Xfr9fHH300S2uz7Rp09KO9bOf/UwoiiLq6+tbPV6S22+/XQCtvkaPHt3inIcNGyYikUjaPtq69q+88ooAxB/+8Ie09rPOOktIkiTWrl2bdv6yLItly5alrXvDDTeI7Oxsoet6u+fRnHfeeUcA4vXXX09rnzFjhhg2bFjq8+mnny7GjRuX0b6FEGLDhg0CEPfcc0+b6/zrX/8SsiyLTz75JK39scceE4D49NNPhRBCrFq1SgDioYceSlvv6quvFllZWanr3dHvshBCDB48WADi7bffbtGv5v8/IYSYPn162nWpqKgQqqqKmTNnpq03e/bsFr+HO++8U/h8PrF69eq0dX/5y18KRVHE5s2bWxyvKRdeeKEARF5enjjjjDPEvffeK1asWNFivd/97ncCEC+99FKLZcnv/ty5cwUg/u///i+1LJFIiClTpoisrCwRCASEEDv/f9nZ2aKqqiptX8cff7wYP368iMViafs//PDDxciRI1NtEydOFKecckq755YptnunCdOmTaOwsJCysjJ+8pOfkJWVxcsvv0xpaWnaej/96U/TPj///PPk5ORwwgknUFNTk3pNnjyZrKwsPvjgAwDee+89EokE1113XZrb5cYbb9xl3xYvXsyGDRu48cYbW4yMm+6roxiGwbvvvsvMmTMZNmxYqr1///6cf/75LFy4kEAgkLbNFVdckXaso446CsMw2LRpU4eO+eKLLzJ//vy011NPPdVivVmzZrXpF25+7d98800UReH6669Pa//5z3+OEIK33norrX3q1Knst99+aW25ubmEw+GMH5mPO+44CgoK0p506urqmD9/Pueee27a/rdu3crXX3+d0f47wvPPP8/YsWMZM2ZM2ncv6ZpMfvdGjRrFpEmT0vpqGAYvvPACp556aup6d/S7nGTo0KFMnz69Rb+a/v8aGhqoqalh6tSprF+/noaGBgAWLFiArutcffXVadted911rZ7nUUcdRV5eXlq/pk2bhmEYfPzxx+1ep6eeeoqHH36YoUOH8vLLL3PzzTczduxYjj/+eLZt25Za78UXX2TixImpkXZTkt/9N998k5KSkrR5PofDwfXXX08oFOKjjz5K2+7MM8+ksLAw9bm2tpb333+fc845h2AwmDqXHTt2MH36dNasWZPqU25uLsuWLWPNmjXtnl8m2O6dJjzyyCOMGjUKVVUpLi5m9OjRLSYQVVVl4MCBaW1r1qyhoaGhhW8wSVVVFUDKOI4cOTJteWFhIXl5ee32Lelq2n///Tt+Qu1QXV1NJBJJPY43ZezYsZimyZYtWxg3blyqfdCgQWnrJfvc3NfbFkcffXSHJnKHDh3aantr137Tpk0MGDAAv9/f4hySy3e176uvvprnnnuOk08+mdLSUk488UTOOeccTjrppHb7qaoqZ555Jk8//TTxeByXy8VLL72EpmlpRv8Xv/gF7733HocccggjRozgxBNP5Pzzz2/Vx5wpa9asYcWKFWlGpSnJ7x5YLp5f/epXbNu2jdLSUj788EOqqqrS+trR73KStv5Xn376Kbfffjuff/45kUgkbVlDQwM5OTmp/82IESPSlufn57f4PaxZs4bvv/++Q+fZGrIsc80113DNNdewY8cOPv30Ux577DHeeustfvKTn/DJJ58A1u/szDPPbHdfmzZtYuTIkS1sQ0e/c2vXrkUIwW9/+1t++9vftnk+paWl/P73v+f0009n1KhR7L///px00kn8z//8DxMmTGi3j+1hG/0mHHLIIanonbZwuVwt/tmmaVJUVMS///3vVrdp64va11AUpdV20cUVN9sa5bd27bti30VFRXz33Xe88847vPXWW7z11ls89dRTXHjhhS0m55rzk5/8hMcff5y33nqLmTNn8txzzzFmzBgmTpyYWmfs2LGsWrWKN954g7fffpsXX3yR//3f/+V3v/sdd9xxx26dj2majB8/nvvvv7/V5WVlZan35557LrfddhvPP/88N954I8899xw5OTlpN7dMv8utXc9169Zx/PHHM2bMGO6//37KyspwOp28+eabPPDAA7uceG3rPE844QRuvfXWVpePGjWqw/vq168fp512Gqeddlpq/mrTpk0p339X0/waJc//5ptvbvUpCXbeCI8++mjWrVvHq6++yrvvvsvf//53HnjgAR577DEuu+yyTvXHNvpdwPDhw3nvvfc44ogj2g1XS36p1qxZk+ZSqa6u3uVoefjw4QAsXbqUadOmtbleR109hYWFeL1eVq1a1WLZypUrkWU5zWD0VgYPHsx7771HMBhMG+2vXLkytbwjOJ1OTj31VE499VRM0+Tqq6/m8ccf57e//W2LkWhTjj76aPr378+zzz7LkUceyfvvv8+vf/3rFuv5fD7OPfdczj33XBKJBD/+8Y/54x//yG233bZbIaPDhw9nyZIlHH/88bv83w8dOpRDDjmEZ599lmuvvZaXXnqJmTNn4nK50vbXke9ye7z++uvE43Fee+21tKfD5q6h5P9m7dq1aaPhHTt2tPg9DB8+nFAo1O53vzMcdNBBfPTRR5SXlzN48GCGDx/O0qVL291m8ODBfP/995immTYI6eh3LvnbdzgcHTqfZJTYxRdfTCgU4uijj2b27NmdNvq2T78LOOecczAMgzvvvLPFMl3Xqa+vB6w5A4fDwUMPPZQ2Om4v8iDJgQceyNChQ5k7d25qf0ma7isZt958neYoisKJJ57Iq6++ysaNG1PtlZWVPP300xx55JFkZ2fvsl89zYwZMzAMg4cffjit/YEHHkCSJE4++eRd7mPHjh1pn2VZTj0+Nw17bA1ZljnrrLN4/fXX+de//oWu62nuktb273Q62W+//RBCoGnaLvvXHueccw7btm3jb3/7W4tl0WiUcDic1nbuuefyxRdf8OSTT1JTU9Oirx39LrdH8omw6feyoaGhxfzN8ccfj6qqLUI5m/8vk/36/PPPeeedd1osq6+vR9f1NvtTUVGRCpNsSiKRYMGCBciynLqxn3nmmSxZsoSXX365xfrJ85kxYwYVFRVp8yO6rvPQQw+RlZXF1KlT2+wLWE+WxxxzDI8//jjl5eUtljcNg27+3cnKymLEiBG7/F62hz3S7wKmTp3KlVdeyZw5c/juu+848cQTcTgcrFmzhueff54HH3yQs846i8LCQm6++WbmzJnDj370I2bMmMHixYt56623dunrlmWZRx99lFNPPZVJkyZx8cUX079/f1auXMmyZctSP4bJkycDVgbi9OnTURQlLRyvKX/4wx+YP38+Rx55JFdffTWqqvL4448Tj8e5++67u/YiAS+88EKrGbknnHACxcXFndrnqaeeyrHHHsuvf/1rNm7cyMSJE3n33Xd59dVXufHGG1NPSO1x2WWXUVtby3HHHcfAgQPZtGkTDz30EJMmTUr5advj3HPP5aGHHuL2229n/PjxLbY58cQTKSkp4YgjjqC4uJgVK1bw8MMPc8opp7SYi2iNBQsWtAhhBJg5cyb/8z//w3PPPcdVV13FBx98wBFHHIFhGKxcuZLnnnsuFUOf5JxzzuHmm2/m5ptvJj8/v8VIs6Pf5fY48cQTU09OV155JaFQiL/97W8UFRWlGbni4mJuuOEG7rvvPk477TROOukklixZkvo9NH1yueWWW3jttdf40Y9+xEUXXcTkyZMJh8P88MMPvPDCC2zcuLHN39DWrVs55JBDOO644zj++OMpKSmhqqqKZ555hiVLlnDjjTemtr3lllt44YUXOPvss7nkkkuYPHkytbW1vPbaazz22GNMnDiRK664gscff5yLLrqIb7/9liFDhvDCCy/w6aefMnfu3A79Tx955BGOPPJIxo8fz+WXX86wYcOorKzk888/Z+vWrSxZsgSA/fbbj2OOOYbJkyeTn5/PN998wwsvvMC11167y2O0SZfGAvVRkiGJX3/9dbvrzZo1S/h8vjaX//WvfxWTJ08WHo9H+P1+MX78eHHrrbeK7du3p9YxDEPccccdon///sLj8YhjjjlGLF26VAwePLjdkM0kCxcuFCeccILw+/3C5/OJCRMmpIXh6bourrvuOlFYWCgkSUoL36RZyKYQQixatEhMnz5dZGVlCa/XK4499ljx2Wefdej6tNXH5rQXstl0++T+nn/++Rb7aO/aB4NB8bOf/UwMGDBAOBwOMXLkSHHPPfekhZcmz/+aa65psf0LL7wgTjzxRFFUVCScTqcYNGiQuPLKK0V5eXm755XENE1RVlbWauioEEI8/vjj4uijjxb9+vUTLpdLDB8+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uX70IaLRqFi+fLmIRqMtFyavZWuvGTPS1/V621536tT0dQsKWl+vEzzxxBPioIMOEkII8frrr4vhw4cL09z5HdywYYMAxOLFi1ts+/LLLwtAfPnll7s8ztSpU8UNN9zQon3WrFkiOztbXHXVVWLFihXi9ddfF16vV/z1r39NrTN48GCRnZ0t7r33XrF27drUy+fziQceeECsXr1afPrpp+KAAw4QF110kRBCiK+//looiiKefvppsXHjRrFo0SLx4IMPpvUnOztbzJ49W6xevVr84x//EJIkiXfffVcIIYRhGGLSpEniyCOPFN9884344osvxOTJk8XUJv+L22+/XUycODH1+c9//rPIy8sTL774oli+fLm49NJLhd/vF6effnqr16S9704mdq3XGH1d18UzzzwjnE6nWLZsWavrlJWViQceeCCt7Xe/+52YMGFCm/uNxWKioaEh9dqyZUufNvq9CtvoZ0xfN/qHH364mDt3rhBCCE3TREFBgfjggw9Sy9sz+itWrBCAePbZZ3d5nPaM/uDBg4Wu66m2s88+W5x77rmpz4MHDxYzZ85M2+7SSy8VV1xxRVrbJ598ImRZFtFoVLz44osiOztbBAKBNvtz5JFHprUdfPDB4he/+IUQQoh3331XKIoiNm/enFq+bNkyAYivvvpKCNHS6Pfv31/cfffdqc+apomBAwd2u9Hv8Tj9H374gSlTphCLxcjKyuLll19mv/32a3XdiooKiouL09qKi4upqKhoc/9z5szhjjvu6NI+29h0C6FQ28uaZ2BWVbW9bvPEry6aAF21ahVfffUVL7/8MgCqqnLuuefyxBNPcMwxx+xye9HoutjdJKNx48aluXr69+/PDz/8kLbOQQcdlPZ5yZIlfP/99/z73/9O609SFuOEE05g8ODBDBs2jJNOOomTTjqJM844Iy1resKECWn77N+/P1WN/4cVK1ZQVlaWNu+w3377kZuby4oVKzj44IPTtm1oaKC8vJxDDz001aaqKgcddFC3u3h63OiPHj2a7777joaGBl544QVmzZrFRx991Kbhz5TbbruNm266KfU5WVasr2ILru3FZFKEprvWbYcnnngCXdcZMGBAqk0Igcvl4uGHHyYnJ6fd7VesWAHA0KFDd6sfDkd6AqQkSZimmdbma3bOoVCIK6+8kuuvv77F/gYNGoTT6WTRokV8+OGHvPvuu/zud79j9uzZfP311+Tm5nb4uH2BHjf6TqeTESNGADB58mS+/vprHnzwQR5//PEW65aUlLSYGKmsrKSkpKTN/btcLlwuV9d2uofodYJrbjd89dXO9zZ7Lbqu889//pP77ruPE088MW3ZzJkzeeaZZ7jqqqva3N40Tf7yl78wdOhQDjjggO7ubgsOPPBAli9fnrI1raGqKtOmTWPatGncfvvt5Obm8v777/PjH/94l/sfO3YsW7ZsYcuWLalB5fLly6mvr291AJuTk0P//v358ssvOfroowHrGn/77bcceOCBnTzLjtHjRr85pmkSj8dbXTZlyhQWLFiQliQxf/58pkyZsod613P0SsE1RYFmj602eydvvPEGdXV1XHrppS1G9GeeeSZPPPFEmtHfsWMHFRUVRCIRli5dyty5c/nqq6/473//2yWa8Jnyi1/8gsMOO4xrr72Wyy67DJ/Px/Lly5k/fz4PP/wwb7zxBuvXr+foo48mLy+PN998E9M0GT16dIf2P23aNMaPH88FF1zA3Llz0XWdq6++mqlTp7ZwNSW54YYbuOuuuxg5ciRjxozh/vvvp76+vgvPunV61OjfdtttnHzyyQwaNIhgMMjTTz/Nhx9+yDvvvAPAhRdeSGlpKXPmzAGsizR16lTuu+8+TjnlFP7zn//wzTff8Ne//rUnT6Pb6bWCazb7DE888QTTpk1r1YVz5plncvfdd/P999+n6rNOmzYNAK/Xy+DBgzn22GP561//2u5IuzuZMGECH330Eb/+9a856qijEEIwfPhwzj33XAByc3N56aWXmD17NrFYjJEjR/LMM88wroPaUpIk8eqrr3Lddddx9NFHI8syJ510Eg899FCb2/z85z+nvLycWbNmIcsyl1xyCWeccQYNDQ1dcs5t9lV096xBO1x66aUsWLCA8vJycnJymDBhAr/4xS844YQTACv1eciQIWmxwM8//zy/+c1v2LhxIyNHjuTuu+9mxowZHT5mXyyMvqU2wgPzV5PrdbQq6BaMadRHNH52wqg9K7iWSMCDD1rvb7gBnM49d+w+SiwWY8OGDQwdOhS37RKzyYD2vjuZ2LUeHek/8cQT7S7/8MMPW7SdffbZnH322d3Uo95Jd0s3dxpNg1tvtd5ffbVt9G1s+gC29k4foNcKrtnY2PQ5bKPfB+i1gms9iGkKttRG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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(4, 3))\n", + "plt.scatter(leverage_raw_scores, abs_errors, alpha=0.5)\n", + "plt.axvline(x=count_fp_pipe.named_steps['leverage'].threshold_, color='r', linestyle='--', label='AD Threshold')\n", + "plt.xlabel('Leverage AD Score')\n", + "plt.ylabel('Absolute Prediction Error')\n", + "plt.title('Prediction Errors vs Leverage Scores')\n", + "plt.legend()\n", + "\n", + "\n", + "# Calculate error statistics\n", + "in_domain = count_fp_pipe.named_steps['leverage'].predict(X_test_transformed)\n", + "errors_in = abs_errors[in_domain == 1]\n", + "errors_out = abs_errors[in_domain == -1]\n", + "\n", + "print(f\"95th percentile of errors inside domain: {np.percentile(errors_in, 95):.2f}\")\n", + "print(f\"95th percentile of errors outside domain: {np.percentile(errors_out, 95):.2f}\")\n", + "print(f\"Fraction of samples outside domain: {(in_domain == -1).mean():.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "86f8a09e", + "metadata": {}, + "source": [ + "Dissappointingly the error seems larger within the domain, than outside the domain." + ] + }, + { + "cell_type": "markdown", + "id": "e22b19f0", + "metadata": {}, + "source": [ + "## Testing Famous Drugs\n", + "\n", + "Let's test some well-known drugs to see if they fall within our model's applicability domain:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1d33100d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define famous drugs\n", + "famous_drugs = {\n", + " 'Aspirin': 'CC(=O)OC1=CC=CC=C1C(=O)O',\n", + " 'Viagra': 'CCc1nn(C)c2c(=O)[nH]c(nc12)c3cc(ccc3OCC)S(=O)(=O)N4CCN(C)CC4',\n", + " 'Heroin': 'CN1CC[C@]23[C@H]4Oc5c(O)ccc(CC1[C@H]2C=C[C@@H]4O3)c5',\n", + "}\n", + "\n", + "\n", + "Draw.MolsToGridImage([Chem.MolFromSmiles(drug) for drug in famous_drugs.values()], molsPerRow=3,\n", + " subImgSize=(250,250), legends=[f\"{name}\" for name, smiles in famous_drugs.items()])" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "904ed0d0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/metrics/pairwise.py:2466: DataConversionWarning: Data was converted to boolean for metric jaccard\n", + " warnings.warn(msg, DataConversionWarning)\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n", + "/home/esben/envs/vscode/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Predicted pIC50k-NN Scorek-NN StatusLeverage ScoreLeverage Status
Drug
Aspirin5.900.719194Outside0.020058Inside
Viagra9.050.786921Outside0.050743Inside
Heroin6.450.812649Outside0.021588Inside
\n", + "
" + ], + "text/plain": [ + " Predicted pIC50 k-NN Score k-NN Status Leverage Score \\\n", + "Drug \n", + "Aspirin 5.90 0.719194 Outside 0.020058 \n", + "Viagra 9.05 0.786921 Outside 0.050743 \n", + "Heroin 6.45 0.812649 Outside 0.021588 \n", + "\n", + " Leverage Status \n", + "Drug \n", + "Aspirin Inside \n", + "Viagra Inside \n", + "Heroin Inside " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "leverage_ad = count_fp_pipe.named_steps['leverage']\n", + "\n", + "# Function to process a drug through both AD pipelines\n", + "def check_drug_applicability(smiles, name):\n", + " mol = Chem.MolFromSmiles(smiles)\n", + " \n", + " # k-NN AD\n", + " fp_binary = binary_fp_pipe.named_steps['fp'].transform([mol])\n", + " knn_score = knn_ad.transform(fp_binary)[0][0]\n", + " knn_status = \"Inside\" if knn_ad.predict(fp_binary)[0] == 1 else \"Outside\"\n", + " \n", + " # Leverage AD\n", + " fp_count = count_fp_pipe.named_steps['fp'].transform([mol])\n", + " fp_pca = count_fp_pipe.named_steps['pca'].transform(fp_count)\n", + " fp_scaled = count_fp_pipe.named_steps['scaler'].transform(fp_pca)\n", + " leverage_score = leverage_ad.transform(fp_scaled)[0][0]\n", + " leverage_status = \"Inside\" if leverage_ad.predict(fp_scaled)[0] == 1 else \"Outside\"\n", + " \n", + " # Get prediction\n", + " pred_pIC50 = binary_fp_pipe.predict([mol])[0]\n", + " \n", + " return {\n", + " 'knn_score': knn_score,\n", + " 'knn_status': knn_status,\n", + " 'leverage_score': leverage_score,\n", + " 'leverage_status': leverage_status,\n", + " 'pred_pIC50': pred_pIC50\n", + " }\n", + "\n", + "# Process each drug\n", + "results = []\n", + "for name, smiles in famous_drugs.items():\n", + " result = check_drug_applicability(smiles, name)\n", + " results.append({\n", + " 'Drug': name,\n", + " 'Predicted pIC50': f\"{result['pred_pIC50']:.2f}\",\n", + " 'k-NN Score': result['knn_score'],\n", + " 'k-NN Status': result['knn_status'],\n", + " 'Leverage Score': result['leverage_score'],\n", + " 'Leverage Status': result['leverage_status']\n", + " })\n", + "\n", + "# Display results\n", + "pd.DataFrame(results).set_index('Drug')" + ] + }, + { + "cell_type": "markdown", + "id": "a5241345", + "metadata": {}, + "source": [ + "Let's visualize where these drugs fall in our AD plots:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "3aaf4485", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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7sDcuXOxYcMGHHXUUTFJpylTpuCll17ChRdeiPvuuw8zZ85EbW1tUvW1iIio7ezevRt/+MMf8O677+Kqq67CkiVL0K9fP1x88cVYvHgxAKBr164YO3Ysnn322QbPf+aZZyDLMs466ywAgM/nw9ixY/H4449j2rRpuOeeezB69GjMmTMH1157bYPnP/LII/jXv/6Fyy67DIsWLUJ+fj5qamrw3//+F+PGjcMdd9yBefPmYe/evZgwYQLWrVsXfa6u65g0aRKeeuopTJ8+Hf/4xz9QVlYWd0Tz999/jz/84Q/YuHEjbrjhBixatAh2ux2TJ0/GSy+9lNBn9dRTT8Fut+OUU07B4Ycfjr59+yZ9ogoA/vjHP0IIkdDxwMqVK/Hrr782+5oXX3xxNGF0xx134IYbboDFYok5CXXJJZfgpptuwvDhw/HPf/4TY8eOxYIFC3DOOec0eL0ff/wR5557Lo4//ngsWbIEhxxySMJ/2xUrVuDcc89FXl4e7rjjDtx+++0YN24cPvrooyQ+JSIigiCilLr55psFALF3716xceNG0a1bNzFy5EhRWVmZ0PPHjh0rDjroICGEELfccosAIL788kshhBBbtmwRAMRdd90V3f79998XAMRzzz0nqqurRV5enjj11FOjj0+fPl3Y7faE9v38888LAGLTpk1CCCFqamqExWIR//znP2O2i9eO+k477TQBQLjd7ka38fl8YsCAAQKAKC0tFTNmzBBLly4Vu3fvbrDt0UcfLZxOp9i2bVvM/bquR69PnjxZmEwmsXnz5uh9u3btEk6nUxx99NHR+x555BEBQBx11FFCVdXo/bW1tSI3N1dceumlMfsoLy8XLpcren9VVVWz75+IiNpOXb/++eefN7rNxRdfLEpKSsS+ffti7j/nnHOEy+USPp9PCCHEgw8+KACI7777Lma7wYMHi2OPPTZ6e/78+cJut4uffvopZrsbbrhByLIstm/fLoT4PWbm5OSIPXv2xGyrqqoIBoMx91VVVYmuXbuKiy66KHrfCy+8IACIxYsXR+/TNE0ce+yxAoB45JFHovcfd9xxYujQoSIQCETv03VdHHnkkaJ///6Nfj77Gzp0qDj//POjt2+88UbRpUsXEQ6HY7Zr7hjj66+/FgDE//t//6/J/S1dulQAECaTSRxzzDFi7ty54sMPPxSapsVs99577wkAYubMmQ1eo+54YN26dQKAuOSSS2Ie//Of/ywAiPfeey96X2lpqQAg3nrrrZhtE/3bzpo1S+Tk5MQcSxARUfI4YoooTdavX4+xY8eid+/eePfdd5GXl5f0a9SNmoo3pSAel8uFa665Bq+88gq+/vrrpPf3xBNPYMSIEejXrx8ARKexteQsad1KgE0VT7darVi7di2uu+46AJEpdhdffDFKSkpw9dVXR4fc7927Fx988AEuuugi9OrVK+Y1JEkCAGiahnfeeQeTJ0/GAQccEH28pKQE5513HtasWdOgAOull14KWZajt1esWIHq6mqce+652LdvX/QiyzKOOOKI6NQKq9UKk8mEVatWoaqqKunPhoiI2pYQAi+88AImTZoEIURMHz9hwgS43e7oaOMzzjgDiqLE1EVav349NmzYELNa7XPPPYcxY8YgLy8v5vXGjx8PTdPwwQcfxLRhypQpKCwsjLlPluXo9DVd11FZWQlVVTFixIiY0c9vvfUWjEYjLr300uh9BoMBV155ZczrVVZW4r333sPUqVNRW1sbbVNFRQUmTJiATZs2YefOnU1+Vt9++y2+++676OhpANG4+Pbbbzf53PoSORYAgIsuughvvfUWxo0bhzVr1mD+/PkYM2YM+vfvHzOF8IUXXoAkSbj55psbvEbd8cAbb7wBAA1Grc2ePRsA8Prrr8fc36dPH0yYMCHmvkT/trm5ufB6vc2OCCMioqYxMUWUJpMmTYLT6cTbb7/doFaSx+NBeXl59LJ37964r9GSRNOsWbOQm5ubdK2p6upqvPHGGxg7dix+/vnn6GX06NH44osvkl4hz+PxAIgkt5ricrlw5513YuvWrdi6dSuWLl2KAQMG4N5778X8+fMBAL/88gsAYMiQIY2+zt69e+Hz+TBgwIAGjw0aNAi6rmPHjh0x9/fp0yfm9qZNmwAAxx57LAoLC2Mu77zzTrQAq9lsxh133IE333wTXbt2xdFHH40777wz6SmPRETUNvbu3Yvq6mo89NBDDfr3Cy+8EMDvRba7dOmC4447LmY63zPPPANFUXDGGWdE79u0aRPeeuutBq83fvz4mNerUz/m1Hn00UcxbNgwWCwWFBQUoLCwEK+//jrcbnd0m23btqGkpAQ2my3muXUnkur8/PPPEEJg7ty5DdpVl8xprpj4448/DrvdjgMOOCB6LGCxWNC7d++kT1QleiwAABMmTMDbb7+N6upqfPDBB7jyyiuxbds2nHLKKdE2b968Gd26dUN+fn6jr7Nt2zYYDIYGn01xcTFyc3Oxbdu2mPvj/V0S/dteccUVOPDAA3HiiSeiR48e0QQbERElh6vyEaXJlClT8Oijj+KJJ57An/70p5jHFi5cGDMKqrS0tNGi2bNmzcI///lP3HLLLdEaGE2pS2bNmzcvqVFTzz33HILBIBYtWoRFixY1ePyJJ55IeOQWEDm7XFRU1CAp15TS0lJcdNFFOP3003HAAQfgiSeeaHQVoFSov+pOXUHTxx57DMXFxQ22V5Tfu8xrrrkGkyZNwvLly/H2229j7ty5WLBgAd577z0ceuihaWszERElr65/v+CCCxpdaXbYsGHR6+eccw4uvPBCrFu3DocccgieffZZHHfccejSpUvMax5//PG4/vrr477egQceGHM73kpvjz/+OGbMmIHJkyfjuuuuQ1FREWRZxoIFC6ILhLTkff75z39uMAqoTv2Ezf6EEHjqqafg9XoxePDgBo/v2bMHHo8nOhKqOevXr292n/XZbDaMGTMGY8aMQZcuXXDLLbfgzTffbPTv1pi6EVTNifd3SfRvW1RUhHXr1uHtt9/Gm2++iTfffBOPPPIIpk2bhkcffTSp9hIRdWZMTBGlyV133QVFUXDFFVfA6XTivPPOiz42bdo0HHXUUdHbTS1LvH+iKdGDsmuuuQaLFy/GLbfcgtzc3ISe88QTT2DIkCFxh8c/+OCDePLJJxNOTH3yySfYvHlzg+WjE5WXl4e+fftGD2jrpubV3Y6nsLAQNpsNP/74Y4PHfvjhBxgMBvTs2bPJ/fbt2xdA5ECz7qxoc9vPnj0bs2fPxqZNm3DIIYdg0aJFePzxx5t9LhERtZ3CwkI4nU5ompZQ/z558mT86U9/ik7n++mnnzBnzpyYbfr27QuPx5PQ6zXm+eefxwEHHIAXX3wxJpFSPxaXlpbi/fffh8/nixk19fPPP8dsVxcvjUZji9q1evVq/Prrr7j11lsxaNCgmMeqqqpw2WWXYfny5QnH98ceewySJOH4449Pui0AMGLECABAWVkZgMhn/vbbb6OysrLRUVOlpaXQdR2bNm2KeQ+7d+9GdXV1dOGVpiTztzWZTJg0aRImTZoEXddxxRVX4MEHH8TcuXOTSsgREXVmnMpHlCaSJOGhhx7CmWeeienTp8cs03zAAQdg/Pjx0cvo0aObfK1rrrkGubm5uPXWWxPad10y6+WXX45Z1acxO3bswAcffICpU6fizDPPbHC58MIL8fPPP8dd2a6+bdu2YcaMGTCZTNHaUY355ptvsG/fvrivsWHDhui0vMLCQhx99NF4+OGHG6x6J35bzU+WZZxwwgl4+eWXY0af7d69G08++SSOOuqoZkdvTZgwATk5ObjtttsQDocbPF435dLn8yEQCMQ81rdvXzidzgZLURMRUebJsowpU6bghRdeiHuSo/6U+tzcXEyYMAHPPvssnn76aZhMJkyePDlmm6lTp+KTTz6JW3epuroaqqom1C4AMSvTrl27Fp988knMdhMmTEA4HMZ//vOf6H26ruPf//53zHZFRUUYN24cHnzwwWgyp6n3WV/dNL7rrruuwbHApZdeiv79+yc8ne/222/HO++8g7PPPhv9+/dvctuVK1fGvb+uXlTd8cCUKVMghIh7oqzuMzzppJMAoMEo87vvvhsAcPLJJzfb9kT/thUVFTGPGQyG6Mg7Hg8QESWOI6aI0shgMODxxx/H5MmTMXXqVLzxxhs49thjk34dl8uFWbNmJTWVrm4K4DfffAO73d7ktk8++SSEEDj11FPjPn7SSSdBURQ88cQTOOKII6L3f/XVV3j88ceh6zqqq6vx+eefRwuTPvbYYzHTIuJZsWIFbr75Zpx66qn4wx/+AIfDgV9++QUPP/wwgsFgTJ2se+65B0cddRSGDx+Oyy67DH369MHWrVvx+uuvR5Nvf//737FixQocddRRuOKKK6AoCh588EEEg0HceeedzX5mOTk5uP/++/HHP/4Rw4cPxznnnIPCwkJs374dr7/+OkaPHo17770XP/30E4477jhMnToVgwcPhqIoeOmll7B79+64S1ETEVHbePjhh+PW+Jk1axZuv/12vP/++zjiiCNw6aWXYvDgwaisrMRXX32Fd999F5WVlTHPOfvss3HBBRfgvvvuw4QJExqMQL7uuuvwyiuv4JRTTsGMGTNw2GGHwev14rvvvsPzzz+PrVu3xkz9i+eUU07Biy++iNNPPx0nn3wytmzZggceeACDBw+O1mcCIiO4Dj/8cMyePRs///wzBg4ciFdeeSXa5v1HW/373//GUUcdhaFDh+LSSy/FAQccgN27d+OTTz7Br7/+im+++SZuW4LBIF544QUcf/zxsFgscbc59dRTsWTJEuzZswdFRUUAAFVVoyOFA4EAtm3bhldeeQXffvstjjnmGDz00ENNfgYAcNppp6FPnz6YNGkS+vbtC6/Xi3fffRevvvoqRo4ciUmTJgEAjjnmGPzxj3/EPffcg02bNmHixInQdR0ffvghjjnmGFx11VU4+OCDMX36dDz00EOorq7G2LFj8dlnn+HRRx/F5MmTccwxxzTbnkT/tpdccgkqKytx7LHHokePHti2bRv+9a9/4ZBDDmkw4oyIiJqQsfUAiTqom2++WQAQe/fujd7n8/nE2LFjhcPhEJ9++mmTzx87dqw46KCDGtxfVVUlXC6XACDuuuuu6P3vv/++ACCee+65RtvS1FLOQkSWhe7Vq1eT24wbN04UFRWJcDgcXfq67qIoisjPzxdHHHGEmDNnjti2bVuTr1Xnl19+ETfddJP4wx/+IIqKioSiKKKwsFCcfPLJMcs511m/fr04/fTTRW5urrBYLGLAgAFi7ty5Mdt89dVXYsKECcLhcAibzSaOOeYY8fHHH8ds09yy4u+//76YMGGCcLlcwmKxiL59+4oZM2aIL774QgghxL59+8SVV14pBg4cKOx2u3C5XOKII44Qzz77bELvm4iIUquuX2/ssmPHDiGEELt37xZXXnml6NmzpzAajaK4uFgcd9xx4qGHHmrwmjU1NcJqtQoA4vHHH4+739raWjFnzhzRr18/YTKZRJcuXcSRRx4pFi5cKEKhkBBCRGPm/rG7jq7r4rbbbhOlpaXCbDaLQw89VLz22mti+vTporS0NGbbvXv3ivPOO084nU7hcrnEjBkzxEcffSQAiKeffjpm282bN4tp06aJ4uJiYTQaRffu3cUpp5winn/++UY/wxdeeEEAEEuXLm10m1WrVgkAYsmSJUIIIaZPnx7zOdtsNtG7d28xZcoU8fzzzwtN0xp9rf099dRT4pxzzhF9+/YVVqtVWCwWMXjwYPHXv/5V1NTUxGyrqqq46667xMCBA4XJZBKFhYXixBNPFF9++WV0m3A4LG655RbRp08fYTQaRc+ePcWcOXNEIBCIea3S0lJx8sknx21TIn/b559/XpxwwgmiqKhImEwm0atXL/GnP/1JlJWVJfS+iYgoQhJiv7HDRERERETULixfvhynn3461qxZ02xZACIiomzF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7+KII47AxRdfDE3TGmxvtVpRXFwcvZhMJthstpj7ZFmObr9w4UKUlJSgoKAAV155JcLhcPSx3r17Y/78+Zg2bRpycnJw2WWXAQDWrFmDMWPGwGq1omfPnpg5cya8Xm/0effddx/69+8Pi8WCrl274swzz4xpo67ruP7665Gfn4/i4mLMmzcv5vHt27fjtNNOg8PhQE5ODqZOndrgxN/+NE3Dtddei9zcXBQUFOD666+HyMIFRzKamJozZw4++OADbN26Fd999x3mzJmDVatW4fzzzwcATJs2DXPmzIluP2vWLLz11ltYtGgRfvjhB8ybNw9ffPEFrrrqqky9BSIiok6JMZyIiKgd8nobvwQCiW/r9ye2bQs8++yzGDhwIAYMGIALLrgADz/8cELJFIPBgFmzZmHbtm348ssvW7TvOu+//z42b96M999/H48++iiWLVvWIEG2cOFCHHzwwfj6668xd+5cbN68GRMnTsSUKVPw7bff4plnnsGaNWuixzpffPEFZs6ciVtvvRU//vgj3nrrLRx99NExr/noo4/Cbrdj7dq1uPPOO3HrrbdixYoVACJJq9NOOw2VlZVYvXo1VqxYgV9++QVnn312o+9j0aJFWLZsGR5++GGsWbMGlZWVeOmll1r12aRDRouf79mzB9OmTUNZWRlcLheGDRuGt99+G8cffzyASDbQYPg9d3bkkUfiySefxN/+9jfceOON6N+/P5YvX44hQ4Zk6i0QERF1SozhRERE7ZDD0fhjJ50EvP7677ebWjV17Fhg1arfb/fuDezb13C7FozOWbp0KS644AIAwMSJE+F2u7F69WqMGzeu2ecOHDgQQKQO1eGHH570vuvk5eXh3nvvhSzLGDhwIE4++WSsXLkSl156aXSbY489FrNnz47evuSSS3D++efjmmuuAQD0798f99xzD8aOHYv7778f27dvh91uxymnnAKn04nS0lIceuihMfsdNmwYbr755ujz7733XqxcuRLHH388Vq5cie+++w5btmyJTjn83//+h4MOOgiff/45Ro4c2eB9LF68GHPmzMEZZ5wBAHjggQfw9ttvt/hzSZeMJqaWLl3a5OOr9v9H/81ZZ52Fs846K00too5CCAFvSIOq6VBkA+wmGZIkZbpZ1FkoCjB9+u/XiTogxvDM6hBxjn0lERHV8+OPP+Kzzz6LjupRFAVn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xp7aIDwBFXaTAptJgVE2oLyG8aJR7CuJiGg/kyZNwsSJE+M+9uGHH0KSJJxxxhlYuXJlG7eM0o2JKaJGCCGwrcILb1BFicsKi1GGQZJgMcoocVnhDarYXultdJoGdXL79kUuRNThCSGwvcKLsKrDYVFgMcqQJECRJZTkMF40iX0lERH95uKLL8aKFSvw66+/NnjskUcewYgRIzBs2DAUFBSkvS3hME8otSUmpoga4Q1pqPSEkGczxX08z2ZCRW0I3pDWxi0jIqJsUhcvLEY57uOMF0RERM075ZRTUFhYiGXLlsXc7/F48Nxzz+Hiiy/GvHnzcMghh0Qf+/zzz3H88cejS5cucLlcGDt2LL766quY5//www846qijYLFYMHjwYLz77ruQJAnLly8HAGzduhWSJOGZZ57B2LFjYbFY8MQTT6CiogLnnnsuunfvDpvNhqFDh+Kpp55K86fQOTExRdQIVdOh6gImJf7XxCgboOoCqqanZH91BXOrfSF4girPrBMRtROqpkMVArIh/qIYrYkXjA1ERJQKQgiEtFBGLonGLkVRMG3aNCxbtizmOc899xw0TcO5557b4Dm1tbWYPn061qxZg08//RT9+/fHSSedhNraWgCApmmYPHkybDYb1q5di4ceegh//etf4+7/hhtuwKxZs7Bx40ZMmDABgUAAhx12GF5//XWsX78el112Gf74xz/is88+a8FfgJrC4udEjVBkAxSDhJCqxz0LHtZ0KAYJitz6/G4qC6xnegXBTO+fiKitKbIBiiRB0wUUuWF/19J44faHsXWfB7uqAwiqOsyKAd1yLejdxdHpi6kz1hARJSesh3Hbh7dlZN83jrkRJjn+LJT6LrroItx1111YvXo1xo0bByAyjW/KlClwuVwNtj/22GNjbj/00EPIzc3F6tWrccopp2DFihXYvHkzVq1aheLiYgDAP/7xDxx//PENXuuaa67BGWecEXPfn//85+j1q6++Gm+//TaeffZZHH744Qm9H0oME1NEjbCbZOQ7TCh3B1DisjZ4vMoXQkmuBXZT/KkbiaorsO4NqsizmWBSDAipOsrdAdQGVAzp7kr4B0imVxDM9P6JiDKhLl4EwhoccsNDq5bEC7c/jLW/VGBrhRdCAAYJ0AWwo8qH3TVBHHFAQaftVxlriIg6roEDB+LII4/Eww8/jHHjxuHnn3/G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hz59+iT9Gpqm4bvvvkNJSUkaWkhE1AI2G/D995GLzZbp1hClBWM4tRr7SiIiIkKGE1NXXnklHn/8cTz55JNwOp0oLy9HeXk5/H5/dJtp06Zhzpw50du33nor3nnnHfzyyy/46quvcMEFF2Dbtm245JJLMvEWiIiIOiXGcCIiIupoevfujcWLF2e6GZ1ORqfy3X///QCAcePGxdz/yCOPYMaMGQCA7du3w2D4PX9WVVWFSy+9FOXl5cjLy8Nhhx2Gjz/+GIPrhoITERFR2jGGExERUarNmDED1dXVWL58ecz9q1atwjHHHIOqqirk5uambf+ff/457HZ72l6f4stoYkoI0ew2q1atirn9z3/+E//85z/T1CIiohTw+YCRIyPXP/+cU1SoQ2IMp1ZjX0lERG0kFArBZDI1u11hYWEbtIbqy5pV+YiIOgwhgA0bIpcEfrwTEXVK7CuJiKiF1qxZgzFjxsBqtaJnz56YOXMmvF5v9PHevXtj/vz5mDZtGnJycnDZZZcBAF544QUcdNBBMJvN6N27NxYtWhTzuvWn8kmShP/+9784/fTTYbPZ0L9/f7zyyitt8h47EyamiIiIiIiIiDowIQRCWigjl0RGWSdj8+bNmDhxIqZMmYJvv/0WzzzzDNasWYOrrroqZruFCxfi4IMPxtdff425c+fiyy+/xNSpU3HOOefgu+++w7x58zB37lwsW7asyf3dcsstmDp1Kr799lucdNJJOP/881FZWZnS99TZZXQqHxERERERERGlV1gP47YPb8vIvm8ccyNMcvPT6Oq89tprcDgcMfdpmha9vmDBApx//vm45pprAAD9+/fHPffcg7Fjx+L++++HxWIBABx77LGYPXt29Hnnn38+jjvuOMydOxcAcOCBB2LDhg246667ovUx45kxYwbOPfdcAMBtt92Ge+65B5999hkmTpyY8HuipnHEFBERERERERFlhWOOOQbr1q2Lufz3v/+NPv7NN99g2bJlcDgc0cuECROg6zq2bNkS3W7EiBExr7tx40aMHj065r7Ro0dj06ZNMYmv+oYNGxa9brfbkZOTgz179rT2bdJ+OGKKiIiIiIiIqAMzGoy4ccyNGdt3Mux2O/r16xdz36+//hq97vF48Kc//QkzZ85s8NxevXrFvE4qGI2x7ZckCbqup+S1KYKJKSIiIiIiIqIOTJKkpKbT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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot for k-NN AD\n", + "plt.figure(figsize=(12, 5))\n", + "plt.subplot(1, 2, 1)\n", + "plt.scatter(knn_scores, abs_errors, alpha=0.2, label='Test compounds')\n", + "plt.axvline(x=knn_ad.threshold_, color='r', linestyle='--', label='AD Threshold')\n", + "\n", + "for result in results:\n", + " plt.axvline(x=result['k-NN Score'], color='g', alpha=0.5,\n", + " label=f\"{result['Drug']}\")\n", + "\n", + "plt.xlabel('k-NN AD Score')\n", + "plt.ylabel('Absolute Prediction Error')\n", + "plt.title('k-NN AD Scores')\n", + "#plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n", + "\n", + "# Plot for Leverage AD\n", + "plt.subplot(1, 2, 2)\n", + "plt.scatter(leverage_raw_scores, abs_errors, alpha=0.2, label='Test compounds')\n", + "plt.axvline(x=leverage_ad.threshold_, color='r', linestyle='--', label='AD Threshold')\n", + "\n", + "for result in results:\n", + " plt.axvline(x=result['Leverage Score'], color='g', alpha=0.5,\n", + " label=f\"{result['Drug']}\")\n", + "\n", + "plt.xlabel('Leverage AD Score')\n", + "plt.ylabel('Absolute Prediction Error')\n", + "plt.title('Leverage AD Scores')\n", + "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3c018e68", + "metadata": {}, + "source": [ + "## Conclusions on testing the AD estimators\n", + "\n", + "This notebook demonstrated two different approaches to applicability domain estimation:\n", + "\n", + "1. The k-NN based approach with binary fingerprints and Tanimoto distance provides a chemical similarity-based assessment\n", + "of whether new compounds are similar enough to the training set.\n", + "\n", + "2. The leverage-based approach with count-based fingerprints and dimensionality reduction focuses on the statistical\n", + "novelty of compounds in the reduced feature space.\n", + "\n", + "Heroin and Aspirin was predicted to have a low affinity, whereas Viagra was predicted as having a ~9 pXC50 corresponding to nanomolar affinity. As the regression model had only been trained on actives it will have a tendency to always predict things as active, which is hard to believe for compounds so dissimilar to the training set and with our prior knowledge about their primary targets.\n", + "\n", + "The famous drugs we tested showed marked differences between the two AD estimation techniques. \n", + "\n", + "The kNN based method using tanimoto distance showed all test drugs to be distant from the training set and thus outside the applicability domain, whereas the leverage method gave the \"green light\" for all of them. As the drug have different primary targets than the SLC6A4 serotonin transporter, it seems like the kNN based method in this instance (dataset, featurization, ML-model) is a better way to estimate the AD for given novel compounds. This is consistent with our analysis of the 95 percentile of the absolute errors for two different methods, where kNN had a higher 95% percentile error outside the domain, it was lower for the leverage based method.\n", + "\n" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "formats": "ipynb,py:percent", + "main_language": "python" + }, + "kernelspec": { + "display_name": "vscode", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scikit_mol/adapters.py b/scikit_mol/adapters.py deleted file mode 100644 index 5e3b2f0..0000000 --- a/scikit_mol/adapters.py +++ /dev/null @@ -1,539 +0,0 @@ -from typing import Dict, List, Optional, Tuple - -import numpy as np -from numpy.typing import NDArray -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.pipeline import FeatureUnion -from sklearn.utils import Bunch -from sklearn.utils._estimator_html_repr import _VisualBlock -from sklearn.utils._user_interface import _print_elapsed_time -from sklearn.utils.metadata_routing import ( - _raise_for_params, - _routing_enabled, - process_routing, -) -from sklearn.utils.parallel import Parallel as skParallel -from sklearn.utils.parallel import delayed -from sklearn.utils.validation import check_is_fitted - - -class EstimatorUnion(FeatureUnion): - """EXPERIMENTAL: A more flexible version of FeatureUnion that supports various estimator types. - - This class extends scikit-learn's FeatureUnion to support estimators with different - method interfaces (predict, transform, etc.) and allows explicit method selection. - It maintains all functionality of FeatureUnion while adding flexible method resolution. - - Parameters - ---------- - estimators : list of (str, estimator) tuples - List of (name, estimator) tuples, where estimator is any scikit-learn - compatible estimator with at least one of the methods specified in - method_resolution_order. - method_resolution_order : tuple of str, default=("predict", "transform") - Ordered tuple of method names to try when getting output from estimators. - Methods are tried in order until a valid one is found. - selected_methods : dict or None, default=None - Optional mapping of estimator names to specific methods to use. Takes - precedence over method_resolution_order. - n_jobs : int or None, default=None - Number of jobs to run in parallel. None means 1. - transformer_weights : dict or None, default=None - Multiplicative weights for features per transformer. Keys are transformer - names, values are weights. - verbose : bool, default=False - If True, the time elapsed while fitting each transformer will be printed. - verbose_feature_names_out : bool, default=True - If True, the feature names out will be verbose. - - Attributes - ---------- - transformers_ : list - List of fitted transformers. - - Notes - ----- - This class inherits from FeatureUnion and maintains all its functionality including - parallel processing, transformer weights, and metadata routing. The key extension is - the ability to handle estimators with different method interfaces through configurable - method resolution. - - See Also - -------- - sklearn.pipeline.FeatureUnion : The parent class providing base functionality. - """ - - def __init__( - self, - estimator_list: List[Tuple[str, BaseEstimator]], - *, - method_resolution_order: Tuple[str, ...] = ("predict", "transform"), - selected_methods: Optional[Dict[str, str]] = None, - n_jobs: Optional[int] = None, - transformer_weights: Optional[Dict[str, float]] = None, - verbose: bool = False, - verbose_feature_names_out: bool = True, - ) -> None: - # Store all parameters as properties - self.estimator_list = estimator_list - self.method_resolution_order = method_resolution_order - self.selected_methods = selected_methods or {} - self.n_jobs = n_jobs - self.transformer_weights = transformer_weights - self.verbose = verbose - self.verbose_feature_names_out = verbose_feature_names_out - - @property - def estimator_list(self) -> List[Tuple[str, BaseEstimator]]: - """Get estimators (alias for transformer_list).""" - return self.transformer_list - - @estimator_list.setter - def estimator_list(self, estimator_list: List[Tuple[str, BaseEstimator]]) -> None: - """Set estimators (and internal transformer_list property).""" - self.transformer_list = estimator_list - - def _get_method_name(self, estimator_tuple: Tuple[str, BaseEstimator]) -> str: - """Get the appropriate method name for the estimator, raising clear errors if not found. - - Parameters - ---------- - estimator_tuple : tuple of (str, estimator) - Tuple containing the estimator name and instance. - - Returns - ------- - str - Name of the method to use for this estimator. - - Raises - ------ - ValueError - If no valid method is found for the estimator. - """ - name, estimator = estimator_tuple - - # Check explicit method if specified - if name in self.selected_methods: - method = self.selected_methods[name] - if not hasattr(estimator, method): - raise ValueError( - f"Estimator '{name}' ({type(estimator).__name__}) does not have " - f"explicitly selected method '{method}'. Consider changing selected_methods " - f"or using only method_resolution_order to specify valid methods." - ) - return method - - # Try methods in resolution order - for method in self.method_resolution_order: - if hasattr(estimator, method): - return method - - raise ValueError( - f"Estimator '{name}' ({type(estimator).__name__}) does not have any of " - f"the methods: {', '.join(self.method_resolution_order)}. Consider using " - f"method_resolution_order or selected_methods to specify valid methods." - ) - - def _get_estimator_output( - self, estimator_tuple: Tuple[str, BaseEstimator], X: NDArray - ) -> NDArray: - """Get output from estimator using appropriate method.""" - name, estimator = estimator_tuple - method = self._get_method_name(estimator_tuple) - output = getattr(estimator, method)(X) - - # Ensure 2D output - if output.ndim == 1: - output = output.reshape(-1, 1) - return output - - def _validate_transformers(self): - names, transformers = zip(*self.transformer_list) - - # validate names - self._validate_names(names) - - # validate estimators - for t in transformers: - if t in ("drop", "passthrough"): - continue - # TODO, make a check that the methods in the method_resolution_order /method mappting are present - # if not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not hasattr( - # t, "transform" - # ): - # raise TypeError( - # "All estimators should implement fit and " - # "transform. '%s' (type %s) doesn't" % (t, type(t)) - # ) - - def transform(self, X, **params): - """Transform X separately by each transformer, concatenate results. - - Parameters - ---------- - X : iterable or array-like, depending on transformers - Input data to be transformed. - - **params : dict, default=None - - Parameters routed to the `transform` method of the sub-transformers via the - metadata routing API. See :ref:`Metadata Routing User Guide - ` for more details. - - .. versionadded:: 1.5 - - Returns - ------- - X_t : array-like or sparse matrix of shape (n_samples, sum_n_components) - The `hstack` of results of transformers. `sum_n_components` is the - sum of `n_components` (output dimension) over transformers. - """ - _raise_for_params(params, self, "transform") - - if _routing_enabled(): - routed_params = process_routing(self, "transform", **params) - else: - # TODO(SLEP6): remove when metadata routing cannot be disabled. - routed_params = Bunch() - for name, _ in self.transformer_list: - routed_params[name] = Bunch(transform={}) - - # Build delayed jobs with custom methods - delayed_jobs = [] - for name, trans, weight in self._iter(): - method_name = self._get_method_name((name, trans)) - delayed_jobs.append( - delayed( - _transform_one - )( # Seems like the only reason we modify this method from base class is to handle it with a custom function for parallel processing - trans, - X, - None, - weight, - params=routed_params[name], - method=method_name, - ) - ) - - Xs = skParallel(n_jobs=self.n_jobs)(delayed_jobs) - - if not Xs: - # All transformers are None - return np.zeros((X.shape[0], 0)) - - return self._hstack(Xs) - - def fit_transform(self, X, y=None, **params): - """Fit all transformers, transform the data and concatenate results. - - Parameters - ---------- - X : iterable or array-like, depending on transformers - Input data to be transformed. - - y : array-like of shape (n_samples, n_outputs), default=None - Targets for supervised learning. - - **params : dict, default=None - - If `enable_metadata_routing=False` (default): - Parameters directly passed to the `fit` methods of the - sub-transformers. - - - If `enable_metadata_routing=True`: - Parameters safely routed to the `fit` methods of the - sub-transformers. See :ref:`Metadata Routing User Guide - ` for more details. - - .. versionchanged:: 1.5 - `**params` can now be routed via metadata routing API. - - Returns - ------- - X_t : array-like or sparse matrix of \ - shape (n_samples, sum_n_components) - The `hstack` of results of transformers. `sum_n_components` is the - sum of `n_components` (output dimension) over transformers. - """ - if _routing_enabled(): - routed_params = process_routing(self, "fit_transform", **params) - else: - # TODO(SLEP6): remove when metadata routing cannot be disabled. - routed_params = Bunch() - for name, obj in self.transformer_list: - if hasattr(obj, "fit_transform"): - routed_params[name] = Bunch(fit_transform={}) - routed_params[name].fit_transform = params - else: - routed_params[name] = Bunch(fit={}) - routed_params[name] = Bunch(transform={}) - routed_params[name].fit = params - - results = self._parallel_func(X, y, _fit_transform_one, routed_params) - if not results: - # All transformers are None - return np.zeros((X.shape[0], 0)) - - Xs, transformers = zip(*results) - self._update_transformer_list(transformers) - - return self._hstack(Xs) - - def predict(self, X: NDArray) -> NDArray: - """Predict using all estimators. - - Alias for transform to maintain predictor interface. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Input data to be predicted. - - Returns - ------- - ndarray of shape (n_samples, sum_n_output_features) - Horizontally stacked predictions of all estimators. - """ - return self.transform(X) - - def get_feature_names_out(self, input_features=None): - """Get output feature names for transformation. - - Parameters - ---------- - input_features : array-like of str or None, default=None - Input features. - - Returns - ------- - feature_names_out : ndarray of str objects - Transformed feature names. - """ - # List of tuples (name, feature_names_out) - transformer_with_feature_names_out = [] - for name, trans, _ in self._iter(): - if hasattr(trans, "predict") and not hasattr( - trans, "get_feature_names_out" - ): - # Assume predictors only return 1D output and thus we use their name as feature_name - feature_names_out = np.array([name]) - elif not hasattr(trans, "get_feature_names_out"): - raise AttributeError( - "Transformer %s (type %s) does not provide get_feature_names_out." - % (str(name), type(trans).__name__) - ) - else: - feature_names_out = trans.get_feature_names_out(input_features) - transformer_with_feature_names_out.append((name, feature_names_out)) - - return self._add_prefix_for_feature_names_out( - transformer_with_feature_names_out - ) - - -def _transform_one(transformer, X, y, weight, params=None, method="transform"): - """Call transform and apply weight to output. - - Parameters - ---------- - transformer : estimator - Estimator to be used for transformation. - - X : {array-like, sparse matrix} of shape (n_samples, n_features) - Input data to be transformed. - - y : ndarray of shape (n_samples,) - Ignored. - - weight : float - Weight to be applied to the output of the transformation. - - method : str - Method to use for transformation (e.g. "transform", "predict", "predict_proba"). - - params : dict - Parameters to be passed to the transformer's ``transform`` method. - - This should be of the form ``process_routing()["step_name"]``. - """ - res = getattr(transformer, method)(X, **params.transform) - # Ensure 2D output - if res.ndim == 1: - res = res.reshape(-1, 1) - # if we have a weight for this transformer, multiply output - if weight is None: - return res - return res * weight - - -def _fit_transform_one( - transformer, - X, - y, - weight, - message_clsname="", - message=None, - params=None, - method="transform", -): - """ - Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned - with the fitted transformer. If ``weight`` is not ``None``, the result will - be multiplied by ``weight``. - - ``params`` needs to be of the form ``process_routing()["step_name"]``. - """ - params = params or {} - with _print_elapsed_time(message_clsname, message): - if hasattr(transformer, "fit_transform"): - res = transformer.fit_transform(X, y, **params.get("fit_transform", {})) - elif hasattr(transformer, "transform"): - res = transformer.fit(X, y, **params.get("fit", {})).transform( - X, **params.get("transform", {}) - ) - # Custom handling of methods that has predict but no fit_transform or transform - elif hasattr(transformer, "predict"): - transformer.fit(X, y, **params.get("fit", {})) - res = transformer.predict(X, **params.get("predict", {})) - if res.ndim == 1: - res = res.reshape(-1, 1) - else: - raise ValueError( - f"Transformer {transformer} does not have a fit_transform, fit or predict method." - ) - - if weight is None: - return res, transformer - return res * weight, transformer - - -class _BaseAdapter(BaseEstimator): - """EXPERIMENTAL: Base class for adapters that wrap estimators and modify their interface.""" - - def __init__( - self, estimator: BaseEstimator, _feature_names_out: Optional[List[str]] = None - ): - """Initialize the adapter with an estimator.""" - self.estimator = estimator - self._feature_names_out = _feature_names_out - - def get_feature_names_out(self, input_features=None): - """Get output feature names for transformation. - - Parameters - ---------- - input_features : array-like of str or None, default=None - Input features.""" - - return ["tester"] - - def __getattr__(self, name): - """Delegate any unknown attributes/methods to wrapped estimator.""" - if hasattr(self.estimator, name): - attr = getattr(self.estimator, name) - if isinstance(attr, property): - return attr.__get__(self.estimator) - return attr - raise AttributeError( - f"Neither {self.__class__.__name__} nor {self.estimator.__class__.__name__} " - f"has attribute '{name}'" - ) - - def __dir__(self): - """List all attributes including those from wrapped estimator.""" - return list(set(super().__dir__() + dir(self.estimator))) - - @property - def __dict__(self): - """Include estimator's properties in the instance dict.""" - d = super().__dict__.copy() - estimator_dict = vars(self.estimator) - for name, value in estimator_dict.items(): - if not name.startswith("_"): - d[name] = value - return d - - def _sk_visual_block_(self): - # TODO: this looks strange when putting the wrapped estimator into a pipeline - """Generate information about how to display the adapter.""" - return _VisualBlock( - "parallel", - [self.estimator], - names=None, - name_details=None, - name_caption=None, - dash_wrapped=False, - ) - - -class PredictToTransformAdapter(_BaseAdapter, TransformerMixin): - """EXPERIMENTAL: Adapter that exposes an estimator's predict method as transform.""" - - def __init__(self, estimator: BaseEstimator, method: str = "predict"): - """Initialize the adapter with an estimator and a method to use. - - Parameters - ---------- - estimator : BaseEstimator - The estimator to wrap. - method : str, default="predict" - The method to use for transformation. - """ - super().__init__(estimator) - self.method = method - - def transform(self, X): - """Transform X using the wrapped estimator's specified method. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The input data to transform. - check_is_fitted(self) - - Example - -------- - >>> from sklearn.linear_model import LogisticRegression - >>> from sklearn_mol.adapters import PredictToTransformAdapter - >>> estimator = LogisticRegression() - >>> adapter = PredictToTransformAdapter(estimator, method="predict"") - >>> adapter.fit(X, y) - >>> adapter.transform(X) - """ - prediction = getattr(self.estimator, self.method)(X) - if prediction.ndim == 1: - prediction = prediction.reshape(-1, 1) - return prediction - - -class TransformToPredictAdapter(_BaseAdapter, TransformerMixin): - """EXPERIMENTAL: Adapter that exposes an estimator's transform method as predict. - - 2D column vector output is flattened to 1D.""" - - def __init__(self, estimator: BaseEstimator, method: str = "transform"): - super().__init__(estimator) - self.method = method - - def predict(self, X): - """Predict using the wrapped estimator's specified method. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The input data to predict. - - Example - -------- - >>> from sklearn.preprocessing import StandardScaler - >>> from sklearn_mol.adapters import TransformToPredictAdapter - >>> estimator = StandardScaler() - >>> adapter = TransformToPredictAdapter(estimator, method="transform") - >>> adapter.fit(X, y) - >>> adapter.predict(X) - """ - check_is_fitted(self) - prediction = self.estimator.transform(X) - if prediction.shape[1] == 1: - prediction = prediction.flatten() - return prediction diff --git a/scikit_mol/applicability_old.py b/scikit_mol/applicability_old.py deleted file mode 100644 index e7d8ecf..0000000 --- a/scikit_mol/applicability_old.py +++ /dev/null @@ -1,276 +0,0 @@ -import numpy as np -from scipy import linalg, stats -from scipy.sparse import csr_matrix -from sklearn.base import BaseEstimator, TransformerMixin, check_array, check_is_fitted -from sklearn.neighbors import NearestNeighbors - - -class NearestNeighborsDistance(BaseEstimator, TransformerMixin): - def __init__(self, n_neighbors=1): - self.n_neighbors = n_neighbors - self.feature_name = "nn_distance" - - def fit(self, X, y=None): - self.X_sparse = csr_matrix(X) - self.nn = NearestNeighbors(n_neighbors=self.n_neighbors, metric="cosine") - self.nn.fit(self.X_sparse) - return self - - def transform(self, X): - X_sparse = csr_matrix(X) - distances, _ = self.nn.kneighbors(X_sparse) - avg_distances = np.mean(distances, axis=1) - return avg_distances.reshape(-1, 1) # Return 2D array for consistency - - def predict(self, X): - return self.transform(X) - - def get_feature_names_out(self, input_features=None): - return np.array([self.feature_name]) - - -class LeverageDistanceSlow(BaseEstimator, TransformerMixin): - """Calculate leverage-based distances for applicability domain assessment. - - The leverage approach measures how far a sample is from the center of the - X variable space. It's based on the hat matrix H = X(X'X)^(-1)X'. - - Parameters - ---------- - threshold_factor : float, default=3 - Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n - where p is the number of features and n is the number of samples. - - Attributes - ---------- - n_features_in_ : int - Number of features seen during fit. - X_fit_ : ndarray - Training data used in fit. - leverage_threshold_ : float - Calculated leverage threshold (h*). - """ - - def __init__(self, threshold_factor=3): - self.threshold_factor = threshold_factor - - def fit(self, X, y=None): - """Fit the model using X as training data. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - Training data. - y : Ignored - Not used, present here for API consistency by convention. - - Returns - ------- - self : object - Returns the instance itself. - """ - X = check_array(X, accept_sparse=False) - self.n_features_in_ = X.shape[1] - self.X_fit_ = X - - # Calculate leverage threshold h* - n_samples = X.shape[0] - self.leverage_threshold_ = ( - self.threshold_factor * (self.n_features_in_ + 1) / n_samples - ) - - # Store (X'X)^(-1) for later use - self.xtx_inv_ = np.linalg.inv(X.T @ X) - - return self - - def transform(self, X): - """Calculate leverage-based distances for X. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The data to calculate leverage distances for. - - Returns - ------- - h : ndarray of shape (n_samples, 1) - The leverage values for each sample. - """ - check_is_fitted(self) - X = check_array(X, accept_sparse=False) - - if X.shape[1] != self.n_features_in_: - raise ValueError( - f"X has {X.shape[1]} features, but LeverageDistance " - f"was fitted with {self.n_features_in_} features." - ) - - # Calculate leverage values h = diag(X(X'X)^(-1)X') - # Slighlty different implementation (from another package) - # hat_matrix = X @ self.xtx_inv_ @ X.T - # leverages = np.diag(hat_matrix) - - h = np.sum(X @ self.xtx_inv_ * X, axis=1) - - return h.reshape(-1, 1) - - def predict(self, X): - """Alias for transform, following scikit-learn conventions.""" - return self.transform(X) - - def get_feature_names_out(self, input_features=None): - """Get output feature names. - - Parameters - ---------- - input_features : None - Ignored as the transformer generates new feature names. - - Returns - ------- - feature_names_out : ndarray of str objects - Leverage distance feature name. - """ - check_is_fitted(self) - return np.array(["leverage_distance"]) - - -# Faster but gives some _very_ large distances for some compounds! -class LeverageDistance(BaseEstimator, TransformerMixin): - """Calculate leverage-based distances for applicability domain assessment. - - Parameters - ---------- - threshold_factor : float, default=3 - Factor used in calculating the leverage threshold h* = threshold_factor * (p+1)/n - """ - - def __init__(self, threshold_factor=3): - self.threshold_factor = threshold_factor - - def fit(self, X, y=None): - X = check_array(X, accept_sparse=False) - self.n_features_in_ = X.shape[1] - n_samples = X.shape[0] - - # Calculate leverage threshold h* - self.leverage_threshold_ = ( - self.threshold_factor * (self.n_features_in_ + 1) / n_samples - ) - - # Use more efficient matrix operations - # Calculate (X'X)^(-1) using SVD which is more stable - U, s, Vh = linalg.svd(X, full_matrices=False) - - # Store components for faster transform - self.s_inv_ = 1 / s - self.U_ = U - self.Vh_ = Vh - - return self - - def transform(self, X): - check_is_fitted(self) - X = check_array(X, accept_sparse=False) - - if X.shape[1] != self.n_features_in_: - raise ValueError( - f"X has {X.shape[1]} features, but LeverageDistance " - f"was fitted with {self.n_features_in_} features." - ) - - # Efficient leverage calculation using stored SVD components - # This avoids explicit matrix inversion - Z = X @ self.Vh_.T * self.s_inv_ - h = np.sum(Z * Z, axis=1) - - return h.reshape(-1, 1) - - def predict(self, X): - return self.transform(X) - - def get_feature_names_out(self, input_features=None): - check_is_fitted(self) - return np.array(["leverage_distance"]) - - -class MahalanobisDistance(BaseEstimator, TransformerMixin): - """Calculate Mahalanobis distances for applicability domain assessment. - - Parameters - ---------- - threshold_quantile : float, default=0.975 - Quantile of chi-square distribution to use as threshold. - threshold_strategy : str, default='chi2' - Strategy to compute threshold. Options: - - 'chi2': Use chi-square distribution (theoretical) - - 'empirical': Use empirical distribution from training data - - None: Don't compute threshold (useful for CV) - """ - - def __init__(self, threshold_quantile=0.975, threshold_strategy="chi2"): - self.threshold_quantile = threshold_quantile - self.threshold_strategy = threshold_strategy - - def fit(self, X, y=None): - X = check_array(X) - self.n_features_in_ = X.shape[1] - - # Compute mean and covariance - self.mean_ = np.mean(X, axis=0) - self.covariance_ = np.cov(X, rowvar=False) - self.inv_covariance_ = np.linalg.inv(self.covariance_) - - # Calculate distances for training set - train_distances = self._mahalanobis(X) - self.train_distances_ = train_distances - - # Set threshold based on strategy - if self.threshold_strategy == "chi2": - self.threshold_ = stats.chi2.ppf( - self.threshold_quantile, df=self.n_features_in_ - ) - elif self.threshold_strategy == "empirical": - self.threshold_ = np.quantile(train_distances, self.threshold_quantile) - elif self.threshold_strategy is None: - self.threshold_ = None - else: - raise ValueError(f"Unknown threshold_strategy: {self.threshold_strategy}") - - return self - - def _mahalanobis(self, X): - """Calculate Mahalanobis distances.""" - X_centered = X - self.mean_ - return np.sqrt(np.sum(X_centered @ self.inv_covariance_ * X_centered, axis=1)) - - def transform(self, X): - check_is_fitted(self) - X = check_array(X) - - if X.shape[1] != self.n_features_in_: - raise ValueError( - f"X has {X.shape[1]} features, but {self.__class__.__name__} " - f"was fitted with {self.n_features_in_} features." - ) - - distances = self._mahalanobis(X) - return distances.reshape(-1, 1) - - def set_threshold(self, threshold): - """Set threshold manually, e.g., from cross-validation.""" - self.threshold_ = threshold - return self - - def get_feature_names_out(self, input_features=None): - check_is_fitted(self) - return np.array(["mahalanobis_distance"]) - - def _more_tags(self): - return { - "requires_fit": True, - "X_types": ["2darray"], - "poor_score": False, - "allow_nan": False, - } From 756e6c9636fa252b0c221bff0bbbde31635b0160 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Mar 2025 11:08:56 +0100 Subject: [PATCH 17/24] Further fixes in tests and some estimators --- scikit_mol/applicability/kernel_density.py | 4 +- scikit_mol/applicability/local_outlier.py | 4 + tests/applicability/test_local_outlier.py | 8 +- tests/test_adapters.py | 145 --------------------- tests/test_desctransformer.py | 19 +-- tests/test_fptransformers.py | 21 +-- tests/test_fptransformersgenerator.py | 19 +-- tests/test_metrics.py | 91 ------------- tests/test_parameter_types.py | 13 +- tests/test_safeinferencemode.py | 13 +- tests/test_sanitizer.py | 22 +++- tests/test_smilestomol.py | 13 +- tests/test_transformers.py | 30 ++--- 13 files changed, 93 insertions(+), 309 deletions(-) delete mode 100644 tests/test_adapters.py delete mode 100644 tests/test_metrics.py diff --git a/scikit_mol/applicability/kernel_density.py b/scikit_mol/applicability/kernel_density.py index 3f7281d..c32090c 100644 --- a/scikit_mol/applicability/kernel_density.py +++ b/scikit_mol/applicability/kernel_density.py @@ -33,7 +33,7 @@ class KernelDensityApplicabilityDomain(BaseApplicabilityDomain): 'exponential', 'linear', 'cosine']. percentile : float or None, default=None The percentile of training set densities to use as threshold (0-100). - If None, uses 1.0 (exclude bottom 1% of training samples). + If None, uses 99.0 (exclude bottom 1% of training samples). feature_name : str, default="KernelDensity" Name for the output feature column. @@ -68,7 +68,7 @@ def __init__( percentile: Optional[float] = None, feature_name: str = "KernelDensity", ) -> None: - super().__init__(percentile=percentile or 1.0, feature_name=feature_name) + super().__init__(percentile=percentile or 99.0, feature_name=feature_name) self.bandwidth = bandwidth self.kernel = kernel diff --git a/scikit_mol/applicability/local_outlier.py b/scikit_mol/applicability/local_outlier.py index 9366723..5dfb055 100644 --- a/scikit_mol/applicability/local_outlier.py +++ b/scikit_mol/applicability/local_outlier.py @@ -124,6 +124,10 @@ def _transform(self, X: NDArray) -> NDArray[np.float64]: scores = -self.lof_.score_samples(X) return scores.reshape(-1, 1) + def _set_statistical_threshold(self, X): + """Set the statistical threshold for the LOF scores.""" + self.threshold_ = -self.lof_.offset_ + # def predict(self, X): # """Predict whether samples are within the applicability domain. diff --git a/tests/applicability/test_local_outlier.py b/tests/applicability/test_local_outlier.py index c0b129f..8a5f6cb 100644 --- a/tests/applicability/test_local_outlier.py +++ b/tests/applicability/test_local_outlier.py @@ -19,8 +19,8 @@ def test_n_neighbors_effect(): outlier = np.array([[10, 10]]) # Compare different n_neighbors settings - ad_small = LocalOutlierFactorApplicabilityDomain(n_neighbors=5) - ad_large = LocalOutlierFactorApplicabilityDomain(n_neighbors=20) + ad_small = LocalOutlierFactorApplicabilityDomain(n_neighbors=2) + ad_large = LocalOutlierFactorApplicabilityDomain(n_neighbors=5) ad_small.fit(X) ad_large.fit(X) @@ -51,8 +51,8 @@ def test_contamination_effect(): X = np.random.randn(100, 2) # Compare different contamination levels - ad_low = LocalOutlierFactorApplicabilityDomain(contamination=0.1) - ad_high = LocalOutlierFactorApplicabilityDomain(contamination=0.2) + ad_low = LocalOutlierFactorApplicabilityDomain(contamination=0.05) + ad_high = LocalOutlierFactorApplicabilityDomain(contamination=0.25) ad_low.fit(X) ad_high.fit(X) diff --git a/tests/test_adapters.py b/tests/test_adapters.py deleted file mode 100644 index 340c986..0000000 --- a/tests/test_adapters.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Tests for EstimatorUnion adapter.""" - -import numpy as np -import pandas as pd -import pytest -from sklearn.exceptions import NotFittedError -from sklearn.preprocessing import StandardScaler - -from scikit_mol.adapters import EstimatorUnion -from scikit_mol.applicability import ( - MahalanobisApplicabilityDomain, - StandardizationApplicabilityDomain, -) - -# Use existing fixtures -from .fixtures import ( - atompair_transformer, - mols_list, - morgan_transformer, - skip_pandas_output_test, -) - - -def test_estimator_union_basic(morgan_transformer, atompair_transformer): - """Test basic functionality of EstimatorUnion.""" - - union = EstimatorUnion( - [ - ("fp1", morgan_transformer), - ( - "fp2", - atompair_transformer, - ), # Different radius for different features - ] - ) - - # Test unfitted raises exception - with pytest.raises(NotFittedError): - union.transform(mols_list) - - # Test fit and transform - union.fit(mols_list) - features = union.transform(mols_list) - - # Check output shape - n_fp = morgan_transformer().fpSize - assert features.shape == (len(mols_list), n_fp * 2) - - -def test_estimator_union_with_ad(morgan_transformer): - """Test EstimatorUnion with applicability domain estimator.""" - union = EstimatorUnion( - [ - ("fp", morgan_transformer), - ("ad", MahalanobisApplicabilityDomain()), - ], - method_resolution_order=("transform", "transform_score"), - ) - - union.fit(mols_list) - features = union.transform(mols_list) - - # Check output shape (fingerprints + 1 AD score) - n_fp = morgan_transformer().fpSize - assert features.shape == (len(mols_list), n_fp + 1) - - -def test_estimator_union_mixed_methods(morgan_transformer): - """Test EstimatorUnion with different methods specified.""" - union = EstimatorUnion( - [ - ("scale", StandardScaler(), "transform"), - ("ad", StandardizationApplicabilityDomain(), "transform_score"), - ("fp", morgan_transformer, "transform"), - ] - ) - - # Create some numeric data for StandardScaler - X = morgan_transformer.fit_transform(mols_list) - union.fit(X) - features = union.transform(X) - - # Check output shape - assert features.shape[0] == len(X) - assert features.shape[1] == X.shape[1] * 2 + 1 # scaled + fp + 1 AD score - - -@skip_pandas_output_test -def test_estimator_union_pandas_output(pandas_output, morgan_transformer): - """Test pandas DataFrame output from EstimatorUnion.""" - union = EstimatorUnion( - [ - ("fp", morgan_transformer), - ("ad", MahalanobisApplicabilityDomain(), "transform_score"), - ] - ) - - union.fit(mols_list) - features = union.transform(mols_list) - - # Check output type and structure - assert isinstance(features, pd.DataFrame) - assert len(features) == len(mols_list) - - # Check column names - fp_cols = [f"fp_{i}" for i in range(morgan_transformer.fpSize)] - expected_cols = fp_cols + ["Mahalanobis"] - assert features.columns.tolist() == expected_cols - - -def test_estimator_union_invalid_method(morgan_transformer): - """Test EstimatorUnion with invalid method specification.""" - with pytest.raises(ValueError): - EstimatorUnion([("fp", morgan_transformer, "invalid_method")]) - - -def test_estimator_union_get_feature_names_out(morgan_transformer): - """Test feature names output from EstimatorUnion.""" - union = EstimatorUnion( - [ - ("fp", morgan_transformer), - ("ad", MahalanobisApplicabilityDomain(), "transform_score"), - ] - ) - - union.fit(mols_list) - feature_names = union.get_feature_names_out() - - # Check number and format of feature names - n_fp = morgan_transformer().fpSize - assert len(feature_names) == n_fp + 1 - assert all(name.startswith("fp_") for name in feature_names[:-1]) - assert feature_names[-1] == "Mahalanobis" - - -def test_estimator_union_partial_fit(morgan_transformer): - """Test EstimatorUnion with some estimators already fitted.""" - fp = morgan_transformer.fit(mols_list) - ad = MahalanobisApplicabilityDomain() - - union = EstimatorUnion([("fp", fp), ("ad", ad, "transform_score")]) - - # Should work since fp is already fitted - features = union.fit_transform(mols_list) - assert features.shape == (len(mols_list), fp.fpSize + 1) diff --git a/tests/test_desctransformer.py b/tests/test_desctransformer.py index ed6c1a9..4f2dae9 100644 --- a/tests/test_desctransformer.py +++ b/tests/test_desctransformer.py @@ -6,15 +6,6 @@ import pandas as pd import pytest import sklearn -from fixtures import ( - mols_container, - mols_list, - mols_with_invalid_container, - skip_pandas_output_test, - smiles_container, - smiles_list, - smiles_list_with_invalid, -) from packaging.version import Version from rdkit.Chem import Descriptors from sklearn import clone @@ -24,6 +15,16 @@ from scikit_mol.core import SKLEARN_VERSION_PANDAS_OUT from scikit_mol.descriptors import MolecularDescriptorTransformer +from .fixtures import ( + mols_container, + mols_list, + mols_with_invalid_container, + skip_pandas_output_test, + smiles_container, + smiles_list, + smiles_list_with_invalid, +) + @pytest.fixture def default_descriptor_transformer(): diff --git a/tests/test_fptransformers.py b/tests/test_fptransformers.py index 23b734d..25bd64d 100644 --- a/tests/test_fptransformers.py +++ b/tests/test_fptransformers.py @@ -4,7 +4,17 @@ import numpy as np import pandas as pd import pytest -from fixtures import ( +from rdkit import Chem +from sklearn import clone + +from scikit_mol.fingerprints import ( + AvalonFingerprintTransformer, + MACCSKeysFingerprintTransformer, + MHFingerprintTransformer, + SECFingerprintTransformer, +) + +from .fixtures import ( chiral_mols_list, chiral_smiles_list, fingerprint, @@ -15,15 +25,6 @@ smiles_list, smiles_list_with_invalid, ) -from rdkit import Chem -from sklearn import clone - -from scikit_mol.fingerprints import ( - AvalonFingerprintTransformer, - MACCSKeysFingerprintTransformer, - MHFingerprintTransformer, - SECFingerprintTransformer, -) @pytest.fixture diff --git a/tests/test_fptransformersgenerator.py b/tests/test_fptransformersgenerator.py index 905ce27..ef94342 100644 --- a/tests/test_fptransformersgenerator.py +++ b/tests/test_fptransformersgenerator.py @@ -3,15 +3,6 @@ import numpy as np import pytest -from fixtures import ( - chiral_mols_list, - chiral_smiles_list, - fingerprint, - mols_container, - mols_list, - smiles_container, - smiles_list, -) from sklearn import clone from scikit_mol.fingerprints import ( @@ -21,6 +12,16 @@ TopologicalTorsionFingerprintTransformer, ) +from .fixtures import ( + chiral_mols_list, + chiral_smiles_list, + fingerprint, + mols_container, + mols_list, + smiles_container, + smiles_list, +) + test_transformers = [ AtomPairFingerprintTransformer, MorganFingerprintTransformer, diff --git a/tests/test_metrics.py b/tests/test_metrics.py deleted file mode 100644 index 81b8773..0000000 --- a/tests/test_metrics.py +++ /dev/null @@ -1,91 +0,0 @@ -import numpy as np -import pytest - -from scikit_mol.metrics import tanimoto_distance - -from .applicability.conftest import binary_fingerprints - - -@pytest.fixture -def simple_fingerprints(): - """Create simple binary fingerprints for testing.""" - return np.array( - [ - [1, 1, 0, 0], # fp0: 2 bits set - [1, 0, 1, 0], # fp1: 2 bits set, 1 in common with fp1 - [0, 0, 1, 1], # fp2: 2 bits set, 1 in common with fp2, none with fp1 - [1, 1, 1, 1], # fp3: all bits set - [0, 0, 0, 0], # fp4: no bits set - ], - dtype=bool, - ) - - -def test_tanimoto_distance_basic(simple_fingerprints): - """Test basic properties of Tanimoto distance.""" - distances = tanimoto_distance(simple_fingerprints[0], simple_fingerprints[1]) - - # Check distance range [0,1] - assert 0 <= distances <= 1 - - # Check specific distances - # fp0 vs fp1: 1 bit in common, 3 in union -> distance = 2/3 - assert np.isclose(distances, 2 / 3) - # fp0 vs fp2: no bits in common, 4 in union -> distance = 1 - assert np.isclose( - tanimoto_distance(simple_fingerprints[0], simple_fingerprints[2]), 1.0 - ) - # fp0 vs fp3: 2 bits in common, 4 in union -> distance = 0.5 - assert np.isclose( - tanimoto_distance(simple_fingerprints[0], simple_fingerprints[3]), 0.5 - ) - # fp0 vs fp4: no bits in common, 2 in union -> distance = 1 - assert np.isclose( - tanimoto_distance(simple_fingerprints[0], simple_fingerprints[4]), 1.0 - ) - - -def test_tanimoto_distance_edge_cases(simple_fingerprints): - """Test edge cases for Tanimoto distance.""" - empty = simple_fingerprints[4] # Empty fingerprint - full = simple_fingerprints[3] # Full fingerprint - - # Two empty fingerprints (fp4) - dist = tanimoto_distance(empty, empty) - # No bits in common, 0 in union -> distance = 0/0 = 0 in our implementation. - assert np.isclose(dist, 0.0) - - # Empty vs full fingerprint (fp3) - dist = tanimoto_distance(empty, full) - assert np.isclose(dist, 1.0) # No overlap -> maximum distance - - -# TODO, can rdkit speed things up? But not working with np.arrays -# def test_tanimoto_implementations_equivalent(simple_fingerprints): -# """Test that both implementations give equivalent results.""" -# X = simple_fingerprints[:2] -# Y = simple_fingerprints[2:4] - -# dist1 = tanimoto_distance(X, Y) -# dist2 = tanimoto_distance_rdkit(X, Y) - -# assert np.allclose(dist1, dist2) - - -# def test_tanimoto_distance_rdkit_basic(binary_fingerprints): -# """Test basic properties of RDKit-based Tanimoto distance.""" -# # Get a subset of fingerprints for testing -# X = binary_fingerprints[:3] -# Y = binary_fingerprints[3:6] - -# distances = tanimoto_distance_rdkit(X, Y) - -# # Check output shape -# assert distances.shape == (3, 3) - -# # Check distance range [0,1] -# assert np.all((0 <= distances) & (distances <= 1)) - -# # Check self-distance is 0 for identical fingerprints -# self_distances = tanimoto_distance_rdkit(X, X) -# assert np.allclose(np.diag(self_distances), 0) diff --git a/tests/test_parameter_types.py b/tests/test_parameter_types.py index 4b73959..15e4855 100644 --- a/tests/test_parameter_types.py +++ b/tests/test_parameter_types.py @@ -1,15 +1,16 @@ -import pytest import numpy as np +import pytest from rdkit import Chem -from fixtures import ( + +from .fixtures import ( + atompair_transformer, mols_list, - smiles_list, morgan_transformer, - atompair_transformer, - topologicaltorsion_transformer, rdkit_transformer, + smiles_list, + topologicaltorsion_transformer, ) -from test_fptransformers import ( +from .test_fptransformers import ( avalon_transformer, ) diff --git a/tests/test_safeinferencemode.py b/tests/test_safeinferencemode.py index 60f8d1f..615694d 100644 --- a/tests/test_safeinferencemode.py +++ b/tests/test_safeinferencemode.py @@ -1,12 +1,6 @@ import numpy as np import pandas as pd import pytest -from fixtures import ( - SLC6A4_subset, - invalid_smiles_list, - skip_pandas_output_test, - smiles_list, -) from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline @@ -15,6 +9,13 @@ from scikit_mol.safeinference import SafeInferenceWrapper from scikit_mol.utilities import set_safe_inference_mode +from .fixtures import ( + SLC6A4_subset, + invalid_smiles_list, + skip_pandas_output_test, + smiles_list, +) + def equal_val(value, expected_value): try: diff --git a/tests/test_sanitizer.py b/tests/test_sanitizer.py index 9dd09cc..05c334c 100644 --- a/tests/test_sanitizer.py +++ b/tests/test_sanitizer.py @@ -1,10 +1,12 @@ -import pytest import numpy as np import pandas as pd +import pytest from rdkit import Chem -from fixtures import smiles_list, smiles_list_with_invalid + from scikit_mol.utilities import CheckSmilesSanitazion +from .fixtures import smiles_list, smiles_list_with_invalid + @pytest.fixture def sanitizer(): @@ -23,7 +25,9 @@ def test_checksmilessanitation(smiles_list, smiles_list_with_invalid, sanitizer) assert errors[0] == sanitizer.errors.SMILES[0] -def test_checksmilessanitation_x_and_y(smiles_list, smiles_list_with_invalid, sanitizer): +def test_checksmilessanitation_x_and_y( + smiles_list, smiles_list_with_invalid, sanitizer +): smiles_list_sanitized, y_sanitized, errors, y_errors = sanitizer.sanitize( smiles_list_with_invalid, list(range(len(smiles_list_with_invalid))) ) @@ -36,14 +40,18 @@ def test_checksmilessanitation_x_and_y(smiles_list, smiles_list_with_invalid, sa def test_checksmilessanitation_np(smiles_list, smiles_list_with_invalid, sanitizer): - smiles_list_sanitized, errors = sanitizer.sanitize(np.array(smiles_list_with_invalid)) + smiles_list_sanitized, errors = sanitizer.sanitize( + np.array(smiles_list_with_invalid) + ) assert len(smiles_list_with_invalid) > len(smiles_list_sanitized) assert all([a == b for a, b in zip(smiles_list, smiles_list_sanitized)]) assert errors[0] == sanitizer.errors.SMILES[0] def test_checksmilessanitation_numpy(smiles_list, smiles_list_with_invalid, sanitizer): - smiles_list_sanitized, errors = sanitizer.sanitize(pd.Series(smiles_list_with_invalid)) + smiles_list_sanitized, errors = sanitizer.sanitize( + pd.Series(smiles_list_with_invalid) + ) assert len(smiles_list_with_invalid) > len(smiles_list_sanitized) assert all([a == b for a, b in zip(smiles_list, smiles_list_sanitized)]) assert errors[0] == sanitizer.errors.SMILES[0] @@ -52,7 +60,9 @@ def test_checksmilessanitation_numpy(smiles_list, smiles_list_with_invalid, sani def test_checksmilessanitation_return_mol( smiles_list, smiles_list_with_invalid, return_mol_sanitizer ): - smiles_list_sanitized, errors = return_mol_sanitizer.sanitize(smiles_list_with_invalid) + smiles_list_sanitized, errors = return_mol_sanitizer.sanitize( + smiles_list_with_invalid + ) assert len(smiles_list_with_invalid) > len(smiles_list_sanitized) assert all( [ diff --git a/tests/test_smilestomol.py b/tests/test_smilestomol.py index 2bb5f0f..0f53fa6 100644 --- a/tests/test_smilestomol.py +++ b/tests/test_smilestomol.py @@ -2,12 +2,6 @@ import pandas as pd import pytest import sklearn -from fixtures import ( - skip_pandas_output_test, - smiles_container, - smiles_list, - smiles_list_with_invalid, -) from packaging.version import Version from rdkit import Chem from sklearn import clone @@ -19,6 +13,13 @@ InvalidMol, ) +from .fixtures import ( + skip_pandas_output_test, + smiles_container, + smiles_list, + smiles_list_with_invalid, +) + @pytest.fixture def smilestomol_transformer(): diff --git a/tests/test_transformers.py b/tests/test_transformers.py index a47b8bf..633a3f0 100644 --- a/tests/test_transformers.py +++ b/tests/test_transformers.py @@ -6,36 +6,36 @@ # pytest tests/test_transformers.py --> tests/test_transformers.py::test_transformer PASSED -import pytest +import numpy as np import pandas as pd -from packaging.version import Version +import pytest import sklearn -import numpy as np -from sklearn.pipeline import Pipeline +from packaging.version import Version from sklearn.ensemble import RandomForestRegressor +from sklearn.pipeline import Pipeline + from scikit_mol.conversions import SmilesToMolTransformer from scikit_mol.core import SKLEARN_VERSION_PANDAS_OUT +from scikit_mol.descriptors import MolecularDescriptorTransformer from scikit_mol.fingerprints import ( - MACCSKeysFingerprintTransformer, - RDKitFingerprintTransformer, AtomPairFingerprintTransformer, - TopologicalTorsionFingerprintTransformer, + AvalonFingerprintTransformer, + MACCSKeysFingerprintTransformer, + MHFingerprintTransformer, MorganFingerprintTransformer, + RDKitFingerprintTransformer, SECFingerprintTransformer, - MHFingerprintTransformer, - AvalonFingerprintTransformer, + TopologicalTorsionFingerprintTransformer, ) from scikit_mol.fingerprints.baseclasses import BaseFpsTransformer -from scikit_mol.descriptors import MolecularDescriptorTransformer - -from fixtures import ( +from .fixtures import ( SLC6A4_subset, SLC6A4_subset_with_cddd, - skip_pandas_output_test, - mols_container, - featurizer, combined_transformer, + featurizer, + mols_container, + skip_pandas_output_test, ) From 64bc1179bc7900649a8bb11ef46c5c93e274c535 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Mar 2025 11:17:19 +0100 Subject: [PATCH 18/24] Setting numpy random seed automatically for consistent testing behaviour. --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index ee916ec..c1ca602 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from urllib.parse import urlsplit from urllib.request import urlopen +import numpy as np import pandas as pd import pytest import sklearn @@ -60,3 +61,10 @@ def pandas_output(): sklearn.set_config(transform_output="pandas") yield sklearn.set_config(transform_output="default") + + +# Fixed Numpy random seed in all tests automatically +@pytest.fixture(autouse=True) +def setup_random(): + """Set fixed random seed before each test.""" + np.random.seed(0xDEADFACE) From e8a4c7b45478c65a2bb5f349637b5ac4d570ada4 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sat, 8 Mar 2025 11:52:31 +0100 Subject: [PATCH 19/24] Adding a link for the new notebook --- README.md | 1 + docs/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 1cc0006..0707d61 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Example notebooks and API documentation are now hosted on [https://scikit-mol.re - [Testing different fingerprints as part of the hyperparameter optimization](https://scikit-mol.readthedocs.io/en/latest/notebooks/09_Combinatorial_Method_Usage_with_FingerPrint_Transformers/) - [Using pandas output for easy feature importance analysis and combine pre-exisitng values with new computations](https://scikit-mol.readthedocs.io/en/latest/notebooks/10_pipeline_pandas_output/) - [Working with pipelines and estimators in safe inference mode for handling prediction on batches with invalid smiles or molecules](https://scikit-mol.readthedocs.io/en/latest/notebooks/11_safe_inference/) +- [Estimating applicability domain using feature based estimators](https://scikit-mol.readthedocs.io/en/latest/notebooks/11_safe_inference/12_applicability_domain/) We also put a software note on ChemRxiv. [https://doi.org/10.26434/chemrxiv-2023-fzqwd](https://doi.org/10.26434/chemrxiv-2023-fzqwd) diff --git a/docs/index.md b/docs/index.md index 352cbf8..6c1e065 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,6 +65,7 @@ Example notebooks and API documentation are now hosted on [https://scikit-mol.re - [Testing different fingerprints as part of the hyperparameter optimization](https://scikit-mol.readthedocs.io/en/latest/notebooks/09_Combinatorial_Method_Usage_with_FingerPrint_Transformers/) - [Using pandas output for easy feature importance analysis and combine pre-exisitng values with new computations](https://scikit-mol.readthedocs.io/en/latest/notebooks/10_pipeline_pandas_output/) - [Working with pipelines and estimators in safe inference mode for handling prediction on batches with invalid smiles or molecules](https://scikit-mol.readthedocs.io/en/latest/notebooks/11_safe_inference/) +- [Estimating applicability domain using feature based estimators](https://scikit-mol.readthedocs.io/en/latest/notebooks/11_safe_inference/12_applicability_domain/) We also put a software note on ChemRxiv. [https://doi.org/10.26434/chemrxiv-2023-fzqwd](https://doi.org/10.26434/chemrxiv-2023-fzqwd) From 07a25d85cc39bed36b9ea3ea882b86d53bed4759 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sun, 6 Apr 2025 09:03:17 +0200 Subject: [PATCH 20/24] Fixing MD to proper links --- mkdocs.yml | 1 + scikit_mol/applicability/README.md | 4 ++-- scikit_mol/applicability/bounding_box.py | 2 +- scikit_mol/applicability/convex_hull.py | 2 +- scikit_mol/applicability/hotelling.py | 2 +- scikit_mol/applicability/isolation_forest.py | 2 +- scikit_mol/applicability/kernel_density.py | 2 +- scikit_mol/applicability/knn.py | 2 +- scikit_mol/applicability/leverage.py | 2 +- scikit_mol/applicability/local_outlier.py | 2 +- scikit_mol/applicability/standardization.py | 2 +- scikit_mol/applicability/topkat.py | 2 +- 12 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 5c44fa5..3ac50dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,5 +64,6 @@ nav: - Using pandas output for easy feature importance analysis and combine pre-existing values with new computations: notebooks/10_pipeline_pandas_output.ipynb - Working with pipelines and estimators in safe inference mode: notebooks/11_safe_inference.ipynb - Creating custom fingerptint transformers: notebooks/12_custom_fingerprint_transformer.ipynb + - Estimating applicability domain using feature based estimators: notebooks/13_applicability_domain.ipynb - Contributing: contributing.md \ No newline at end of file diff --git a/scikit_mol/applicability/README.md b/scikit_mol/applicability/README.md index 3fd2bfb..80e1786 100644 --- a/scikit_mol/applicability/README.md +++ b/scikit_mol/applicability/README.md @@ -4,13 +4,13 @@ This module contains applicability domain estimators for chemical modeling. ## License Information -Files in this module are licensed under LGPL as part of scikit-mol, with some files containing code adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD). +Files in this module are licensed under LGPL as part of scikit-mol, with some files containing code adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD). - Files containing the following header are adapted from MLChemAD (originally MIT licensed): ```python """ - This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) + This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/bounding_box.py b/scikit_mol/applicability/bounding_box.py index 9d8ec4a..8de2d7f 100644 --- a/scikit_mol/applicability/bounding_box.py +++ b/scikit_mol/applicability/bounding_box.py @@ -1,7 +1,7 @@ """ Bounding box applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/convex_hull.py b/scikit_mol/applicability/convex_hull.py index 9b7a486..df68d93 100644 --- a/scikit_mol/applicability/convex_hull.py +++ b/scikit_mol/applicability/convex_hull.py @@ -1,7 +1,7 @@ """ Convex hull applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/hotelling.py b/scikit_mol/applicability/hotelling.py index 2f9b6b8..8d1d8a0 100644 --- a/scikit_mol/applicability/hotelling.py +++ b/scikit_mol/applicability/hotelling.py @@ -1,7 +1,7 @@ """ Hotelling T² applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/isolation_forest.py b/scikit_mol/applicability/isolation_forest.py index ee29659..5a827cc 100644 --- a/scikit_mol/applicability/isolation_forest.py +++ b/scikit_mol/applicability/isolation_forest.py @@ -1,7 +1,7 @@ """ Isolation Forest applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/kernel_density.py b/scikit_mol/applicability/kernel_density.py index c32090c..88d24ac 100644 --- a/scikit_mol/applicability/kernel_density.py +++ b/scikit_mol/applicability/kernel_density.py @@ -1,7 +1,7 @@ """ Kernel Density applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD)Chem Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/knn.py b/scikit_mol/applicability/knn.py index c9be686..4cee9c8 100644 --- a/scikit_mol/applicability/knn.py +++ b/scikit_mol/applicability/knn.py @@ -1,7 +1,7 @@ """ K-Nearest Neighbors applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/leverage.py b/scikit_mol/applicability/leverage.py index d5a9e01..65c4150 100644 --- a/scikit_mol/applicability/leverage.py +++ b/scikit_mol/applicability/leverage.py @@ -1,7 +1,7 @@ """ Leverage-based applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) as described in the README.md file. +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/local_outlier.py b/scikit_mol/applicability/local_outlier.py index 5dfb055..f4ad937 100644 --- a/scikit_mol/applicability/local_outlier.py +++ b/scikit_mol/applicability/local_outlier.py @@ -1,7 +1,7 @@ """ Local Outlier Factor applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/standardization.py b/scikit_mol/applicability/standardization.py index babeeba..cd9cd69 100644 --- a/scikit_mol/applicability/standardization.py +++ b/scikit_mol/applicability/standardization.py @@ -1,7 +1,7 @@ """ Standardization approach applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. diff --git a/scikit_mol/applicability/topkat.py b/scikit_mol/applicability/topkat.py index 4849fc3..1725884 100644 --- a/scikit_mol/applicability/topkat.py +++ b/scikit_mol/applicability/topkat.py @@ -1,7 +1,7 @@ """ TOPKAT's Optimal Prediction Space (OPS) applicability domain. -This module was adapted from MLChemAD (https://github.com/OlivierBeq/MLChemAD) +This module was adapted from [MLChemAD](https://github.com/OlivierBeq/MLChemAD) Original work Copyright (c) 2023 Olivier J. M. Béquignon (MIT License) Modifications Copyright (c) 2025 scikit-mol contributors (LGPL License) See LICENSE.MIT in this directory for the original MIT license. From 0b804a47fce4e07f85edd5986c98123608a22f31 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sun, 6 Apr 2025 09:05:50 +0200 Subject: [PATCH 21/24] Adding to docs --- docs/api/scikit_mol.applicability.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/api/scikit_mol.applicability.md diff --git a/docs/api/scikit_mol.applicability.md b/docs/api/scikit_mol.applicability.md new file mode 100644 index 0000000..69d1c82 --- /dev/null +++ b/docs/api/scikit_mol.applicability.md @@ -0,0 +1,5 @@ +# `scikit-mol.applicability` + +::: scikit_mol.applicability + + From 6c95acc005eb21b5a53916319a132ac72c9044b8 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sun, 6 Apr 2025 09:14:02 +0200 Subject: [PATCH 22/24] Adding baseapplicability to include into docs, and deleing outcommented code --- scikit_mol/applicability/__init__.py | 1 + scikit_mol/applicability/kernel_density.py | 35 ---------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/scikit_mol/applicability/__init__.py b/scikit_mol/applicability/__init__.py index 003c3be..55f9265 100644 --- a/scikit_mol/applicability/__init__.py +++ b/scikit_mol/applicability/__init__.py @@ -1,3 +1,4 @@ +from .base import BaseApplicabilityDomain # noqa: F401 from .bounding_box import BoundingBoxApplicabilityDomain from .convex_hull import ConvexHullApplicabilityDomain from .hotelling import HotellingT2ApplicabilityDomain diff --git a/scikit_mol/applicability/kernel_density.py b/scikit_mol/applicability/kernel_density.py index 88d24ac..c3a2eca 100644 --- a/scikit_mol/applicability/kernel_density.py +++ b/scikit_mol/applicability/kernel_density.py @@ -117,38 +117,3 @@ def _transform(self, X: NDArray) -> NDArray[np.float64]: """ scores = self.kde_.score_samples(X) return scores.reshape(-1, 1) - - # def predict(self, X): - # """Predict whether samples are within the applicability domain. - - # Parameters - # ---------- - # X : array-like of shape (n_samples, n_features) - # The samples to predict. - - # Returns - # ------- - # y_pred : ndarray of shape (n_samples,) - # Returns 1 for samples inside the domain and -1 for samples outside - # (following scikit-learn's convention for outlier detection). - # """ - # scores = self._transform(X).ravel() - # return np.where(scores >= self.threshold_, 1, -1) - - # def fit_threshold(self, X): - # """Update the threshold using new data without refitting the model. - - # Parameters - # ---------- - # X : array-like of shape (n_samples, n_features) - # Data to compute threshold from. - - # Returns - # ------- - # self : object - # Returns the instance itself. - # """ - # densities = self._transform(X).ravel() - # self.threshold_ = np.percentile(densities, self.percentile) - - # return self From 14758c182367eccde1a7973c3552fee597f85405 Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sun, 6 Apr 2025 09:54:07 +0200 Subject: [PATCH 23/24] Fixes to mkdocs references --- docs/api/scikit_mol.applicability.md | 1 + mkdocs.yml | 1 + scikit_mol/applicability/base.py | 8 ++++---- scikit_mol/applicability/convex_hull.py | 17 ----------------- scikit_mol/applicability/knn.py | 8 +++++--- scikit_mol/descriptors.py | 2 +- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/docs/api/scikit_mol.applicability.md b/docs/api/scikit_mol.applicability.md index 69d1c82..07da383 100644 --- a/docs/api/scikit_mol.applicability.md +++ b/docs/api/scikit_mol.applicability.md @@ -1,5 +1,6 @@ # `scikit-mol.applicability` +::: scikit_mol.applicability.base ::: scikit_mol.applicability diff --git a/mkdocs.yml b/mkdocs.yml index 3ac50dd..7807873 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,6 +41,7 @@ plugins: nav: - Overview: index.md - API: + - scikit-mol.applicability: api/scikit_mol.applicability.md - scikit-mol.core: api/scikit_mol.core.md - scikit-mol.conversion: api/scikit_mol.conversions.md - scikit-mol.descriptors: api/scikit_mol.descriptors.md diff --git a/scikit_mol/applicability/base.py b/scikit_mol/applicability/base.py index 813a8b1..8fb0c2b 100644 --- a/scikit_mol/applicability/base.py +++ b/scikit_mol/applicability/base.py @@ -60,12 +60,12 @@ class BaseApplicabilityDomain(BaseEstimator, TransformerMixin, _ADOutputMixin, A Notes ----- - Subclasses must define _scoring_convention as either: + Subclasses must define `_scoring_convention` as either: - 'high_outside': Higher scores indicate samples outside domain (e.g., distances) - 'high_inside': Higher scores indicate samples inside domain (e.g., likelihoods) - The raw scores from transform() should maintain their natural interpretation, - while predict() will handle the conversion to ensure consistent output + The raw scores from `.transform()` should maintain their natural interpretation, + while `.predict()` will handle the conversion to ensure consistent output (1 = inside domain, -1 = outside domain). Attributes @@ -171,7 +171,7 @@ def transform( Returns ------- scores : ndarray or pandas DataFrame - Method-specific scores. Interpretation depends on _scoring_convention: + Method-specific scores. Interpretation depends on `_scoring_convention`: - 'high_outside': Higher scores indicate samples further from training data - 'high_inside': Higher scores indicate samples closer to training data Shape (n_samples, 1). diff --git a/scikit_mol/applicability/convex_hull.py b/scikit_mol/applicability/convex_hull.py index df68d93..7e2a1b3 100644 --- a/scikit_mol/applicability/convex_hull.py +++ b/scikit_mol/applicability/convex_hull.py @@ -114,20 +114,3 @@ def _transform(self, X: NDArray) -> NDArray[np.float64]: distances.append(0.0 if result.success else 1.0) return np.array(distances).reshape(-1, 1) - - # def predict(self, X): - # """Predict whether samples are within the applicability domain. - - # Parameters - # ---------- - # X : array-like of shape (n_samples, n_features) - # The samples to predict. - - # Returns - # ------- - # y_pred : ndarray of shape (n_samples,) - # Returns 1 for samples inside the domain and -1 for samples outside - # (following scikit-learn's convention for outlier detection). - # """ - # scores = self._transform(X).ravel() - # return np.where(scores == 0, 1, -1) diff --git a/scikit_mol/applicability/knn.py b/scikit_mol/applicability/knn.py index 4cee9c8..e897756 100644 --- a/scikit_mol/applicability/knn.py +++ b/scikit_mol/applicability/knn.py @@ -7,7 +7,7 @@ See LICENSE.MIT in this directory for the original MIT license. """ -from typing import Callable, ClassVar, Optional, Union +from typing import Callable, ClassVar, Literal, Optional, Union import numpy as np from numpy.typing import ArrayLike @@ -32,7 +32,7 @@ class KNNApplicabilityDomain(BaseApplicabilityDomain): Percentile of training set distances to use as threshold (0-100). If None, uses 99.0 (include 99% of training samples). distance_metric : str or callable, default='euclidean' - Distance metric to use. Options: + Distance metric to use. As examples: - 'euclidean': Euclidean distance (default) - 'manhattan': Manhattan distance - 'cosine': Cosine distance @@ -95,7 +95,9 @@ def __init__( self, n_neighbors: int = 5, percentile: Optional[float] = None, - distance_metric: Union[str, Callable] = "euclidean", + distance_metric: Union[ + Literal["euclidean", "manhattan", "cosine", "tanimoto", "jaccard"], Callable + ] = "euclidean", n_jobs: Optional[int] = None, feature_name: str = "KNN", ) -> None: diff --git a/scikit_mol/descriptors.py b/scikit_mol/descriptors.py index ee1d496..c115377 100644 --- a/scikit_mol/descriptors.py +++ b/scikit_mol/descriptors.py @@ -133,7 +133,7 @@ def transform(self, x: List[Mol], y=None) -> Union[np.ndarray, np.ma.MaskedArray """Transform a list of molecules into an array of descriptor values Parameters ---------- - X : (List, np.array, pd.Series) + x : (List, np.array, pd.Series) A list of RDKit molecules y : NoneType, optional Target values for scikit-learn compatibility, not used, by default None From b7c19d0dbdb6f5d0a524c4369fb15044351b8d7d Mon Sep 17 00:00:00 2001 From: Esben Jannik Bjerrum Date: Sun, 6 Apr 2025 10:17:59 +0200 Subject: [PATCH 24/24] Further updates to mkdocs configuration and some cleanup --- README.md | 2 +- docs/api/scikit_mol.applicability.md | 1 - scikit_mol/applicability/__init__.py | 3 ++- tests/applicability/test_base.py | 4 ---- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fdee10e..afa0ee4 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ There are more information about how to contribute to the project in [CONTRIBUTI Probably still, please check issues at GitHub and report there -## Contributors: +## Contributors Scikit-Mol has been developed as a community effort with contributions from people from many different companies, consortia, foundations and academic institutions. diff --git a/docs/api/scikit_mol.applicability.md b/docs/api/scikit_mol.applicability.md index 07da383..69d1c82 100644 --- a/docs/api/scikit_mol.applicability.md +++ b/docs/api/scikit_mol.applicability.md @@ -1,6 +1,5 @@ # `scikit-mol.applicability` -::: scikit_mol.applicability.base ::: scikit_mol.applicability diff --git a/scikit_mol/applicability/__init__.py b/scikit_mol/applicability/__init__.py index 55f9265..b1a1d3e 100644 --- a/scikit_mol/applicability/__init__.py +++ b/scikit_mol/applicability/__init__.py @@ -1,4 +1,4 @@ -from .base import BaseApplicabilityDomain # noqa: F401 +from .base import BaseApplicabilityDomain from .bounding_box import BoundingBoxApplicabilityDomain from .convex_hull import ConvexHullApplicabilityDomain from .hotelling import HotellingT2ApplicabilityDomain @@ -12,6 +12,7 @@ from .topkat import TopkatApplicabilityDomain __all__ = [ + "BaseApplicabilityDomain", "BoundingBoxApplicabilityDomain", "ConvexHullApplicabilityDomain", "HotellingT2ApplicabilityDomain", diff --git a/tests/applicability/test_base.py b/tests/applicability/test_base.py index 56eba50..24b6ba4 100644 --- a/tests/applicability/test_base.py +++ b/tests/applicability/test_base.py @@ -5,10 +5,6 @@ from numpy.testing import assert_array_almost_equal, assert_array_equal from sklearn.utils.estimator_checks import check_estimator -# def test_estimator_api(ad_estimator): -# """Test scikit-learn API compatibility.""" -# check_estimator(ad_estimator) - def test_basic_functionality(ad_estimator, reduced_fingerprints): """Test basic fit/transform on reduced fingerprints."""