qsarkit-learn 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qsarkit/__init__.py +134 -0
- qsarkit/applicability/__init__.py +57 -0
- qsarkit/applicability/_analyzer.py +317 -0
- qsarkit/applicability/_domains.py +1010 -0
- qsarkit/base/__init__.py +29 -0
- qsarkit/base/exceptions.py +67 -0
- qsarkit/base/optional_deps.py +76 -0
- qsarkit/base/transformer.py +215 -0
- qsarkit/chemistry/__init__.py +45 -0
- qsarkit/chemistry/fragments/__init__.py +6 -0
- qsarkit/chemistry/fragments/_core_extractor.py +111 -0
- qsarkit/chemistry/fragments/_remover.py +133 -0
- qsarkit/chemistry/glycans/__init__.py +7 -0
- qsarkit/chemistry/glycans/_descriptors.py +109 -0
- qsarkit/chemistry/glycans/_detector.py +194 -0
- qsarkit/chemistry/glycans/_remover.py +139 -0
- qsarkit/chemistry/graph/__init__.py +5 -0
- qsarkit/chemistry/graph/_graph.py +129 -0
- qsarkit/chemistry/standardization/__init__.py +5 -0
- qsarkit/chemistry/standardization/_standardizer.py +143 -0
- qsarkit/chemspace/__init__.py +41 -0
- qsarkit/chemspace/_analyzers.py +996 -0
- qsarkit/chemspace/_fingerprints.py +214 -0
- qsarkit/cluster/__init__.py +21 -0
- qsarkit/cluster/_butina.py +212 -0
- qsarkit/cluster/_pickers.py +384 -0
- qsarkit/data_quality/__init__.py +53 -0
- qsarkit/data_quality/_duplicates.py +398 -0
- qsarkit/data_quality/_pipeline.py +370 -0
- qsarkit/data_quality/_validators.py +600 -0
- qsarkit/explainability/__init__.py +80 -0
- qsarkit/explainability/_atom_maps.py +561 -0
- qsarkit/explainability/_atomic.py +545 -0
- qsarkit/explainability/_importance.py +672 -0
- qsarkit/feature_selection/__init__.py +38 -0
- qsarkit/feature_selection/_boruta.py +225 -0
- qsarkit/feature_selection/_correlation.py +194 -0
- qsarkit/feature_selection/_mutual_info.py +155 -0
- qsarkit/feature_selection/_rfe.py +133 -0
- qsarkit/feature_selection/_variance.py +97 -0
- qsarkit/functional/__init__.py +174 -0
- qsarkit/functional/_core.py +1127 -0
- qsarkit/functional/_model_steps.py +1310 -0
- qsarkit/functional/_steps.py +999 -0
- qsarkit/functional/_viz.py +546 -0
- qsarkit/metrics/__init__.py +123 -0
- qsarkit/metrics/_calibration.py +605 -0
- qsarkit/metrics/_classification.py +748 -0
- qsarkit/metrics/_common.py +79 -0
- qsarkit/metrics/_regression.py +949 -0
- qsarkit/metrics/_reports.py +167 -0
- qsarkit/metrics/_thresholds.py +528 -0
- qsarkit/model_selection/__init__.py +55 -0
- qsarkit/model_selection/_search.py +242 -0
- qsarkit/model_selection/_splitters.py +802 -0
- qsarkit/models/__init__.py +33 -0
- qsarkit/models/_baseline.py +198 -0
- qsarkit/models/_consensus.py +249 -0
- qsarkit/models/_facades.py +830 -0
- qsarkit/models/_gaussian_process.py +164 -0
- qsarkit/models/_neural_network.py +112 -0
- qsarkit/models/_pls.py +128 -0
- qsarkit/models/_random_forest.py +102 -0
- qsarkit/models/_svm.py +82 -0
- qsarkit/models/_tanimoto_kernel.py +187 -0
- qsarkit/neighbors/__init__.py +25 -0
- qsarkit/neighbors/_distance.py +212 -0
- qsarkit/neighbors/_knn.py +306 -0
- qsarkit/neighbors/_search.py +236 -0
- qsarkit/persistence/__init__.py +61 -0
- qsarkit/persistence/_bundle.py +735 -0
- qsarkit/persistence/_metadata.py +187 -0
- qsarkit/py.typed +0 -0
- qsarkit/reporting/__init__.py +66 -0
- qsarkit/reporting/_plots.py +979 -0
- qsarkit/reporting/_report.py +1015 -0
- qsarkit/representation/__init__.py +65 -0
- qsarkit/representation/descriptors/_3d.py +162 -0
- qsarkit/representation/descriptors/__init__.py +37 -0
- qsarkit/representation/descriptors/_base.py +125 -0
- qsarkit/representation/descriptors/_calculator.py +169 -0
- qsarkit/representation/descriptors/_constitutional.py +185 -0
- qsarkit/representation/descriptors/_fragments.py +78 -0
- qsarkit/representation/descriptors/_lipinski.py +139 -0
- qsarkit/representation/descriptors/_physicochemical.py +127 -0
- qsarkit/representation/descriptors/_rdkit_descriptors.py +78 -0
- qsarkit/representation/embeddings/__init__.py +16 -0
- qsarkit/representation/embeddings/_chemberta.py +89 -0
- qsarkit/representation/embeddings/_hf_base.py +187 -0
- qsarkit/representation/fingerprints/__init__.py +59 -0
- qsarkit/representation/fingerprints/_atompair.py +187 -0
- qsarkit/representation/fingerprints/_avalon.py +110 -0
- qsarkit/representation/fingerprints/_base.py +150 -0
- qsarkit/representation/fingerprints/_combiner.py +160 -0
- qsarkit/representation/fingerprints/_maccs.py +80 -0
- qsarkit/representation/fingerprints/_mhfp.py +454 -0
- qsarkit/representation/fingerprints/_morgan.py +180 -0
- qsarkit/representation/fingerprints/_pharmacophore.py +85 -0
- qsarkit/representation/fingerprints/_rdkit.py +232 -0
- qsarkit/representation/mol2vec/__init__.py +14 -0
- qsarkit/representation/mol2vec/_mol2vec.py +365 -0
- qsarkit/sar/__init__.py +45 -0
- qsarkit/sar/_cliffs.py +925 -0
- qsarkit/sar/_mmp.py +372 -0
- qsarkit/sar/_rgroup.py +490 -0
- qsarkit/transform/__init__.py +49 -0
- qsarkit/transform/_transforms.py +722 -0
- qsarkit/uncertainty/__init__.py +70 -0
- qsarkit/uncertainty/_calibration.py +308 -0
- qsarkit/uncertainty/_conformal.py +536 -0
- qsarkit/uncertainty/_estimators.py +554 -0
- qsarkit/utils/__init__.py +49 -0
- qsarkit/utils/constants.py +82 -0
- qsarkit/utils/io.py +624 -0
- qsarkit/utils/logging.py +134 -0
- qsarkit/utils/validation.py +360 -0
- qsarkit/validation/__init__.py +43 -0
- qsarkit/validation/_cross_validation.py +257 -0
- qsarkit/validation/_robustness.py +609 -0
- qsarkit/validation/_scoring.py +318 -0
- qsarkit_learn-0.5.0.dist-info/METADATA +408 -0
- qsarkit_learn-0.5.0.dist-info/RECORD +125 -0
- qsarkit_learn-0.5.0.dist-info/WHEEL +5 -0
- qsarkit_learn-0.5.0.dist-info/licenses/LICENSE +21 -0
- qsarkit_learn-0.5.0.dist-info/top_level.txt +1 -0
qsarkit/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""qsarkit - a focused, open-source Python library for QSAR modeling.
|
|
2
|
+
|
|
3
|
+
qsarkit covers the QSAR workflow proper: curating structures, turning them
|
|
4
|
+
into features, fitting and validating a model, defining where it applies,
|
|
5
|
+
and interpreting what it learned.
|
|
6
|
+
|
|
7
|
+
.. code-block:: text
|
|
8
|
+
|
|
9
|
+
structures + measured activities -> chemical curation
|
|
10
|
+
-> molecular representation -> QSAR modeling
|
|
11
|
+
-> validation -> applicability domain -> uncertainty
|
|
12
|
+
-> SAR interpretation -> reporting
|
|
13
|
+
|
|
14
|
+
It deliberately stops there. Literature mining, database retrieval,
|
|
15
|
+
docking and de-novo design are separate disciplines with separate failure
|
|
16
|
+
modes; bundling them would make the library broad rather than trustworthy.
|
|
17
|
+
|
|
18
|
+
Subpackages are imported lazily: ``import qsarkit`` is cheap, and the
|
|
19
|
+
heavy optional dependencies (PyTorch, transformers, gensim, ...) are only
|
|
20
|
+
loaded when you actually touch the feature that needs them.
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
--------
|
|
24
|
+
>>> import qsarkit
|
|
25
|
+
>>> from qsarkit.chemistry import MolecularStandardizer
|
|
26
|
+
>>> from rdkit import Chem
|
|
27
|
+
>>> standardizer = MolecularStandardizer()
|
|
28
|
+
>>> mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)[O-].[Na+]")
|
|
29
|
+
>>> Chem.MolToSmiles(standardizer.transform([mol])[0])
|
|
30
|
+
'CC(=O)Oc1ccccc1C(=O)O'
|
|
31
|
+
|
|
32
|
+
References
|
|
33
|
+
----------
|
|
34
|
+
- OECD (2007). "Guidance Document on the Validation of (Quantitative)
|
|
35
|
+
Structure-Activity Relationship [(Q)SAR] Models." OECD Series on
|
|
36
|
+
Testing and Assessment No. 69, ENV/JM/MONO(2007)2.
|
|
37
|
+
https://doi.org/10.1787/9789264085442-en
|
|
38
|
+
- RDKit: Open-source cheminformatics. https://www.rdkit.org
|
|
39
|
+
- Pedregosa, F. et al. (2011). "Scikit-learn: Machine Learning in
|
|
40
|
+
Python." J. Mach. Learn. Res., 12, 2825-2830.
|
|
41
|
+
https://jmlr.org/papers/v12/pedregosa11a.html
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import importlib
|
|
47
|
+
from typing import TYPE_CHECKING, Any, List
|
|
48
|
+
|
|
49
|
+
__version__ = "0.5.0"
|
|
50
|
+
|
|
51
|
+
# Every public subpackage. Kept explicit (rather than scanned from the
|
|
52
|
+
# filesystem) so that `dir(qsarkit)` and tab-completion are stable and
|
|
53
|
+
# a typo in a subpackage name fails loudly.
|
|
54
|
+
_SUBPACKAGES = (
|
|
55
|
+
"applicability",
|
|
56
|
+
"base",
|
|
57
|
+
"chemistry",
|
|
58
|
+
"chemspace",
|
|
59
|
+
"cluster",
|
|
60
|
+
"data_quality",
|
|
61
|
+
"explainability",
|
|
62
|
+
"feature_selection",
|
|
63
|
+
"functional",
|
|
64
|
+
"metrics",
|
|
65
|
+
"model_selection",
|
|
66
|
+
"models",
|
|
67
|
+
"neighbors",
|
|
68
|
+
"persistence",
|
|
69
|
+
"representation",
|
|
70
|
+
"reporting",
|
|
71
|
+
"sar",
|
|
72
|
+
"transform",
|
|
73
|
+
"uncertainty",
|
|
74
|
+
"utils",
|
|
75
|
+
"validation",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if TYPE_CHECKING: # pragma: no cover - import for type checkers only
|
|
79
|
+
from qsarkit import (
|
|
80
|
+
applicability,
|
|
81
|
+
base,
|
|
82
|
+
chemistry,
|
|
83
|
+
chemspace,
|
|
84
|
+
cluster,
|
|
85
|
+
data_quality,
|
|
86
|
+
explainability,
|
|
87
|
+
feature_selection,
|
|
88
|
+
functional,
|
|
89
|
+
metrics,
|
|
90
|
+
model_selection,
|
|
91
|
+
models,
|
|
92
|
+
neighbors,
|
|
93
|
+
persistence,
|
|
94
|
+
representation,
|
|
95
|
+
reporting,
|
|
96
|
+
sar,
|
|
97
|
+
transform,
|
|
98
|
+
uncertainty,
|
|
99
|
+
utils,
|
|
100
|
+
validation,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def __getattr__(name: str) -> Any:
|
|
105
|
+
"""Import a subpackage on first attribute access (PEP 562).
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
name : str
|
|
110
|
+
Attribute being looked up on the ``qsarkit`` module.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
module
|
|
115
|
+
The imported subpackage.
|
|
116
|
+
|
|
117
|
+
Raises
|
|
118
|
+
------
|
|
119
|
+
AttributeError
|
|
120
|
+
If ``name`` is not a qsarkit subpackage.
|
|
121
|
+
"""
|
|
122
|
+
if name in _SUBPACKAGES:
|
|
123
|
+
module = importlib.import_module(f"qsarkit.{name}")
|
|
124
|
+
globals()[name] = module
|
|
125
|
+
return module
|
|
126
|
+
raise AttributeError(f"module 'qsarkit' has no attribute {name!r}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def __dir__() -> List[str]:
|
|
130
|
+
"""Expose subpackages to ``dir()`` and interactive completion."""
|
|
131
|
+
return sorted([*globals().keys(), *_SUBPACKAGES])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = ["__version__", *_SUBPACKAGES]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Applicability domain estimation (OECD validation principle 3).
|
|
2
|
+
|
|
3
|
+
Every estimator shares one interface: ``fit(X)``, ``score_samples(X)``
|
|
4
|
+
(larger = further outside), ``predict(X)`` (True = inside) and
|
|
5
|
+
``decision_function(X)`` (positive = inside).
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
>>> import numpy as np
|
|
10
|
+
>>> from qsarkit.applicability import LeverageAD
|
|
11
|
+
>>> X = np.random.RandomState(0).normal(size=(50, 3))
|
|
12
|
+
>>> ad = LeverageAD().fit(X)
|
|
13
|
+
>>> bool(ad.predict(np.zeros((1, 3)))[0])
|
|
14
|
+
True
|
|
15
|
+
|
|
16
|
+
References
|
|
17
|
+
----------
|
|
18
|
+
- OECD (2007). "Guidance Document on the Validation of (Quantitative)
|
|
19
|
+
Structure-Activity Relationship [(Q)SAR] Models." OECD Series on
|
|
20
|
+
Testing and Assessment No. 69, ENV/JM/MONO(2007)2.
|
|
21
|
+
https://doi.org/10.1787/9789264085442-en
|
|
22
|
+
- Sahigara, F. et al. (2012). "Comparison of Different Approaches to
|
|
23
|
+
Define the Applicability Domain of QSAR Models." Molecules, 17(5),
|
|
24
|
+
4791-4810. https://doi.org/10.3390/molecules17054791
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from qsarkit.applicability._analyzer import ADAnalyzer
|
|
28
|
+
from qsarkit.applicability._domains import (
|
|
29
|
+
BaseApplicabilityDomain,
|
|
30
|
+
BoundingBoxAD,
|
|
31
|
+
ConvexHullAD,
|
|
32
|
+
DistanceToModelAD,
|
|
33
|
+
EnsembleAD,
|
|
34
|
+
IsolationForestAD,
|
|
35
|
+
KernelDensityAD,
|
|
36
|
+
KNNApplicabilityDomain,
|
|
37
|
+
LeverageAD,
|
|
38
|
+
PCABoundingBoxAD,
|
|
39
|
+
RangeAD,
|
|
40
|
+
TanimotoSimilarityAD,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"BaseApplicabilityDomain",
|
|
45
|
+
"LeverageAD",
|
|
46
|
+
"DistanceToModelAD",
|
|
47
|
+
"KNNApplicabilityDomain",
|
|
48
|
+
"RangeAD",
|
|
49
|
+
"BoundingBoxAD",
|
|
50
|
+
"PCABoundingBoxAD",
|
|
51
|
+
"ConvexHullAD",
|
|
52
|
+
"TanimotoSimilarityAD",
|
|
53
|
+
"KernelDensityAD",
|
|
54
|
+
"IsolationForestAD",
|
|
55
|
+
"EnsembleAD",
|
|
56
|
+
"ADAnalyzer",
|
|
57
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Applicability-domain reporting: coverage, accuracy-vs-coverage, Williams plot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
|
|
10
|
+
from qsarkit.applicability._domains import BaseApplicabilityDomain
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import plotly.graph_objects as go
|
|
15
|
+
|
|
16
|
+
__all__ = ["ADAnalyzer"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ADAnalyzer:
|
|
20
|
+
"""Quantify what an applicability domain buys you in prediction accuracy.
|
|
21
|
+
|
|
22
|
+
A domain definition is only useful if excluding the compounds it
|
|
23
|
+
rejects actually improves accuracy on the ones it keeps. This class
|
|
24
|
+
measures exactly that trade-off: as the domain is tightened, coverage
|
|
25
|
+
falls and error should fall with it. A domain whose accuracy curve is
|
|
26
|
+
flat is not carrying information, however statistically principled it
|
|
27
|
+
looks.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
domain : BaseApplicabilityDomain
|
|
32
|
+
A fitted (or fittable) applicability-domain estimator.
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> import numpy as np
|
|
37
|
+
>>> from qsarkit.applicability import KNNApplicabilityDomain
|
|
38
|
+
>>> rng = np.random.RandomState(0)
|
|
39
|
+
>>> X = rng.normal(size=(60, 3))
|
|
40
|
+
>>> analyzer = ADAnalyzer(KNNApplicabilityDomain(n_neighbors=3)).fit(X)
|
|
41
|
+
>>> 0.0 <= analyzer.coverage(X) <= 1.0
|
|
42
|
+
True
|
|
43
|
+
|
|
44
|
+
References
|
|
45
|
+
----------
|
|
46
|
+
- OECD (2007). "Guidance Document on the Validation of (Q)SAR
|
|
47
|
+
Models." OECD Series on Testing and Assessment No. 69,
|
|
48
|
+
ENV/JM/MONO(2007)2. https://doi.org/10.1787/9789264085442-en
|
|
49
|
+
- Dragos, H., Gilles, M. & Alexandre, V. (2009). "Predicting the
|
|
50
|
+
Predictability: A Unified Approach to the Applicability Domain
|
|
51
|
+
Problem of QSAR Models." J. Chem. Inf. Model., 49(7), 1762-1776.
|
|
52
|
+
https://doi.org/10.1021/ci9000579
|
|
53
|
+
- Sheridan, R. P. (2012). "Three Useful Dimensions for Domain
|
|
54
|
+
Applicability in QSAR Models Using Random Forest." J. Chem. Inf.
|
|
55
|
+
Model., 52(3), 814-823. https://doi.org/10.1021/ci300004n
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, domain: BaseApplicabilityDomain) -> None:
|
|
59
|
+
self.domain = domain
|
|
60
|
+
|
|
61
|
+
def fit(
|
|
62
|
+
self, X: npt.ArrayLike, y: Optional[npt.ArrayLike] = None
|
|
63
|
+
) -> "ADAnalyzer":
|
|
64
|
+
"""Fit the wrapped domain on training descriptors.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
X : array-like of shape (n_samples, n_features)
|
|
69
|
+
y : ignored
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
ADAnalyzer
|
|
74
|
+
"""
|
|
75
|
+
self.domain.fit(X, y)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def coverage(self, X: npt.ArrayLike) -> float:
|
|
79
|
+
"""Fraction of ``X`` inside the domain.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
X : array-like of shape (n_samples, n_features)
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
float
|
|
88
|
+
"""
|
|
89
|
+
return self.domain.coverage(X)
|
|
90
|
+
|
|
91
|
+
def report(
|
|
92
|
+
self,
|
|
93
|
+
X: npt.ArrayLike,
|
|
94
|
+
y_true: npt.ArrayLike,
|
|
95
|
+
y_pred: npt.ArrayLike,
|
|
96
|
+
) -> Dict[str, float]:
|
|
97
|
+
"""Compare in-domain and out-of-domain prediction error.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
X : array-like of shape (n_samples, n_features)
|
|
102
|
+
Descriptors of the evaluated set.
|
|
103
|
+
y_true : array-like of shape (n_samples,)
|
|
104
|
+
Observed values.
|
|
105
|
+
y_pred : array-like of shape (n_samples,)
|
|
106
|
+
Model predictions.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
dict
|
|
111
|
+
``coverage``, ``n_inside``, ``n_outside``, ``rmse_inside``,
|
|
112
|
+
``rmse_outside``, ``mae_inside``, ``mae_outside`` and
|
|
113
|
+
``rmse_ratio`` (outside/inside; > 1 means the domain is
|
|
114
|
+
doing its job). Error entries are ``nan`` when the
|
|
115
|
+
corresponding subset is empty.
|
|
116
|
+
"""
|
|
117
|
+
true = np.asarray(y_true, dtype=np.float64)
|
|
118
|
+
pred = np.asarray(y_pred, dtype=np.float64)
|
|
119
|
+
if true.shape != pred.shape:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"y_true has shape {true.shape} but y_pred has {pred.shape}."
|
|
122
|
+
)
|
|
123
|
+
inside = self.domain.predict(X)
|
|
124
|
+
if inside.shape[0] != true.shape[0]:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"X has {inside.shape[0]} samples but y_true has {true.shape[0]}."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _rmse(mask: npt.NDArray[np.bool_]) -> float:
|
|
130
|
+
if not mask.any():
|
|
131
|
+
return float("nan")
|
|
132
|
+
return float(np.sqrt(np.mean((true[mask] - pred[mask]) ** 2)))
|
|
133
|
+
|
|
134
|
+
def _mae(mask: npt.NDArray[np.bool_]) -> float:
|
|
135
|
+
if not mask.any():
|
|
136
|
+
return float("nan")
|
|
137
|
+
return float(np.mean(np.abs(true[mask] - pred[mask])))
|
|
138
|
+
|
|
139
|
+
rmse_in, rmse_out = _rmse(inside), _rmse(~inside)
|
|
140
|
+
return {
|
|
141
|
+
"coverage": float(np.mean(inside)),
|
|
142
|
+
"n_inside": int(inside.sum()),
|
|
143
|
+
"n_outside": int((~inside).sum()),
|
|
144
|
+
"rmse_inside": rmse_in,
|
|
145
|
+
"rmse_outside": rmse_out,
|
|
146
|
+
"mae_inside": _mae(inside),
|
|
147
|
+
"mae_outside": _mae(~inside),
|
|
148
|
+
"rmse_ratio": (
|
|
149
|
+
rmse_out / rmse_in if rmse_in and np.isfinite(rmse_out) else float("nan")
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def accuracy_vs_coverage(
|
|
154
|
+
self,
|
|
155
|
+
X: npt.ArrayLike,
|
|
156
|
+
y_true: npt.ArrayLike,
|
|
157
|
+
y_pred: npt.ArrayLike,
|
|
158
|
+
n_points: int = 20,
|
|
159
|
+
) -> "pd.DataFrame":
|
|
160
|
+
"""Trace prediction error as the domain is progressively tightened.
|
|
161
|
+
|
|
162
|
+
Compounds are ranked by how far outside the domain they score;
|
|
163
|
+
the curve then reports RMSE over the most-confident fraction at
|
|
164
|
+
a series of coverage levels. A useful domain gives a curve that
|
|
165
|
+
rises monotonically from left (strictest) to right (all compounds).
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
X : array-like of shape (n_samples, n_features)
|
|
170
|
+
y_true, y_pred : array-like of shape (n_samples,)
|
|
171
|
+
n_points : int, default 20
|
|
172
|
+
Number of coverage levels sampled.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
pandas.DataFrame
|
|
177
|
+
Columns ``coverage``, ``n_samples``, ``rmse``, ``mae``.
|
|
178
|
+
"""
|
|
179
|
+
import pandas as pd
|
|
180
|
+
|
|
181
|
+
true = np.asarray(y_true, dtype=np.float64)
|
|
182
|
+
pred = np.asarray(y_pred, dtype=np.float64)
|
|
183
|
+
scores = self.domain.score_samples(X)
|
|
184
|
+
if not (len(scores) == len(true) == len(pred)):
|
|
185
|
+
raise ValueError("X, y_true and y_pred must all have the same length.")
|
|
186
|
+
if n_points < 1:
|
|
187
|
+
raise ValueError(f"n_points must be positive, got {n_points}.")
|
|
188
|
+
|
|
189
|
+
order = np.argsort(scores, kind="stable")
|
|
190
|
+
n = len(order)
|
|
191
|
+
counts = np.unique(
|
|
192
|
+
np.clip(np.linspace(1, n, num=min(n_points, n)).astype(int), 1, n)
|
|
193
|
+
)
|
|
194
|
+
rows = []
|
|
195
|
+
for k in counts:
|
|
196
|
+
idx = order[:k]
|
|
197
|
+
residual = true[idx] - pred[idx]
|
|
198
|
+
rows.append(
|
|
199
|
+
{
|
|
200
|
+
"coverage": k / n,
|
|
201
|
+
"n_samples": int(k),
|
|
202
|
+
"rmse": float(np.sqrt(np.mean(residual**2))),
|
|
203
|
+
"mae": float(np.mean(np.abs(residual))),
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
return pd.DataFrame(rows, columns=["coverage", "n_samples", "rmse", "mae"])
|
|
207
|
+
|
|
208
|
+
def plot_accuracy_vs_coverage(
|
|
209
|
+
self,
|
|
210
|
+
X: npt.ArrayLike,
|
|
211
|
+
y_true: npt.ArrayLike,
|
|
212
|
+
y_pred: npt.ArrayLike,
|
|
213
|
+
n_points: int = 20,
|
|
214
|
+
) -> "go.Figure":
|
|
215
|
+
"""Plot the accuracy-vs-coverage curve.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
X : array-like of shape (n_samples, n_features)
|
|
220
|
+
y_true, y_pred : array-like of shape (n_samples,)
|
|
221
|
+
n_points : int, default 20
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
plotly.graph_objects.Figure
|
|
226
|
+
"""
|
|
227
|
+
import plotly.graph_objects as go
|
|
228
|
+
|
|
229
|
+
curve = self.accuracy_vs_coverage(X, y_true, y_pred, n_points)
|
|
230
|
+
fig = go.Figure()
|
|
231
|
+
fig.add_trace(
|
|
232
|
+
go.Scatter(
|
|
233
|
+
x=curve["coverage"], y=curve["rmse"], mode="lines+markers", name="RMSE"
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
fig.add_trace(
|
|
237
|
+
go.Scatter(
|
|
238
|
+
x=curve["coverage"], y=curve["mae"], mode="lines+markers", name="MAE"
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
fig.update_layout(
|
|
242
|
+
title=f"Accuracy vs coverage ({type(self.domain).__name__})",
|
|
243
|
+
xaxis_title="Coverage (fraction of compounds retained)",
|
|
244
|
+
yaxis_title="Prediction error",
|
|
245
|
+
)
|
|
246
|
+
return fig
|
|
247
|
+
|
|
248
|
+
def williams_plot(
|
|
249
|
+
self,
|
|
250
|
+
X: npt.ArrayLike,
|
|
251
|
+
y_true: npt.ArrayLike,
|
|
252
|
+
y_pred: npt.ArrayLike,
|
|
253
|
+
residual_limit: float = 3.0,
|
|
254
|
+
) -> "go.Figure":
|
|
255
|
+
"""Williams plot: standardized residuals against leverage.
|
|
256
|
+
|
|
257
|
+
The standard regulatory diagnostic. Points to the right of the
|
|
258
|
+
vertical ``h*`` line are structural outliers (the model is
|
|
259
|
+
extrapolating); points outside the horizontal +/-3 sigma lines are
|
|
260
|
+
response outliers (the model is wrong). Both together mark
|
|
261
|
+
predictions that should not be relied on.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
X : array-like of shape (n_samples, n_features)
|
|
266
|
+
y_true, y_pred : array-like of shape (n_samples,)
|
|
267
|
+
residual_limit : float, default 3.0
|
|
268
|
+
Standardized-residual warning level, in standard deviations.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
plotly.graph_objects.Figure
|
|
273
|
+
|
|
274
|
+
References
|
|
275
|
+
----------
|
|
276
|
+
- Gramatica, P. (2007). QSAR Comb. Sci., 26(5), 694-701.
|
|
277
|
+
https://doi.org/10.1002/qsar.200610151
|
|
278
|
+
- OECD (2007). Guidance Document No. 69, ENV/JM/MONO(2007)2.
|
|
279
|
+
https://doi.org/10.1787/9789264085442-en
|
|
280
|
+
"""
|
|
281
|
+
import plotly.graph_objects as go
|
|
282
|
+
|
|
283
|
+
from qsarkit.applicability._domains import LeverageAD
|
|
284
|
+
|
|
285
|
+
true = np.asarray(y_true, dtype=np.float64)
|
|
286
|
+
pred = np.asarray(y_pred, dtype=np.float64)
|
|
287
|
+
residual = true - pred
|
|
288
|
+
std = residual.std(ddof=1) if residual.size > 1 else 0.0
|
|
289
|
+
standardized = residual / std if std > 0 else np.zeros_like(residual)
|
|
290
|
+
|
|
291
|
+
leverage_ad = LeverageAD().fit(X)
|
|
292
|
+
leverage = leverage_ad.score_samples(X)
|
|
293
|
+
|
|
294
|
+
fig = go.Figure()
|
|
295
|
+
fig.add_trace(
|
|
296
|
+
go.Scatter(
|
|
297
|
+
x=leverage,
|
|
298
|
+
y=standardized,
|
|
299
|
+
mode="markers",
|
|
300
|
+
name="compounds",
|
|
301
|
+
text=[f"index {i}" for i in range(len(standardized))],
|
|
302
|
+
hovertemplate="%{text}<br>h=%{x:.3f}<br>std resid=%{y:.2f}<extra></extra>",
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
fig.add_vline(
|
|
306
|
+
x=leverage_ad.threshold_,
|
|
307
|
+
line_dash="dash",
|
|
308
|
+
annotation_text="h*",
|
|
309
|
+
)
|
|
310
|
+
fig.add_hline(y=residual_limit, line_dash="dash")
|
|
311
|
+
fig.add_hline(y=-residual_limit, line_dash="dash")
|
|
312
|
+
fig.update_layout(
|
|
313
|
+
title="Williams plot",
|
|
314
|
+
xaxis_title="Leverage (h)",
|
|
315
|
+
yaxis_title="Standardized residual",
|
|
316
|
+
)
|
|
317
|
+
return fig
|