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.
Files changed (125) hide show
  1. qsarkit/__init__.py +134 -0
  2. qsarkit/applicability/__init__.py +57 -0
  3. qsarkit/applicability/_analyzer.py +317 -0
  4. qsarkit/applicability/_domains.py +1010 -0
  5. qsarkit/base/__init__.py +29 -0
  6. qsarkit/base/exceptions.py +67 -0
  7. qsarkit/base/optional_deps.py +76 -0
  8. qsarkit/base/transformer.py +215 -0
  9. qsarkit/chemistry/__init__.py +45 -0
  10. qsarkit/chemistry/fragments/__init__.py +6 -0
  11. qsarkit/chemistry/fragments/_core_extractor.py +111 -0
  12. qsarkit/chemistry/fragments/_remover.py +133 -0
  13. qsarkit/chemistry/glycans/__init__.py +7 -0
  14. qsarkit/chemistry/glycans/_descriptors.py +109 -0
  15. qsarkit/chemistry/glycans/_detector.py +194 -0
  16. qsarkit/chemistry/glycans/_remover.py +139 -0
  17. qsarkit/chemistry/graph/__init__.py +5 -0
  18. qsarkit/chemistry/graph/_graph.py +129 -0
  19. qsarkit/chemistry/standardization/__init__.py +5 -0
  20. qsarkit/chemistry/standardization/_standardizer.py +143 -0
  21. qsarkit/chemspace/__init__.py +41 -0
  22. qsarkit/chemspace/_analyzers.py +996 -0
  23. qsarkit/chemspace/_fingerprints.py +214 -0
  24. qsarkit/cluster/__init__.py +21 -0
  25. qsarkit/cluster/_butina.py +212 -0
  26. qsarkit/cluster/_pickers.py +384 -0
  27. qsarkit/data_quality/__init__.py +53 -0
  28. qsarkit/data_quality/_duplicates.py +398 -0
  29. qsarkit/data_quality/_pipeline.py +370 -0
  30. qsarkit/data_quality/_validators.py +600 -0
  31. qsarkit/explainability/__init__.py +80 -0
  32. qsarkit/explainability/_atom_maps.py +561 -0
  33. qsarkit/explainability/_atomic.py +545 -0
  34. qsarkit/explainability/_importance.py +672 -0
  35. qsarkit/feature_selection/__init__.py +38 -0
  36. qsarkit/feature_selection/_boruta.py +225 -0
  37. qsarkit/feature_selection/_correlation.py +194 -0
  38. qsarkit/feature_selection/_mutual_info.py +155 -0
  39. qsarkit/feature_selection/_rfe.py +133 -0
  40. qsarkit/feature_selection/_variance.py +97 -0
  41. qsarkit/functional/__init__.py +174 -0
  42. qsarkit/functional/_core.py +1127 -0
  43. qsarkit/functional/_model_steps.py +1310 -0
  44. qsarkit/functional/_steps.py +999 -0
  45. qsarkit/functional/_viz.py +546 -0
  46. qsarkit/metrics/__init__.py +123 -0
  47. qsarkit/metrics/_calibration.py +605 -0
  48. qsarkit/metrics/_classification.py +748 -0
  49. qsarkit/metrics/_common.py +79 -0
  50. qsarkit/metrics/_regression.py +949 -0
  51. qsarkit/metrics/_reports.py +167 -0
  52. qsarkit/metrics/_thresholds.py +528 -0
  53. qsarkit/model_selection/__init__.py +55 -0
  54. qsarkit/model_selection/_search.py +242 -0
  55. qsarkit/model_selection/_splitters.py +802 -0
  56. qsarkit/models/__init__.py +33 -0
  57. qsarkit/models/_baseline.py +198 -0
  58. qsarkit/models/_consensus.py +249 -0
  59. qsarkit/models/_facades.py +830 -0
  60. qsarkit/models/_gaussian_process.py +164 -0
  61. qsarkit/models/_neural_network.py +112 -0
  62. qsarkit/models/_pls.py +128 -0
  63. qsarkit/models/_random_forest.py +102 -0
  64. qsarkit/models/_svm.py +82 -0
  65. qsarkit/models/_tanimoto_kernel.py +187 -0
  66. qsarkit/neighbors/__init__.py +25 -0
  67. qsarkit/neighbors/_distance.py +212 -0
  68. qsarkit/neighbors/_knn.py +306 -0
  69. qsarkit/neighbors/_search.py +236 -0
  70. qsarkit/persistence/__init__.py +61 -0
  71. qsarkit/persistence/_bundle.py +735 -0
  72. qsarkit/persistence/_metadata.py +187 -0
  73. qsarkit/py.typed +0 -0
  74. qsarkit/reporting/__init__.py +66 -0
  75. qsarkit/reporting/_plots.py +979 -0
  76. qsarkit/reporting/_report.py +1015 -0
  77. qsarkit/representation/__init__.py +65 -0
  78. qsarkit/representation/descriptors/_3d.py +162 -0
  79. qsarkit/representation/descriptors/__init__.py +37 -0
  80. qsarkit/representation/descriptors/_base.py +125 -0
  81. qsarkit/representation/descriptors/_calculator.py +169 -0
  82. qsarkit/representation/descriptors/_constitutional.py +185 -0
  83. qsarkit/representation/descriptors/_fragments.py +78 -0
  84. qsarkit/representation/descriptors/_lipinski.py +139 -0
  85. qsarkit/representation/descriptors/_physicochemical.py +127 -0
  86. qsarkit/representation/descriptors/_rdkit_descriptors.py +78 -0
  87. qsarkit/representation/embeddings/__init__.py +16 -0
  88. qsarkit/representation/embeddings/_chemberta.py +89 -0
  89. qsarkit/representation/embeddings/_hf_base.py +187 -0
  90. qsarkit/representation/fingerprints/__init__.py +59 -0
  91. qsarkit/representation/fingerprints/_atompair.py +187 -0
  92. qsarkit/representation/fingerprints/_avalon.py +110 -0
  93. qsarkit/representation/fingerprints/_base.py +150 -0
  94. qsarkit/representation/fingerprints/_combiner.py +160 -0
  95. qsarkit/representation/fingerprints/_maccs.py +80 -0
  96. qsarkit/representation/fingerprints/_mhfp.py +454 -0
  97. qsarkit/representation/fingerprints/_morgan.py +180 -0
  98. qsarkit/representation/fingerprints/_pharmacophore.py +85 -0
  99. qsarkit/representation/fingerprints/_rdkit.py +232 -0
  100. qsarkit/representation/mol2vec/__init__.py +14 -0
  101. qsarkit/representation/mol2vec/_mol2vec.py +365 -0
  102. qsarkit/sar/__init__.py +45 -0
  103. qsarkit/sar/_cliffs.py +925 -0
  104. qsarkit/sar/_mmp.py +372 -0
  105. qsarkit/sar/_rgroup.py +490 -0
  106. qsarkit/transform/__init__.py +49 -0
  107. qsarkit/transform/_transforms.py +722 -0
  108. qsarkit/uncertainty/__init__.py +70 -0
  109. qsarkit/uncertainty/_calibration.py +308 -0
  110. qsarkit/uncertainty/_conformal.py +536 -0
  111. qsarkit/uncertainty/_estimators.py +554 -0
  112. qsarkit/utils/__init__.py +49 -0
  113. qsarkit/utils/constants.py +82 -0
  114. qsarkit/utils/io.py +624 -0
  115. qsarkit/utils/logging.py +134 -0
  116. qsarkit/utils/validation.py +360 -0
  117. qsarkit/validation/__init__.py +43 -0
  118. qsarkit/validation/_cross_validation.py +257 -0
  119. qsarkit/validation/_robustness.py +609 -0
  120. qsarkit/validation/_scoring.py +318 -0
  121. qsarkit_learn-0.5.0.dist-info/METADATA +408 -0
  122. qsarkit_learn-0.5.0.dist-info/RECORD +125 -0
  123. qsarkit_learn-0.5.0.dist-info/WHEEL +5 -0
  124. qsarkit_learn-0.5.0.dist-info/licenses/LICENSE +21 -0
  125. 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