qsarkit-learn 0.5.0__tar.gz

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 (227) hide show
  1. qsarkit_learn-0.5.0/CONTRIBUTING_STYLE.md +182 -0
  2. qsarkit_learn-0.5.0/LICENSE +21 -0
  3. qsarkit_learn-0.5.0/MANIFEST.in +26 -0
  4. qsarkit_learn-0.5.0/PKG-INFO +408 -0
  5. qsarkit_learn-0.5.0/README.md +323 -0
  6. qsarkit_learn-0.5.0/notebooks/01_curation_and_the_functional_api.ipynb +682 -0
  7. qsarkit_learn-0.5.0/notebooks/02_representation_and_chemical_space.ipynb +1861 -0
  8. qsarkit_learn-0.5.0/notebooks/03_modeling_validation_and_applicability.ipynb +2181 -0
  9. qsarkit_learn-0.5.0/notebooks/04_sar_explainability_and_reporting.ipynb +3770 -0
  10. qsarkit_learn-0.5.0/notebooks/05_classification_calibration_and_deployment.ipynb +7337 -0
  11. qsarkit_learn-0.5.0/notebooks/README.md +59 -0
  12. qsarkit_learn-0.5.0/package.md +726 -0
  13. qsarkit_learn-0.5.0/pyproject.toml +197 -0
  14. qsarkit_learn-0.5.0/qsarkit/__init__.py +134 -0
  15. qsarkit_learn-0.5.0/qsarkit/applicability/__init__.py +57 -0
  16. qsarkit_learn-0.5.0/qsarkit/applicability/_analyzer.py +317 -0
  17. qsarkit_learn-0.5.0/qsarkit/applicability/_domains.py +1010 -0
  18. qsarkit_learn-0.5.0/qsarkit/base/__init__.py +29 -0
  19. qsarkit_learn-0.5.0/qsarkit/base/exceptions.py +67 -0
  20. qsarkit_learn-0.5.0/qsarkit/base/optional_deps.py +76 -0
  21. qsarkit_learn-0.5.0/qsarkit/base/transformer.py +215 -0
  22. qsarkit_learn-0.5.0/qsarkit/chemistry/__init__.py +45 -0
  23. qsarkit_learn-0.5.0/qsarkit/chemistry/fragments/__init__.py +6 -0
  24. qsarkit_learn-0.5.0/qsarkit/chemistry/fragments/_core_extractor.py +111 -0
  25. qsarkit_learn-0.5.0/qsarkit/chemistry/fragments/_remover.py +133 -0
  26. qsarkit_learn-0.5.0/qsarkit/chemistry/glycans/__init__.py +7 -0
  27. qsarkit_learn-0.5.0/qsarkit/chemistry/glycans/_descriptors.py +109 -0
  28. qsarkit_learn-0.5.0/qsarkit/chemistry/glycans/_detector.py +194 -0
  29. qsarkit_learn-0.5.0/qsarkit/chemistry/glycans/_remover.py +139 -0
  30. qsarkit_learn-0.5.0/qsarkit/chemistry/graph/__init__.py +5 -0
  31. qsarkit_learn-0.5.0/qsarkit/chemistry/graph/_graph.py +129 -0
  32. qsarkit_learn-0.5.0/qsarkit/chemistry/standardization/__init__.py +5 -0
  33. qsarkit_learn-0.5.0/qsarkit/chemistry/standardization/_standardizer.py +143 -0
  34. qsarkit_learn-0.5.0/qsarkit/chemspace/__init__.py +41 -0
  35. qsarkit_learn-0.5.0/qsarkit/chemspace/_analyzers.py +996 -0
  36. qsarkit_learn-0.5.0/qsarkit/chemspace/_fingerprints.py +214 -0
  37. qsarkit_learn-0.5.0/qsarkit/cluster/__init__.py +21 -0
  38. qsarkit_learn-0.5.0/qsarkit/cluster/_butina.py +212 -0
  39. qsarkit_learn-0.5.0/qsarkit/cluster/_pickers.py +384 -0
  40. qsarkit_learn-0.5.0/qsarkit/data_quality/__init__.py +53 -0
  41. qsarkit_learn-0.5.0/qsarkit/data_quality/_duplicates.py +398 -0
  42. qsarkit_learn-0.5.0/qsarkit/data_quality/_pipeline.py +370 -0
  43. qsarkit_learn-0.5.0/qsarkit/data_quality/_validators.py +600 -0
  44. qsarkit_learn-0.5.0/qsarkit/explainability/__init__.py +80 -0
  45. qsarkit_learn-0.5.0/qsarkit/explainability/_atom_maps.py +561 -0
  46. qsarkit_learn-0.5.0/qsarkit/explainability/_atomic.py +545 -0
  47. qsarkit_learn-0.5.0/qsarkit/explainability/_importance.py +672 -0
  48. qsarkit_learn-0.5.0/qsarkit/feature_selection/__init__.py +38 -0
  49. qsarkit_learn-0.5.0/qsarkit/feature_selection/_boruta.py +225 -0
  50. qsarkit_learn-0.5.0/qsarkit/feature_selection/_correlation.py +194 -0
  51. qsarkit_learn-0.5.0/qsarkit/feature_selection/_mutual_info.py +155 -0
  52. qsarkit_learn-0.5.0/qsarkit/feature_selection/_rfe.py +133 -0
  53. qsarkit_learn-0.5.0/qsarkit/feature_selection/_variance.py +97 -0
  54. qsarkit_learn-0.5.0/qsarkit/functional/__init__.py +174 -0
  55. qsarkit_learn-0.5.0/qsarkit/functional/_core.py +1127 -0
  56. qsarkit_learn-0.5.0/qsarkit/functional/_model_steps.py +1310 -0
  57. qsarkit_learn-0.5.0/qsarkit/functional/_steps.py +999 -0
  58. qsarkit_learn-0.5.0/qsarkit/functional/_viz.py +546 -0
  59. qsarkit_learn-0.5.0/qsarkit/metrics/__init__.py +123 -0
  60. qsarkit_learn-0.5.0/qsarkit/metrics/_calibration.py +605 -0
  61. qsarkit_learn-0.5.0/qsarkit/metrics/_classification.py +748 -0
  62. qsarkit_learn-0.5.0/qsarkit/metrics/_common.py +79 -0
  63. qsarkit_learn-0.5.0/qsarkit/metrics/_regression.py +949 -0
  64. qsarkit_learn-0.5.0/qsarkit/metrics/_reports.py +167 -0
  65. qsarkit_learn-0.5.0/qsarkit/metrics/_thresholds.py +528 -0
  66. qsarkit_learn-0.5.0/qsarkit/model_selection/__init__.py +55 -0
  67. qsarkit_learn-0.5.0/qsarkit/model_selection/_search.py +242 -0
  68. qsarkit_learn-0.5.0/qsarkit/model_selection/_splitters.py +802 -0
  69. qsarkit_learn-0.5.0/qsarkit/models/__init__.py +33 -0
  70. qsarkit_learn-0.5.0/qsarkit/models/_baseline.py +198 -0
  71. qsarkit_learn-0.5.0/qsarkit/models/_consensus.py +249 -0
  72. qsarkit_learn-0.5.0/qsarkit/models/_facades.py +830 -0
  73. qsarkit_learn-0.5.0/qsarkit/models/_gaussian_process.py +164 -0
  74. qsarkit_learn-0.5.0/qsarkit/models/_neural_network.py +112 -0
  75. qsarkit_learn-0.5.0/qsarkit/models/_pls.py +128 -0
  76. qsarkit_learn-0.5.0/qsarkit/models/_random_forest.py +102 -0
  77. qsarkit_learn-0.5.0/qsarkit/models/_svm.py +82 -0
  78. qsarkit_learn-0.5.0/qsarkit/models/_tanimoto_kernel.py +187 -0
  79. qsarkit_learn-0.5.0/qsarkit/neighbors/__init__.py +25 -0
  80. qsarkit_learn-0.5.0/qsarkit/neighbors/_distance.py +212 -0
  81. qsarkit_learn-0.5.0/qsarkit/neighbors/_knn.py +306 -0
  82. qsarkit_learn-0.5.0/qsarkit/neighbors/_search.py +236 -0
  83. qsarkit_learn-0.5.0/qsarkit/persistence/__init__.py +61 -0
  84. qsarkit_learn-0.5.0/qsarkit/persistence/_bundle.py +735 -0
  85. qsarkit_learn-0.5.0/qsarkit/persistence/_metadata.py +187 -0
  86. qsarkit_learn-0.5.0/qsarkit/py.typed +0 -0
  87. qsarkit_learn-0.5.0/qsarkit/reporting/__init__.py +66 -0
  88. qsarkit_learn-0.5.0/qsarkit/reporting/_plots.py +979 -0
  89. qsarkit_learn-0.5.0/qsarkit/reporting/_report.py +1015 -0
  90. qsarkit_learn-0.5.0/qsarkit/representation/__init__.py +65 -0
  91. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_3d.py +162 -0
  92. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/__init__.py +37 -0
  93. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_base.py +125 -0
  94. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_calculator.py +169 -0
  95. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_constitutional.py +185 -0
  96. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_fragments.py +78 -0
  97. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_lipinski.py +139 -0
  98. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_physicochemical.py +127 -0
  99. qsarkit_learn-0.5.0/qsarkit/representation/descriptors/_rdkit_descriptors.py +78 -0
  100. qsarkit_learn-0.5.0/qsarkit/representation/embeddings/__init__.py +16 -0
  101. qsarkit_learn-0.5.0/qsarkit/representation/embeddings/_chemberta.py +89 -0
  102. qsarkit_learn-0.5.0/qsarkit/representation/embeddings/_hf_base.py +187 -0
  103. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/__init__.py +59 -0
  104. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_atompair.py +187 -0
  105. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_avalon.py +110 -0
  106. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_base.py +150 -0
  107. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_combiner.py +160 -0
  108. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_maccs.py +80 -0
  109. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_mhfp.py +454 -0
  110. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_morgan.py +180 -0
  111. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_pharmacophore.py +85 -0
  112. qsarkit_learn-0.5.0/qsarkit/representation/fingerprints/_rdkit.py +232 -0
  113. qsarkit_learn-0.5.0/qsarkit/representation/mol2vec/__init__.py +14 -0
  114. qsarkit_learn-0.5.0/qsarkit/representation/mol2vec/_mol2vec.py +365 -0
  115. qsarkit_learn-0.5.0/qsarkit/sar/__init__.py +45 -0
  116. qsarkit_learn-0.5.0/qsarkit/sar/_cliffs.py +925 -0
  117. qsarkit_learn-0.5.0/qsarkit/sar/_mmp.py +372 -0
  118. qsarkit_learn-0.5.0/qsarkit/sar/_rgroup.py +490 -0
  119. qsarkit_learn-0.5.0/qsarkit/transform/__init__.py +49 -0
  120. qsarkit_learn-0.5.0/qsarkit/transform/_transforms.py +722 -0
  121. qsarkit_learn-0.5.0/qsarkit/uncertainty/__init__.py +70 -0
  122. qsarkit_learn-0.5.0/qsarkit/uncertainty/_calibration.py +308 -0
  123. qsarkit_learn-0.5.0/qsarkit/uncertainty/_conformal.py +536 -0
  124. qsarkit_learn-0.5.0/qsarkit/uncertainty/_estimators.py +554 -0
  125. qsarkit_learn-0.5.0/qsarkit/utils/__init__.py +49 -0
  126. qsarkit_learn-0.5.0/qsarkit/utils/constants.py +82 -0
  127. qsarkit_learn-0.5.0/qsarkit/utils/io.py +624 -0
  128. qsarkit_learn-0.5.0/qsarkit/utils/logging.py +134 -0
  129. qsarkit_learn-0.5.0/qsarkit/utils/validation.py +360 -0
  130. qsarkit_learn-0.5.0/qsarkit/validation/__init__.py +43 -0
  131. qsarkit_learn-0.5.0/qsarkit/validation/_cross_validation.py +257 -0
  132. qsarkit_learn-0.5.0/qsarkit/validation/_robustness.py +609 -0
  133. qsarkit_learn-0.5.0/qsarkit/validation/_scoring.py +318 -0
  134. qsarkit_learn-0.5.0/qsarkit_learn.egg-info/PKG-INFO +408 -0
  135. qsarkit_learn-0.5.0/qsarkit_learn.egg-info/SOURCES.txt +225 -0
  136. qsarkit_learn-0.5.0/qsarkit_learn.egg-info/dependency_links.txt +1 -0
  137. qsarkit_learn-0.5.0/qsarkit_learn.egg-info/requires.txt +67 -0
  138. qsarkit_learn-0.5.0/qsarkit_learn.egg-info/top_level.txt +1 -0
  139. qsarkit_learn-0.5.0/setup.cfg +4 -0
  140. qsarkit_learn-0.5.0/tests/api/test_estimator_contract.py +207 -0
  141. qsarkit_learn-0.5.0/tests/applicability/test_domains.py +336 -0
  142. qsarkit_learn-0.5.0/tests/chemistry/test_fragments.py +76 -0
  143. qsarkit_learn-0.5.0/tests/chemistry/test_glycans.py +75 -0
  144. qsarkit_learn-0.5.0/tests/chemistry/test_standardization.py +78 -0
  145. qsarkit_learn-0.5.0/tests/chemspace/test_analyzers.py +206 -0
  146. qsarkit_learn-0.5.0/tests/chemspace/test_chemspace.py +101 -0
  147. qsarkit_learn-0.5.0/tests/cluster/test_cluster.py +243 -0
  148. qsarkit_learn-0.5.0/tests/conftest.py +45 -0
  149. qsarkit_learn-0.5.0/tests/data_quality/test_data_quality.py +461 -0
  150. qsarkit_learn-0.5.0/tests/docs/conftest.py +36 -0
  151. qsarkit_learn-0.5.0/tests/docs/test_doctest_checker.py +85 -0
  152. qsarkit_learn-0.5.0/tests/docs/test_documentation_examples.py +263 -0
  153. qsarkit_learn-0.5.0/tests/docs/test_notebooks.py +89 -0
  154. qsarkit_learn-0.5.0/tests/docs/test_published_site.py +59 -0
  155. qsarkit_learn-0.5.0/tests/docs/test_readme.py +103 -0
  156. qsarkit_learn-0.5.0/tests/explainability/test_atom_maps.py +269 -0
  157. qsarkit_learn-0.5.0/tests/explainability/test_explainability.py +463 -0
  158. qsarkit_learn-0.5.0/tests/feature_selection/test_feature_selection.py +295 -0
  159. qsarkit_learn-0.5.0/tests/functional/test_model_steps.py +363 -0
  160. qsarkit_learn-0.5.0/tests/functional/test_molecules_input.py +104 -0
  161. qsarkit_learn-0.5.0/tests/functional/test_pipe.py +544 -0
  162. qsarkit_learn-0.5.0/tests/functional/test_viz.py +162 -0
  163. qsarkit_learn-0.5.0/tests/metrics/test_calibration.py +298 -0
  164. qsarkit_learn-0.5.0/tests/metrics/test_classification.py +242 -0
  165. qsarkit_learn-0.5.0/tests/metrics/test_common.py +47 -0
  166. qsarkit_learn-0.5.0/tests/metrics/test_regression.py +227 -0
  167. qsarkit_learn-0.5.0/tests/metrics/test_reports.py +66 -0
  168. qsarkit_learn-0.5.0/tests/metrics/test_thresholds.py +268 -0
  169. qsarkit_learn-0.5.0/tests/model_selection/test_splitters.py +362 -0
  170. qsarkit_learn-0.5.0/tests/models/test_custom_estimators.py +202 -0
  171. qsarkit_learn-0.5.0/tests/models/test_models.py +465 -0
  172. qsarkit_learn-0.5.0/tests/neighbors/test_distance.py +128 -0
  173. qsarkit_learn-0.5.0/tests/neighbors/test_knn.py +264 -0
  174. qsarkit_learn-0.5.0/tests/persistence/test_persistence.py +437 -0
  175. qsarkit_learn-0.5.0/tests/reporting/test_reporting.py +488 -0
  176. qsarkit_learn-0.5.0/tests/representation/test_representation.py +375 -0
  177. qsarkit_learn-0.5.0/tests/sar/conftest.py +37 -0
  178. qsarkit_learn-0.5.0/tests/sar/test_cliffs.py +285 -0
  179. qsarkit_learn-0.5.0/tests/sar/test_mmp.py +125 -0
  180. qsarkit_learn-0.5.0/tests/sar/test_rgroup.py +200 -0
  181. qsarkit_learn-0.5.0/tests/transform/test_transforms.py +341 -0
  182. qsarkit_learn-0.5.0/tests/uncertainty/test_uncertainty.py +470 -0
  183. qsarkit_learn-0.5.0/tests/utils/test_io_edge_cases.py +306 -0
  184. qsarkit_learn-0.5.0/tests/utils/test_utils.py +206 -0
  185. qsarkit_learn-0.5.0/tests/validation/test_cross_validation.py +122 -0
  186. qsarkit_learn-0.5.0/tests/validation/test_robustness.py +206 -0
  187. qsarkit_learn-0.5.0/tests/validation/test_scoring.py +244 -0
  188. qsarkit_learn-0.5.0/typings/rdkit/Avalon.pyi +14 -0
  189. qsarkit_learn-0.5.0/typings/rdkit/Chem/AllChem.pyi +14 -0
  190. qsarkit_learn-0.5.0/typings/rdkit/Chem/BRICS.pyi +14 -0
  191. qsarkit_learn-0.5.0/typings/rdkit/Chem/Crippen.pyi +14 -0
  192. qsarkit_learn-0.5.0/typings/rdkit/Chem/Descriptors.pyi +14 -0
  193. qsarkit_learn-0.5.0/typings/rdkit/Chem/Draw.pyi +14 -0
  194. qsarkit_learn-0.5.0/typings/rdkit/Chem/EnumerateStereoisomers.pyi +14 -0
  195. qsarkit_learn-0.5.0/typings/rdkit/Chem/FilterCatalog.pyi +14 -0
  196. qsarkit_learn-0.5.0/typings/rdkit/Chem/Fragments.pyi +14 -0
  197. qsarkit_learn-0.5.0/typings/rdkit/Chem/GraphDescriptors.pyi +14 -0
  198. qsarkit_learn-0.5.0/typings/rdkit/Chem/Lipinski.pyi +14 -0
  199. qsarkit_learn-0.5.0/typings/rdkit/Chem/MACCSkeys.pyi +14 -0
  200. qsarkit_learn-0.5.0/typings/rdkit/Chem/MolStandardize/__init__.pyi +14 -0
  201. qsarkit_learn-0.5.0/typings/rdkit/Chem/MolStandardize/rdMolStandardize.pyi +14 -0
  202. qsarkit_learn-0.5.0/typings/rdkit/Chem/Pharm2D.pyi +14 -0
  203. qsarkit_learn-0.5.0/typings/rdkit/Chem/QED.pyi +14 -0
  204. qsarkit_learn-0.5.0/typings/rdkit/Chem/Recap.pyi +14 -0
  205. qsarkit_learn-0.5.0/typings/rdkit/Chem/Scaffolds/MurckoScaffold.pyi +14 -0
  206. qsarkit_learn-0.5.0/typings/rdkit/Chem/Scaffolds/__init__.pyi +14 -0
  207. qsarkit_learn-0.5.0/typings/rdkit/Chem/__init__.pyi +22 -0
  208. qsarkit_learn-0.5.0/typings/rdkit/Chem/inchi.pyi +14 -0
  209. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdChemReactions.pyi +14 -0
  210. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdDetermineBonds.pyi +14 -0
  211. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdDistGeom.pyi +14 -0
  212. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdFMCS.pyi +14 -0
  213. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdFingerprintGenerator.pyi +14 -0
  214. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdForceFieldHelpers.pyi +14 -0
  215. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdFreeSASA.pyi +14 -0
  216. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdMHFPFingerprint.pyi +14 -0
  217. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdMolAlign.pyi +14 -0
  218. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdMolDescriptors.pyi +14 -0
  219. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdMolTransforms.pyi +14 -0
  220. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdPartialCharges.pyi +14 -0
  221. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdRGroupDecomposition.pyi +14 -0
  222. qsarkit_learn-0.5.0/typings/rdkit/Chem/rdmolops.pyi +14 -0
  223. qsarkit_learn-0.5.0/typings/rdkit/DataStructs.pyi +14 -0
  224. qsarkit_learn-0.5.0/typings/rdkit/ML/__init__.pyi +14 -0
  225. qsarkit_learn-0.5.0/typings/rdkit/RDLogger.pyi +14 -0
  226. qsarkit_learn-0.5.0/typings/rdkit/SimDivFilters.pyi +14 -0
  227. qsarkit_learn-0.5.0/typings/rdkit/__init__.pyi +14 -0
@@ -0,0 +1,182 @@
1
+ # qsarkit contribution style guide
2
+
3
+ This document defines the conventions every module in this package follows.
4
+ Read it before adding a new module.
5
+
6
+ ## 1. Molecule I/O contract
7
+
8
+ Every public class/function that consumes molecules accepts
9
+ `Iterable[rdkit.Chem.Mol]` (never SMILES strings directly as the primary
10
+ input — SMILES parsing belongs in `qsarkit.chemistry` / `qsarkit.utils`
11
+ helpers, or in explicit `from_smiles` convenience constructors).
12
+
13
+ Use `qsarkit.base.ensure_mol_list` to validate/materialize input inside
14
+ `transform`.
15
+
16
+ ## 2. Class shape
17
+
18
+ - Mol -> Mol curation/standardization steps subclass
19
+ `qsarkit.base.MoleculeToMoleculeTransformer`.
20
+ - Mol -> features/vectors steps (fingerprints, descriptors, embeddings)
21
+ subclass `qsarkit.base.MoleculeTransformer` or
22
+ `qsarkit.base.FittableMoleculeTransformer` if they must learn parameters
23
+ (e.g. Mol2Vec, a fitted scaler).
24
+ - All such classes implement `fit(mols, y=None)` and `_transform(mols)`
25
+ (the public `transform` is provided by the base class and does
26
+ validation). They are scikit-learn compatible (`BaseEstimator`,
27
+ `TransformerMixin`), so `get_params`/`set_params`/`fit_transform` work
28
+ out of the box — do not override `__init__` in a way that stores
29
+ anything other than the constructor arguments verbatim (sklearn
30
+ convention).
31
+ - Data records (e.g. `ActivityRecord`, `Relation`) are `@dataclass`.
32
+ - API clients (`qsarkit.databases.*`) are plain classes (not transformers)
33
+ with explicit methods per the spec (e.g. `get_by_cid`), using `requests`
34
+ with a `timeout`, raising `qsarkit.base.DatabaseClientError` on failure.
35
+ - Heavy optional dependencies (torch, transformers, gensim, shap, skopt,
36
+ bs4/pdfminer/lxml, matplotlib, jinja2) are imported lazily via
37
+ `qsarkit.base.require("torch")` inside `__init__`/`fit`/the method that
38
+ needs them — never at module top level. This keeps `import qsarkit`
39
+ cheap and lets users install only the extras they need
40
+ (`pip install qsarkit-learn[nlp]`, see `pyproject.toml`).
41
+
42
+ ## 3. Mandatory documentation
43
+
44
+ Every algorithm, model, metric, database interface, chemical
45
+ transformation and NLP method **must** have a docstring with a
46
+ `References` section citing the original publication (with DOI when
47
+ available) and, where relevant, the official implementation/API docs it
48
+ wraps (RDKit, scikit-learn, PubChem PUG REST, ChEMBL API, etc.). Follow
49
+ the NumPy docstring style used in `qsarkit/base/transformer.py`. A class
50
+ with a non-trivial algorithm and no `References` section is considered
51
+ incomplete.
52
+
53
+ Example:
54
+
55
+ ```python
56
+ class FooTransformer(MoleculeTransformer):
57
+ """One-line summary.
58
+
59
+ Longer description of what it does and why.
60
+
61
+ Parameters
62
+ ----------
63
+ radius : int
64
+ ...
65
+
66
+ References
67
+ ----------
68
+ - Author et al. (Year). "Title." Journal, vol(issue), pages.
69
+ https://doi.org/xxxx
70
+ - RDKit documentation: https://www.rdkit.org/docs/...
71
+ """
72
+ ```
73
+
74
+ ## 4. Error handling
75
+
76
+ Only catch/validate at real boundaries (parsing untrusted text, network
77
+ calls, user-supplied SMILES). Do not wrap internal RDKit calls in
78
+ defensive `try/except` "just in case" — trust that a `Chem.Mol` produced
79
+ by an earlier validated step is a valid `Mol`. Raise the specific
80
+ exception from `qsarkit.base.exceptions` that matches the failure, not a
81
+ bare `Exception`.
82
+
83
+ ## 5. No premature abstraction
84
+
85
+ Implement exactly the classes/methods named in `PROMPT.md` for your
86
+ module. Do not invent extra plugin systems, registries or config
87
+ frameworks. Where an algorithm genuinely requires a heavy pretrained
88
+ model (ChemBERTa, MPNN, generative models) that cannot be trained/loaded
89
+ in this environment, still implement the full class with a real
90
+ `__init__`/`fit`/`transform`/`forward` contract and real tensor
91
+ plumbing — using `require()` for the heavy dependency — rather than a
92
+ placeholder that just raises `NotImplementedError`. It is fine for such a
93
+ class to *download or expect* a pretrained checkpoint (document the
94
+ expected source in `References`); it must not silently return fake data.
95
+
96
+ ## 6. Package layout
97
+
98
+ Each leaf subpackage (e.g. `qsarkit/chemistry/glycans/`) has:
99
+ - one module file per major class (or a couple of closely related classes),
100
+ e.g. `_detector.py`, `_remover.py`, `_descriptors.py`
101
+ - an `__init__.py` that re-exports the public classes
102
+
103
+ Look at `qsarkit/chemistry/standardization/` for a worked example of this
104
+ pattern before writing a new module.
105
+
106
+ ## 7. Static typing (mypy / mypyc)
107
+
108
+ The package is checked with `mypy --strict` (see `[tool.mypy]` in
109
+ `pyproject.toml`) and must stay mypyc-compilable. That means:
110
+
111
+ - **Every** function, method and `__init__` has full parameter and return
112
+ annotations. No bare `def f(x):`.
113
+ - `from __future__ import annotations` at the top of every module.
114
+ - Annotate RDKit molecules as `Mol` via a `TYPE_CHECKING` import:
115
+ ```python
116
+ from typing import TYPE_CHECKING
117
+ if TYPE_CHECKING:
118
+ from rdkit.Chem import Mol
119
+ ```
120
+ and use `"Mol"` / `Iterable["Mol"]` in signatures. Do not annotate them
121
+ as `Any` — RDKit has no stubs, but our own signatures must still be
122
+ precise for readers.
123
+ - Use `npt.NDArray[np.float64]` (`import numpy.typing as npt`) for array
124
+ returns, `list[...]`/`dict[...]` builtins (safe under
125
+ `from __future__ import annotations` on 3.9), `Optional[X]` not `X | None`
126
+ in runtime-evaluated positions, and `Sequence`/`Iterable` for inputs.
127
+ - Prefer `@dataclass` with annotated fields, `Protocol` for duck-typed
128
+ interfaces, and `Literal[...]` for string-enum parameters
129
+ (e.g. `mode: Literal["binary", "count"]`).
130
+ - mypyc-friendliness: avoid monkey-patching instance methods, avoid
131
+ reassigning a name to a different type, and keep class attributes
132
+ declared at class level with annotations.
133
+
134
+ Run `python -m mypy qsarkit/<your_module>` and fix every error in your
135
+ own code before finishing.
136
+
137
+ ## 8. Plotting
138
+
139
+ **All plotting uses Plotly, not matplotlib.** `plotly` is a core
140
+ dependency. Plotting functions return a `plotly.graph_objects.Figure`
141
+ (never call `.show()` internally, never write a file unless explicitly
142
+ asked). Import it normally at the top of plotting modules:
143
+
144
+ ```python
145
+ import plotly.graph_objects as go
146
+ ```
147
+
148
+ Static image export (`fig.write_image`) needs `kaleido`, which is in the
149
+ `reporting` extra — guard that path with `qsarkit.base.require("kaleido")`.
150
+
151
+ ## 9. Testing
152
+
153
+ Target **100% statement and branch coverage** of your module
154
+ (`pytest --cov=qsarkit.<module> --cov-branch`). Every public class,
155
+ method, branch and raised exception needs a test. Where a line is only
156
+ reachable with an optional dependency installed, cover it behind
157
+ `pytest.importorskip` rather than excluding it.
158
+
159
+
160
+
161
+ Add lightweight `pytest` unit tests under `tests/<module_path>/` mirroring
162
+ the package path, using small hardcoded molecules (aspirin, benzene,
163
+ ethanol, a flavonoid glycoside, etc.) — not network calls. Tests for
164
+ network-dependent clients (`qsarkit.databases.*`, live entity linking)
165
+ should mock `requests` rather than hitting real APIs.
166
+
167
+ ## Documentation examples must be executable
168
+
169
+ Every example in a docstring or a documentation page is run by the test
170
+ suite (`pytest tests/docs`). Write them as `>>>` doctests, not as
171
+ `.. code-block:: python`, so that an example which stops being true fails
172
+ CI like any other regression.
173
+
174
+ A `code-block` is acceptable only where the snippet genuinely cannot run in
175
+ CI — it needs an optional dependency, a file that does not exist, or a
176
+ third-party package. `tests/docs/test_documentation_examples.py` still
177
+ checks that every `qsarkit` name such a block references actually exists.
178
+
179
+ Numbers in an example must be **measured, not plausible**. Run the code and
180
+ paste what it prints, including when the result is unflattering: a guide
181
+ that quotes an invented R² teaches the reader to expect something the
182
+ package does not deliver.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frederico Schmitt Kremer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # Files to ship in the source distribution beyond what setuptools infers.
2
+ #
3
+ # The test suite is included deliberately: a downstream packager (conda-forge,
4
+ # a Linux distribution) builds from the sdist and needs to be able to run the
5
+ # tests to verify the build. `package.md` and the notebooks are the
6
+ # documentation someone would want alongside the code.
7
+
8
+ include LICENSE
9
+ include README.md
10
+ include package.md
11
+ include CONTRIBUTING_STYLE.md
12
+ include pyproject.toml
13
+
14
+ recursive-include tests *.py
15
+ recursive-include notebooks *.ipynb *.md
16
+ recursive-include typings *.pyi
17
+
18
+ # The RDKit stub shadow is required for `mypy qsarkit` to run at all; see
19
+ # the note in the README.
20
+ include qsarkit/py.typed
21
+
22
+ # Never ship build artefacts or caches.
23
+ global-exclude *.py[cod]
24
+ global-exclude __pycache__
25
+ prune docs
26
+ prune docs-sphinx/build
@@ -0,0 +1,408 @@
1
+ Metadata-Version: 2.4
2
+ Name: qsarkit-learn
3
+ Version: 0.5.0
4
+ Summary: A focused Python library for QSAR modeling: structure curation, molecular representation, model building and selection, OECD-aligned validation, applicability domain, uncertainty and SAR interpretation.
5
+ Author: Frederico Schmitt Kremer
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/omixlab/qsarkit-learn
8
+ Project-URL: Documentation, https://omixlab.github.io/qsarkit-learn/
9
+ Project-URL: Repository, https://github.com/omixlab/qsarkit-learn
10
+ Project-URL: Issues, https://github.com/omixlab/qsarkit-learn/issues
11
+ Project-URL: Changelog, https://omixlab.github.io/qsarkit-learn/changelog.html
12
+ Keywords: qsar,cheminformatics,rdkit,molecular-descriptors,machine-learning,drug-discovery,applicability-domain
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
23
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.23
29
+ Requires-Dist: scipy>=1.9
30
+ Requires-Dist: pandas>=1.5
31
+ Requires-Dist: scikit-learn>=1.2
32
+ Requires-Dist: rdkit>=2022.9.1
33
+ Requires-Dist: networkx>=3.0
34
+ Requires-Dist: plotly>=5.15
35
+ Provides-Extra: embeddings
36
+ Requires-Dist: gensim>=4.3; extra == "embeddings"
37
+ Provides-Extra: nlp
38
+ Requires-Dist: transformers>=4.30; extra == "nlp"
39
+ Requires-Dist: torch>=2.0; extra == "nlp"
40
+ Provides-Extra: explainability
41
+ Requires-Dist: shap>=0.42; extra == "explainability"
42
+ Requires-Dist: lime>=0.2; extra == "explainability"
43
+ Provides-Extra: boosting
44
+ Requires-Dist: xgboost>=1.7; extra == "boosting"
45
+ Requires-Dist: lightgbm>=3.3; extra == "boosting"
46
+ Provides-Extra: reporting
47
+ Requires-Dist: kaleido>=0.2.1; extra == "reporting"
48
+ Requires-Dist: reportlab>=4.0; extra == "reporting"
49
+ Provides-Extra: persistence
50
+ Requires-Dist: skops>=0.9; extra == "persistence"
51
+ Provides-Extra: balancing
52
+ Requires-Dist: imbalanced-learn>=0.11; extra == "balancing"
53
+ Provides-Extra: embedding-viz
54
+ Requires-Dist: umap-learn>=0.5; extra == "embedding-viz"
55
+ Provides-Extra: dev
56
+ Requires-Dist: pytest>=7.3; extra == "dev"
57
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
58
+ Requires-Dist: mypy>=1.5; extra == "dev"
59
+ Requires-Dist: pandas-stubs; extra == "dev"
60
+ Requires-Dist: ruff>=0.1; extra == "dev"
61
+ Requires-Dist: nbformat>=5.9; extra == "dev"
62
+ Requires-Dist: nbclient>=0.8; extra == "dev"
63
+ Requires-Dist: ipykernel>=6.25; extra == "dev"
64
+ Provides-Extra: docs
65
+ Requires-Dist: sphinx>=7.0; extra == "docs"
66
+ Requires-Dist: furo>=2023.9.10; extra == "docs"
67
+ Requires-Dist: sphinx-autodoc-typehints>=1.24; extra == "docs"
68
+ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
69
+ Requires-Dist: myst-parser>=2.0; extra == "docs"
70
+ Requires-Dist: sphinx-design>=0.5; extra == "docs"
71
+ Provides-Extra: all
72
+ Requires-Dist: gensim>=4.3; extra == "all"
73
+ Requires-Dist: transformers>=4.30; extra == "all"
74
+ Requires-Dist: torch>=2.0; extra == "all"
75
+ Requires-Dist: shap>=0.42; extra == "all"
76
+ Requires-Dist: lime>=0.2; extra == "all"
77
+ Requires-Dist: xgboost>=1.7; extra == "all"
78
+ Requires-Dist: lightgbm>=3.3; extra == "all"
79
+ Requires-Dist: kaleido>=0.2.1; extra == "all"
80
+ Requires-Dist: reportlab>=4.0; extra == "all"
81
+ Requires-Dist: umap-learn>=0.5; extra == "all"
82
+ Requires-Dist: skops>=0.9; extra == "all"
83
+ Requires-Dist: imbalanced-learn>=0.11; extra == "all"
84
+ Dynamic: license-file
85
+
86
+ # qsarkit-learn
87
+
88
+ A focused, open-source Python library for QSAR (Quantitative Structure-Activity Relationship) modeling.
89
+
90
+ **[Documentation](https://omixlab.github.io/qsarkit-learn/)** | **[Package reference](package.md)** | **[Notebooks](notebooks/)** | **[Source Code](https://github.com/omixlab/qsarkit-learn)**
91
+
92
+ `qsarkit-learn` covers the QSAR workflow proper — curating structures, turning them into features, fitting and validating a model, defining where it applies, and interpreting what it learned. It deliberately stops there: it is not a literature-mining, database-retrieval, docking or de-novo design toolkit, and does not pretend to be.
93
+
94
+ ## The Workflow
95
+
96
+ The package is organized around the core stages of a QSAR project:
97
+
98
+ 1. **Curation** (`qsarkit.chemistry`, `qsarkit.data_quality`): Curate structures and resolve activity data conflicts.
99
+ 2. **Representation** (`qsarkit.representation`): Convert molecules into machine-learning ready features (descriptors, fingerprints, learned embeddings).
100
+ 3. **Modeling** (`qsarkit.models`, `qsarkit.model_selection`, `qsarkit.feature_selection`): Select features and fit models.
101
+ 4. **Validation** (`qsarkit.validation`, `qsarkit.metrics`): Evaluate models aligned with OECD principles.
102
+ 5. **Applicability Domain** (`qsarkit.applicability`): Define the chemical space where your model's predictions are trustworthy.
103
+ 6. **Uncertainty** (`qsarkit.uncertainty`): Estimate prediction confidence intervals and error bars.
104
+ 7. **Interpretation** (`qsarkit.sar`, `qsarkit.explainability`): Explain model decisions and identify activity cliffs.
105
+ 8. **Reporting** (`qsarkit.reporting`, `qsarkit.persistence`): Generate model reports and save models reproducibly.
106
+
107
+ ## Design Principles
108
+
109
+ - **Everything is a scikit-learn estimator**: Transformers accept `Iterable[rdkit.Chem.Mol]` and implement `fit` / `transform`. Models implement `fit` / `predict`. Everything composes seamlessly in `sklearn.pipeline.Pipeline`, works with `GridSearchCV`, and supports `clone()`.
110
+ - **Every algorithm cites its source**: Each class documents its original scientific publication with a DOI.
111
+ - **Typed and checked**: The codebase is strictly typed (`mypy --strict`) and ships with a `py.typed` marker.
112
+ - **Every example is executed**: Docstrings, guide pages, API reference and notebooks all run in the test suite, so none of them can go stale silently.
113
+ - **Narrow on purpose**: Everything here earns its place in the core QSAR workflow. Data acquisition and generative modeling are deliberately kept out of scope to maintain a highly trustworthy, specialized tool.
114
+
115
+ ## Installation
116
+
117
+ Install from PyPI with pip. Python 3.9+ is required.
118
+
119
+ ```bash
120
+ pip install qsarkit-learn
121
+ ```
122
+
123
+ > The distribution is **`qsarkit-learn`**; the package you import is **`qsarkit`** — the same split as `scikit-learn` and `sklearn`. The bare name `qsarkit` on PyPI belongs to an unrelated project.
124
+
125
+ The package uses optional dependencies to avoid bloating your environment. You can install specific extras depending on your use case:
126
+
127
+ ```bash
128
+ # For embeddings and NLP-based representations
129
+ pip install qsarkit-learn[embeddings,nlp]
130
+
131
+ # For tree-based models and explainability tools
132
+ pip install qsarkit-learn[boosting,explainability]
133
+
134
+ # For pickle-free model saving and PDF reports
135
+ pip install qsarkit-learn[persistence,reporting]
136
+
137
+ # To install everything
138
+ pip install qsarkit-learn[all]
139
+ ```
140
+
141
+ A missing optional dependency raises an error naming the extra that provides it, rather than an `ImportError` you have to interpret.
142
+
143
+ | Extra | Enables |
144
+ |---|---|
145
+ | `embeddings` | Mol2Vec embeddings (`gensim`) |
146
+ | `nlp` | ChemBERTa embeddings, MC-dropout (`transformers`, `torch`) |
147
+ | `explainability` | SHAP and LIME attribution |
148
+ | `boosting` | XGBoost and LightGBM estimators |
149
+ | `embedding_viz` | UMAP chemical-space projections (`umap-learn`) |
150
+ | `reporting` | PDF and static image export (`reportlab`, `kaleido`) |
151
+ | `persistence` | Pickle-free model saving (`skops`) |
152
+ | `balancing` | imbalanced-learn samplers |
153
+
154
+ ## A First Example
155
+
156
+ Here is a simple example showing how to curate a molecule and analyze the Structure-Activity Relationship (SAR) of a dataset:
157
+
158
+ ```python
159
+ from rdkit import Chem
160
+ from qsarkit.chemistry import MolecularStandardizer
161
+ from qsarkit.sar import activity_cliff_report
162
+
163
+ # Curate: strip the salt, neutralize the charge
164
+ standardizer = MolecularStandardizer()
165
+ mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)[O-].[Na+]")
166
+ curated_mols = standardizer.transform([mol])
167
+ print(Chem.MolToSmiles(curated_mols[0]))
168
+ # Output: 'CC(=O)Oc1ccccc1C(=O)O'
169
+
170
+ # Diagnose a dataset before modeling it to find activity cliffs
171
+ # (Assuming `mols` is a list of RDKit molecules and `pIC50_values` is an array of activities)
172
+ report = activity_cliff_report(mols, pIC50_values)
173
+ print(report["cliff_ratio"]) # Proportion of the SAR that is discontinuous
174
+ print(report["top_transformations"]) # R-group swaps that cause the cliffs
175
+ ```
176
+
177
+ ## The Functional Pipe API
178
+
179
+ A second way to write the same workflow, reading in the order the work happens — in the spirit of R's `%>%`. It is not a separate implementation: `featurize` takes any transformer and `fit` any estimator, so the pipe reaches the whole package.
180
+
181
+ `molecules()` accepts RDKit molecules, SMILES and InChI in any mixture, and every step keeps `y` index-aligned with the molecules — dropping a molecule drops its label with it.
182
+
183
+ ```python
184
+ from qsarkit.functional import *
185
+
186
+ train, test = (
187
+ molecules(smiles_or_inchi, activities)
188
+ >> standardize() >> drop_invalid() # MoleculeSet
189
+ >> remove_duplicates(agg="mean")
190
+ >> fingerprint("morgan", n_bits=2048) # -> FeatureSet
191
+ >> split("scaffold", test_size=0.2)
192
+ )
193
+ model = train >> select_features(k=200) >> fit("rf")
194
+ ```
195
+
196
+ Steps compose, so a curation protocol can be defined once and applied to train and test alike:
197
+
198
+ ```python
199
+ curate = standardize() >> drop_invalid() >> remove_duplicates(max_spread=1.0)
200
+ train = molecules(train_smiles, train_y) >> curate
201
+ test = molecules(test_smiles, test_y) >> curate
202
+ ```
203
+
204
+ > **Use `>>`, not `>`.** Python parses `a > b > c` as the chained comparison `(a > b) and (b > c)`, so a `>`-based pipe silently discards everything but the last two stages. Piping with `>` raises a `TypeError` explaining why.
205
+
206
+ A pipeline is a graph, and drawing it is the quickest way to confirm the stages are in the order you meant:
207
+
208
+ ```python
209
+ pipe = standardize() >> drop_invalid() >> fingerprint() >> scale() >> fit("rf")
210
+
211
+ pipe.plot() # a Plotly figure, no extra dependency
212
+ pipe.render("workflow.pdf") # PNG / PDF / SVG
213
+ print(pipe.to_dot()) # Graphviz DOT source
214
+ ```
215
+
216
+ ## Any Estimator You Like
217
+
218
+ `QSARRegressor` and `QSARClassifier` dispatch on a name (`"rf"`, `"svm"`, `"gbm"`, `"xgboost"`, `"lightgbm"`, `"knn"`, `"pls"`, `"gp"`, …), but they also accept anything following the scikit-learn `fit`/`predict` protocol — XGBoost, LightGBM, CatBoost, or your own wrapper — as a class or an instance:
219
+
220
+ ```python
221
+ from catboost import CatBoostRegressor
222
+
223
+ model = QSARRegressor(CatBoostRegressor,
224
+ model_params={"depth": 6},
225
+ fit_params={"verbose": False}).fit(X, y)
226
+ ```
227
+
228
+ `model_args` / `model_params` go to the constructor; `fit_params`, `predict_params` and `predict_proba_params` reach arguments that belong to the call rather than the constructor. An instance you pass is cloned, never mutated, and the facade remains a real scikit-learn estimator.
229
+
230
+ ## Honest Validation
231
+
232
+ The split is the experiment. A random split of a QSAR dataset measures *interpolation*, because public sets are dense with near-duplicate analogues. On the demo dataset that difference is Q²F1 = 0.82 random against −1.0 by scaffold — same data, same model.
233
+
234
+ OECD principle 4 asks for three separate things, and a single R² addresses only the first:
235
+
236
+ ```python
237
+ from qsarkit.validation import BootstrapValidator, CrossValidator, YScrambling
238
+
239
+ CrossValidator(n_splits=5).evaluate(model, X_train, y_train) # predictivity
240
+ YScrambling(n_iterations=100).run(model, X_train, y_train)["p_value"] # robustness
241
+ BootstrapValidator(n_iterations=100).run(model, X_train, y_train) # precision
242
+ ```
243
+
244
+ y-randomization is the check that catches the classic QSAR failure — a few dozen compounds described by thousands of descriptors, where something will always correlate. It is cheap, so there is no excuse for omitting it.
245
+
246
+ For classification, three traps worth knowing about:
247
+
248
+ ```python
249
+ from qsarkit.metrics import calibration_report, optimal_threshold, threshold_report
250
+
251
+ # `predict()` cuts at 0.5, which is almost never right on an imbalanced set.
252
+ optimal_threshold(y_val, scores, criterion="mcc")
253
+ optimal_threshold(y_val, scores, criterion="cost", cost_fn=5.0, cost_fp=1.0)
254
+ threshold_report(y_val, scores) # every criterion, against the 0.5 default
255
+
256
+ # ROC-AUC depends only on the *ranking* of scores, so a model can have
257
+ # excellent AUC and useless probabilities. Check before you threshold them.
258
+ calibration_report(y_val, scores)["brier_skill_score"]
259
+ ```
260
+
261
+ And for regression, every metric here assumes roughly normal, homoscedastic errors — when that fails, the numbers still compute and quietly mean something else:
262
+
263
+ ```python
264
+ from qsarkit.metrics import qq_data, residual_normality
265
+
266
+ residual_normality(y_test, y_pred) # skew, kurtosis, heteroscedasticity
267
+ qq_data(y_test - y_pred) # the data behind a normal Q-Q plot
268
+ ```
269
+
270
+ ## Applicability Domain
271
+
272
+ A prediction outside the domain is not *wrong* — it is unsupported by the training data, which is a different claim and the one regulators ask about.
273
+
274
+ ```python
275
+ from qsarkit.applicability import ADAnalyzer, TanimotoSimilarityAD
276
+
277
+ domain = TanimotoSimilarityAD(threshold=0.35).fit(X_train)
278
+ report = ADAnalyzer(domain).fit(X_train).report(X_test, y_test, y_pred)
279
+ report["rmse_ratio"] # > 1 means the domain is doing its job
280
+ ```
281
+
282
+ A domain with 100% coverage has told you nothing — and usually indicates a random split rather than a good model.
283
+
284
+ ## Explaining a Model on the Molecule
285
+
286
+ SHAP and LIME attribute a prediction to *features*. For a fingerprint model those are hash buckets, and "bit 1743 contributed +0.21" is not an explanation a chemist can act on. `AttributionAtomMapper` projects it back onto atoms through the fingerprint's bit-provenance map, and RDKit draws the result:
287
+
288
+ ```python
289
+ from qsarkit.explainability import AttributionAtomMapper, draw_atom_weights
290
+
291
+ mapper = AttributionAtomMapper(fingerprint)
292
+ weights = mapper.from_shap(mol, explainer, X, index=0)
293
+
294
+ mapper.collision_rate(mol) # how much to trust the picture
295
+ svg = draw_atom_weights(mol, weights) # RDKit similarity map
296
+ ```
297
+
298
+ ## Saving a Model That Still Works Next Year
299
+
300
+ **Not pickle.** A pickled model embeds the exact class layout of every object, so a file written under one scikit-learn release can fail to load — or load into a subtly different object — under the next; and loading one executes arbitrary code.
301
+
302
+ `qsarkit.persistence` writes a directory bundle instead: plain JSON metadata beside a [skops](https://skops.readthedocs.io) representation of the estimator, which stores parameters as data and refuses to reconstruct untrusted types.
303
+
304
+ ```python
305
+ from qsarkit.persistence import ModelMetadata, inspect_bundle, load_model, save_model
306
+
307
+ path = save_model(
308
+ model,
309
+ "egfr_pIC50",
310
+ pipeline=fingerprint, # so the bundle can take molecules
311
+ metadata=ModelMetadata(name="EGFR", endpoint="pIC50 (-log10 M)"),
312
+ )
313
+
314
+ inspect_bundle(path)["untrusted"] # safe to run on a stranger's bundle
315
+ bundle = load_model(path)
316
+ bundle.predict_mols(new_mols) # straight from structures
317
+ ```
318
+
319
+ A feature-width mismatch is refused rather than producing confident nonsense, and a model loaded under different package versions says so.
320
+
321
+ ## Reports
322
+
323
+ `QSARReport` renders to plain text, Markdown, HTML, JSON and PDF, with tables and plots carried into each. `OECDReportBuilder` structures the same material around the five validation principles and tracks which are **unaddressed** — a submission fails review over a principle nobody noticed was missing.
324
+
325
+ ```python
326
+ from qsarkit.reporting import OECDReportBuilder, QSARReport
327
+
328
+ builder = OECDReportBuilder(title="QMRF for EGFR model", endpoint="pIC50")
329
+ builder.add_evidence(1, True, {"endpoint": "pIC50, CHEMBL203"})
330
+ builder.unaddressed # [2, 3, 4, 5] — explicit gaps
331
+
332
+ builder.build().to_pdf("qmrf.pdf")
333
+ ```
334
+
335
+ All plotting returns `plotly.graph_objects.Figure` objects. Functions never call `.show()` and never write files, so the same figure composes into a notebook, a dashboard and a report.
336
+
337
+ ## Learning the Package
338
+
339
+ - **[`package.md`](package.md)** — the whole package organization in one file: every subpackage, its main classes and functions, and the literature behind them.
340
+ - **[Notebooks](notebooks/)** — five worked walkthroughs covering every public subpackage, committed with their output:
341
+ 1. [Curation and the functional API](notebooks/01_curation_and_the_functional_api.ipynb)
342
+ 2. [Representation and chemical space](notebooks/02_representation_and_chemical_space.ipynb)
343
+ 3. [Modelling, validation and applicability](notebooks/03_modeling_validation_and_applicability.ipynb)
344
+ 4. [SAR, explainability and reporting](notebooks/04_sar_explainability_and_reporting.ipynb)
345
+ 5. [Classification, calibration and deployment](notebooks/05_classification_calibration_and_deployment.ipynb)
346
+ - **[Documentation](https://omixlab.github.io/qsarkit-learn/)** — the API reference, with a worked example and scientific references for every class.
347
+
348
+ The notebooks use a deliberately *hard* 24-compound dataset: one planted activity-cliff outlier, distinct scaffold families, non-normal residuals. Several of them show models scoring badly on it. That is the point — a worked example where everything succeeds teaches nothing about the failure modes these tools exist to detect.
349
+
350
+ ## Development
351
+
352
+ ```bash
353
+ git clone https://github.com/omixlab/qsarkit-learn
354
+ cd qsarkit-learn
355
+ pip install -e ".[dev]"
356
+
357
+ pytest # the suite
358
+ pytest -m slow # plus executing the notebooks
359
+ pytest --cov=qsarkit --cov-branch # with coverage
360
+ mypy qsarkit # strict type check
361
+ ruff check qsarkit # lint
362
+ ```
363
+
364
+ Building the documentation. `docs-sphinx/` is the source; `docs/` is the
365
+ built site served by GitHub Pages, and it is committed, so rebuilding it is
366
+ part of preparing a commit that touches the docs:
367
+
368
+ ```bash
369
+ pip install -e ".[docs]"
370
+
371
+ make docs # rebuild and copy the site into docs/ — the one to run
372
+ make preview # build into docs-sphinx/build/html, leaving docs/ alone
373
+ make doctest # execute every example in the documentation
374
+ make linkcheck # verify external links resolve
375
+ ```
376
+
377
+ `make docs` builds from scratch with warnings treated as errors, so a broken
378
+ cross-reference or a page deleted from the source cannot reach the published
379
+ site. The same targets exist inside `docs-sphinx/` if you prefer to work
380
+ there (`make publish` is the one that writes to `../docs`).
381
+
382
+ > `typings/` holds a local stub shadow for RDKit. The `rdkit-stubs` bundled with the RDKit wheel contain an auto-generation bug — a C++ enum member named `None`, which is an illegal annotation target — that otherwise aborts every mypy run and silently hides all real type errors. Don't delete it.
383
+
384
+ ## OECD Compliance
385
+
386
+ The package is organized around the five OECD validation principles:
387
+
388
+ | # | Principle | Where it lives |
389
+ |---|---|---|
390
+ | 1 | A defined endpoint | `ModelMetadata`, `QSARReport` |
391
+ | 2 | An unambiguous algorithm | Documented hyperparameters; `MoleculeSet.history`; `persistence` provenance |
392
+ | 3 | A defined applicability domain | `qsarkit.applicability` |
393
+ | 4 | Goodness-of-fit, robustness, predictivity | `qsarkit.metrics`, `qsarkit.validation` |
394
+ | 5 | A mechanistic interpretation, if possible | `qsarkit.sar`, `qsarkit.explainability` |
395
+
396
+ > OECD (2007). *Guidance Document on the Validation of (Quantitative) Structure-Activity Relationship [(Q)SAR] Models.* OECD Series on Testing and Assessment No. 69, ENV/JM/MONO(2007)2. [doi:10.1787/9789264085442-en](https://doi.org/10.1787/9789264085442-en)
397
+
398
+ ## Contributing
399
+
400
+ We welcome contributions! Please see the [`CONTRIBUTING_STYLE.md`](CONTRIBUTING_STYLE.md) guide for our conventions on class structures, documentation, error handling, static typing (`mypy`), and unit testing.
401
+
402
+ ## Citing
403
+
404
+ If `qsarkit-learn` contributes to work you publish, please cite the package along with the primary reference for whichever algorithm you used — each class docstring names it.
405
+
406
+ ## License
407
+
408
+ MIT — see [`LICENSE`](LICENSE).