dockcert 1.0.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.
- dockcert/__init__.py +46 -0
- dockcert/cli.py +241 -0
- dockcert/core/__init__.py +40 -0
- dockcert/core/bias.py +85 -0
- dockcert/core/bootstrap.py +111 -0
- dockcert/core/enrichment.py +402 -0
- dockcert/core/rmsd.py +159 -0
- dockcert/core/scoring.py +185 -0
- dockcert/parsers/__init__.py +13 -0
- dockcert/parsers/generic_csv.py +135 -0
- dockcert/parsers/structure_io.py +80 -0
- dockcert/parsers/vina_smina.py +58 -0
- dockcert/reporters/__init__.py +13 -0
- dockcert/reporters/html_report.py +336 -0
- dockcert/reporters/manuscript_prep.py +141 -0
- dockcert/reporters/plot_generator.py +158 -0
- dockcert-1.0.0.dist-info/METADATA +186 -0
- dockcert-1.0.0.dist-info/RECORD +22 -0
- dockcert-1.0.0.dist-info/WHEEL +5 -0
- dockcert-1.0.0.dist-info/entry_points.txt +2 -0
- dockcert-1.0.0.dist-info/licenses/LICENSE +21 -0
- dockcert-1.0.0.dist-info/top_level.txt +1 -0
dockcert/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DockCert: Automated Statistical Validation, Enrichment Metrics, and Reproducibility
|
|
3
|
+
Assessment for Molecular Docking and Virtual Screening Studies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
__author__ = "Andre Monreal-Hernández"
|
|
8
|
+
__license__ = "MIT"
|
|
9
|
+
|
|
10
|
+
from dockcert.core.enrichment import (
|
|
11
|
+
calculate_roc_auc,
|
|
12
|
+
calculate_pr_auc,
|
|
13
|
+
calculate_bedroc,
|
|
14
|
+
calculate_rie,
|
|
15
|
+
calculate_enrichment_factor,
|
|
16
|
+
calculate_log_auc,
|
|
17
|
+
calculate_optimal_mcc,
|
|
18
|
+
evaluate_all_enrichment_metrics
|
|
19
|
+
)
|
|
20
|
+
from dockcert.core.rmsd import (
|
|
21
|
+
calculate_heavy_atom_rmsd,
|
|
22
|
+
calculate_symmetry_corrected_rmsd,
|
|
23
|
+
evaluate_redocking_success
|
|
24
|
+
)
|
|
25
|
+
from dockcert.core.bias import evaluate_decoy_bias
|
|
26
|
+
from dockcert.core.bootstrap import bootstrap_enrichment_ci
|
|
27
|
+
from dockcert.core.scoring import assess_docking_quality, DockingValidationReport
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
"calculate_roc_auc",
|
|
32
|
+
"calculate_pr_auc",
|
|
33
|
+
"calculate_bedroc",
|
|
34
|
+
"calculate_rie",
|
|
35
|
+
"calculate_enrichment_factor",
|
|
36
|
+
"calculate_log_auc",
|
|
37
|
+
"calculate_optimal_mcc",
|
|
38
|
+
"evaluate_all_enrichment_metrics",
|
|
39
|
+
"calculate_heavy_atom_rmsd",
|
|
40
|
+
"calculate_symmetry_corrected_rmsd",
|
|
41
|
+
"evaluate_redocking_success",
|
|
42
|
+
"evaluate_decoy_bias",
|
|
43
|
+
"bootstrap_enrichment_ci",
|
|
44
|
+
"assess_docking_quality",
|
|
45
|
+
"DockingValidationReport"
|
|
46
|
+
]
|
dockcert/cli.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Line Interface (CLI) for DockCert.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from dockcert import __version__
|
|
11
|
+
from dockcert.parsers.generic_csv import load_docking_csv
|
|
12
|
+
from dockcert.parsers.structure_io import load_molecule_coordinates
|
|
13
|
+
from dockcert.core.rmsd import calculate_heavy_atom_rmsd, calculate_symmetry_corrected_rmsd
|
|
14
|
+
from dockcert.core.scoring import assess_docking_quality
|
|
15
|
+
from dockcert.reporters.plot_generator import generate_docking_figures
|
|
16
|
+
from dockcert.reporters.manuscript_prep import generate_docking_manuscript_assets
|
|
17
|
+
from dockcert.reporters.html_report import generate_docking_html_report
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_banner():
|
|
21
|
+
banner = rf"""
|
|
22
|
+
_____ _ _____ _
|
|
23
|
+
| __ \ | | / ____| | |
|
|
24
|
+
| | | | ___ ___| | _| | ___ _ __| |_
|
|
25
|
+
| | | |/ _ \ / __| |/ / | / _ \ '__| __|
|
|
26
|
+
| |__| | (_) | (__| <| |___| __/ | | |_
|
|
27
|
+
|_____/ \___/ \___|_|\_\\_____\___|_| \__| v{__version__}
|
|
28
|
+
|
|
29
|
+
Molecular Docking Validation & Statistical Quality Toolkit
|
|
30
|
+
Monreal-Hernández et al., 2026
|
|
31
|
+
"""
|
|
32
|
+
print(banner)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_demo(output_dir: str = "dockcert_demo_output"):
|
|
36
|
+
"""
|
|
37
|
+
Generates a realistic DUD-E-like benchmark dataset (100 actives, 2000 property-matched decoys,
|
|
38
|
+
redocking RMSD = 1.43 A) and executes the full validation pipeline.
|
|
39
|
+
"""
|
|
40
|
+
print(f"\n[DockCert] Running demonstration mode...")
|
|
41
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
n_actives = 100
|
|
44
|
+
n_decoys = 2000
|
|
45
|
+
|
|
46
|
+
rng = np.random.default_rng(42)
|
|
47
|
+
|
|
48
|
+
# Active scores: N(-9.4 kcal/mol, 1.1)
|
|
49
|
+
active_scores = rng.normal(-9.4, 1.1, size=n_actives)
|
|
50
|
+
# Decoy scores: N(-6.8 kcal/mol, 1.2)
|
|
51
|
+
decoy_scores = rng.normal(-6.8, 1.2, size=n_decoys)
|
|
52
|
+
|
|
53
|
+
labels = np.concatenate([np.ones(n_actives, dtype=int), np.zeros(n_decoys, dtype=int)])
|
|
54
|
+
scores = np.concatenate([active_scores, decoy_scores])
|
|
55
|
+
|
|
56
|
+
# Redocking poses: Best RMSD = 1.43 A, ensemble of 9 poses
|
|
57
|
+
redocking_rmsds = [1.43, 1.78, 2.15, 2.40, 2.89, 3.10, 3.45, 4.12, 4.55]
|
|
58
|
+
|
|
59
|
+
# Physicochemical properties for bias audit (MW & LogP)
|
|
60
|
+
active_mw = rng.normal(380.0, 45.0, size=n_actives)
|
|
61
|
+
decoy_mw = rng.normal(375.0, 50.0, size=n_decoys)
|
|
62
|
+
|
|
63
|
+
active_logp = rng.normal(2.8, 0.7, size=n_actives)
|
|
64
|
+
decoy_logp = rng.normal(2.7, 0.8, size=n_decoys)
|
|
65
|
+
|
|
66
|
+
active_props = {"MolecularWeight": active_mw, "LogP": active_logp}
|
|
67
|
+
decoy_props = {"MolecularWeight": decoy_mw, "LogP": decoy_logp}
|
|
68
|
+
|
|
69
|
+
print(" -> Calculating ROC-AUC, PR-AUC, BEDROC (alpha=20.0), EF1%, EF5%, logAUC, and Bootstrap 95% CIs...")
|
|
70
|
+
report = assess_docking_quality(
|
|
71
|
+
labels=labels,
|
|
72
|
+
scores=scores,
|
|
73
|
+
rmsd_values=redocking_rmsds,
|
|
74
|
+
active_properties=active_props,
|
|
75
|
+
decoy_properties=decoy_props,
|
|
76
|
+
lower_is_better=True
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(" -> Generating publication-quality vector charts (ROC, PR, Score distributions, RMSD)...")
|
|
80
|
+
generate_docking_figures(labels, scores, report, output_dir, lower_is_better=True, rmsd_values=redocking_rmsds)
|
|
81
|
+
|
|
82
|
+
print(" -> Drafting manuscript Methods text snippet, summary LaTeX tables, and BibTeX citations...")
|
|
83
|
+
assets = generate_docking_manuscript_assets(report, output_dir)
|
|
84
|
+
|
|
85
|
+
with open(assets["methods_text"], "r", encoding="utf-8") as f:
|
|
86
|
+
methods_txt = f.read()
|
|
87
|
+
with open(assets["citation_bib"], "r", encoding="utf-8") as f:
|
|
88
|
+
bib_txt = f.read()
|
|
89
|
+
|
|
90
|
+
html_p = os.path.join(output_dir, "report.html")
|
|
91
|
+
print(f" -> Writing interactive dashboard to {html_p}...")
|
|
92
|
+
generate_docking_html_report(report, html_p, methods_text=methods_txt, citation_bib=bib_txt)
|
|
93
|
+
|
|
94
|
+
print("\n" + "="*70)
|
|
95
|
+
print(f" [RESULT] Overall Docking Certification: {report.overall_status}")
|
|
96
|
+
print(f" [SCORE] {report.validation_score}")
|
|
97
|
+
print("="*70)
|
|
98
|
+
if report.redocking_result:
|
|
99
|
+
rr = report.redocking_result
|
|
100
|
+
print(f" * Redocking RMSD : Best = {rr['min_rmsd']:.2f} A | Success Rate (<=2A) = {rr['success_rate_2a']:.1f}% | Status: {rr['status']}")
|
|
101
|
+
for k, item in report.enrichment_metrics.items():
|
|
102
|
+
ci_s = f"[{item.ci_lower_95:.2f}, {item.ci_upper_95:.2f}]" if item.ci_lower_95 is not None else ""
|
|
103
|
+
print(f" * {item.name:18s}: Value = {item.value:6.3f} {ci_s:14s} | Threshold = {item.threshold_pass:4.2f} | Status: {item.status}")
|
|
104
|
+
if report.decoy_bias_result:
|
|
105
|
+
print(f" * Decoy Bias Risk : Level = {report.decoy_bias_result['risk_level']} | Status: {report.decoy_bias_result['status']}")
|
|
106
|
+
print("="*70)
|
|
107
|
+
print(f"\nAll outputs successfully saved to: {os.path.abspath(output_dir)}/")
|
|
108
|
+
print(f"Open {os.path.abspath(html_p)} in your browser to inspect the full report.\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_assess(args):
|
|
112
|
+
"""
|
|
113
|
+
Evaluates user-provided CSV and optional structure files.
|
|
114
|
+
"""
|
|
115
|
+
output_dir = args.output
|
|
116
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
csv_file = args.input
|
|
119
|
+
if not csv_file:
|
|
120
|
+
print("[Error] Please specify a results CSV file with --input.", file=sys.stderr)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
print(f"\n[DockCert] Loading docking screening dataset from {csv_file}...")
|
|
124
|
+
labels, scores, props_dict, meta = load_docking_csv(
|
|
125
|
+
csv_file,
|
|
126
|
+
score_column=args.score_col,
|
|
127
|
+
label_column=args.label_col
|
|
128
|
+
)
|
|
129
|
+
print(f" -> Identified {meta['n_total']} entries: {meta['n_actives']} actives, {meta['n_decoys']} decoys.")
|
|
130
|
+
|
|
131
|
+
# RMSD from structures if provided
|
|
132
|
+
rmsd_values = None
|
|
133
|
+
if args.ref_ligand and args.docked_pose:
|
|
134
|
+
print(" -> Calculating heavy-atom RMSD between reference and docked pose...")
|
|
135
|
+
c_ref, el_ref = load_molecule_coordinates(args.ref_ligand)
|
|
136
|
+
c_dock, el_dock = load_molecule_coordinates(args.docked_pose)
|
|
137
|
+
rmsd_val = calculate_symmetry_corrected_rmsd(c_ref, c_dock, elements=el_ref)
|
|
138
|
+
rmsd_values = [rmsd_val]
|
|
139
|
+
print(f" -> Calculated Redocking RMSD: {rmsd_val:.2f} A")
|
|
140
|
+
|
|
141
|
+
print(" -> Performing statistical validation and enrichment analysis...")
|
|
142
|
+
report = assess_docking_quality(
|
|
143
|
+
labels=labels,
|
|
144
|
+
scores=scores,
|
|
145
|
+
rmsd_values=rmsd_values,
|
|
146
|
+
lower_is_better=not args.higher_is_better
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
print(" -> Generating publication charts...")
|
|
150
|
+
generate_docking_figures(labels, scores, report, output_dir, lower_is_better=not args.higher_is_better, rmsd_values=rmsd_values)
|
|
151
|
+
|
|
152
|
+
print(" -> Generating manuscript text, LaTeX summary table, and BibTeX citations...")
|
|
153
|
+
assets = generate_docking_manuscript_assets(report, output_dir)
|
|
154
|
+
|
|
155
|
+
with open(assets["methods_text"], "r", encoding="utf-8") as f:
|
|
156
|
+
methods_txt = f.read()
|
|
157
|
+
with open(assets["citation_bib"], "r", encoding="utf-8") as f:
|
|
158
|
+
bib_txt = f.read()
|
|
159
|
+
|
|
160
|
+
html_p = os.path.join(output_dir, "report.html")
|
|
161
|
+
print(f" -> Writing HTML quality report to {html_p}...")
|
|
162
|
+
generate_docking_html_report(report, html_p, methods_text=methods_txt, citation_bib=bib_txt)
|
|
163
|
+
|
|
164
|
+
print("\n" + "="*70)
|
|
165
|
+
print(f" [RESULT] Overall Docking Certification: {report.overall_status}")
|
|
166
|
+
print(f" [SCORE] {report.validation_score}")
|
|
167
|
+
print("="*70)
|
|
168
|
+
for k, item in report.enrichment_metrics.items():
|
|
169
|
+
print(f" * {item.name:18s}: {item.value:6.3f} | Status: {item.status}")
|
|
170
|
+
if report.redocking_result:
|
|
171
|
+
print(f" * Redocking RMSD : {report.redocking_result['min_rmsd']:.2f} A | Status: {report.redocking_result['status']}")
|
|
172
|
+
print("="*70)
|
|
173
|
+
print(f"\nReport ready at: {os.path.abspath(html_p)}\n")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def print_citation():
|
|
177
|
+
bib = """@software{monreal2026dockcert,
|
|
178
|
+
author = {Monreal-Hern\\'andez, Andre},
|
|
179
|
+
title = {{DockCert: An Open-Source Toolkit for Statistical Validation, Enrichment Metrics, and Reproducibility Assessment of Molecular Docking Studies}},
|
|
180
|
+
year = {2026},
|
|
181
|
+
version = {1.0.0},
|
|
182
|
+
publisher = {Zenodo},
|
|
183
|
+
url = {https://github.com/sircalch/dockcert}
|
|
184
|
+
}"""
|
|
185
|
+
print("\nIf you use DockCert in your publications, please cite:\n")
|
|
186
|
+
print("APA Style:")
|
|
187
|
+
print("Monreal-Hernández, A. (2026). DockCert: An Open-Source Toolkit for Statistical Validation, Enrichment Metrics, and Reproducibility Assessment of Molecular Docking Studies (v1.0.0). Zenodo. https://github.com/sircalch/dockcert\n")
|
|
188
|
+
print("BibTeX:")
|
|
189
|
+
print(bib)
|
|
190
|
+
print()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main():
|
|
194
|
+
parser = argparse.ArgumentParser(
|
|
195
|
+
prog="dockcert",
|
|
196
|
+
description="DockCert: Automated Statistical Validation, Enrichment Metrics, and Reproducibility Toolkit for Molecular Docking."
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument("-v", "--version", action="version", version=f"dockcert {__version__}")
|
|
199
|
+
|
|
200
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
201
|
+
|
|
202
|
+
# Assess command
|
|
203
|
+
assess_parser = subparsers.add_parser("assess", help="Assess virtual screening enrichment and docking accuracy")
|
|
204
|
+
assess_parser.add_argument("-i", "--input", required=True, help="Virtual screening results file (.csv, .tsv, .txt)")
|
|
205
|
+
assess_parser.add_argument("--score-col", help="Column name containing docking scores/affinities")
|
|
206
|
+
assess_parser.add_argument("--label-col", help="Column name containing active/decoy labels")
|
|
207
|
+
assess_parser.add_argument("--ref-ligand", help="Reference crystallographic ligand structure (.sdf, .pdb, .pdbqt)")
|
|
208
|
+
assess_parser.add_argument("--docked-pose", help="Docked pose structure (.sdf, .pdb, .pdbqt)")
|
|
209
|
+
assess_parser.add_argument("--higher-is-better", action="store_true", help="Set if higher score values indicate better affinity")
|
|
210
|
+
assess_parser.add_argument("-o", "--output", default="dockcert_output", help="Directory for output report and assets (default: dockcert_output)")
|
|
211
|
+
|
|
212
|
+
# Demo command
|
|
213
|
+
demo_parser = subparsers.add_parser("demo", help="Run DockCert on a benchmark DUD-E-like simulation dataset")
|
|
214
|
+
demo_parser.add_argument("-o", "--output", default="dockcert_demo_output", help="Output directory (default: dockcert_demo_output)")
|
|
215
|
+
|
|
216
|
+
# Cite command
|
|
217
|
+
subparsers.add_parser("cite", help="Display BibTeX and APA citation details")
|
|
218
|
+
|
|
219
|
+
if len(sys.argv) == 1:
|
|
220
|
+
print_banner()
|
|
221
|
+
parser.print_help()
|
|
222
|
+
sys.exit(0)
|
|
223
|
+
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
if args.command == "assess":
|
|
227
|
+
print_banner()
|
|
228
|
+
run_assess(args)
|
|
229
|
+
elif args.command == "demo":
|
|
230
|
+
print_banner()
|
|
231
|
+
run_demo(args.output)
|
|
232
|
+
elif args.command == "cite":
|
|
233
|
+
print_banner()
|
|
234
|
+
print_citation()
|
|
235
|
+
else:
|
|
236
|
+
parser.print_help()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|
|
241
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core mathematical and validation algorithms for DockCert.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dockcert.core.enrichment import (
|
|
6
|
+
calculate_roc_auc,
|
|
7
|
+
calculate_pr_auc,
|
|
8
|
+
calculate_bedroc,
|
|
9
|
+
calculate_rie,
|
|
10
|
+
calculate_enrichment_factor,
|
|
11
|
+
calculate_log_auc,
|
|
12
|
+
calculate_optimal_mcc,
|
|
13
|
+
evaluate_all_enrichment_metrics
|
|
14
|
+
)
|
|
15
|
+
from dockcert.core.rmsd import (
|
|
16
|
+
calculate_heavy_atom_rmsd,
|
|
17
|
+
calculate_symmetry_corrected_rmsd,
|
|
18
|
+
evaluate_redocking_success
|
|
19
|
+
)
|
|
20
|
+
from dockcert.core.bias import evaluate_decoy_bias
|
|
21
|
+
from dockcert.core.bootstrap import bootstrap_enrichment_ci
|
|
22
|
+
from dockcert.core.scoring import assess_docking_quality, DockingValidationReport
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"calculate_roc_auc",
|
|
26
|
+
"calculate_pr_auc",
|
|
27
|
+
"calculate_bedroc",
|
|
28
|
+
"calculate_rie",
|
|
29
|
+
"calculate_enrichment_factor",
|
|
30
|
+
"calculate_log_auc",
|
|
31
|
+
"calculate_optimal_mcc",
|
|
32
|
+
"evaluate_all_enrichment_metrics",
|
|
33
|
+
"calculate_heavy_atom_rmsd",
|
|
34
|
+
"calculate_symmetry_corrected_rmsd",
|
|
35
|
+
"evaluate_redocking_success",
|
|
36
|
+
"evaluate_decoy_bias",
|
|
37
|
+
"bootstrap_enrichment_ci",
|
|
38
|
+
"assess_docking_quality",
|
|
39
|
+
"DockingValidationReport"
|
|
40
|
+
]
|
dockcert/core/bias.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decoy bias and physicochemical property distribution diagnostics.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy import stats
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def evaluate_decoy_bias(
|
|
11
|
+
active_properties: Dict[str, np.ndarray],
|
|
12
|
+
decoy_properties: Dict[str, np.ndarray]
|
|
13
|
+
) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Evaluates potential artificial enrichment bias by comparing property distributions
|
|
16
|
+
(e.g., MW, LogP, HBD, HBA, Rotatable Bonds) between actives and decoys.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
active_properties : dict
|
|
21
|
+
Mapping of property name -> 1D array for active molecules.
|
|
22
|
+
decoy_properties : dict
|
|
23
|
+
Mapping of property name -> 1D array for decoy molecules.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
result : dict
|
|
28
|
+
Property-by-property Kolmogorov-Smirnov and Wasserstein statistics,
|
|
29
|
+
bias risk classification (LOW / MODERATE / HIGH), and recommendations.
|
|
30
|
+
"""
|
|
31
|
+
common_props = set(active_properties.keys()).intersection(set(decoy_properties.keys()))
|
|
32
|
+
if not common_props:
|
|
33
|
+
return {
|
|
34
|
+
"status": "PASS",
|
|
35
|
+
"risk_level": "LOW",
|
|
36
|
+
"properties_evaluated": 0,
|
|
37
|
+
"property_metrics": {},
|
|
38
|
+
"recommendation": "No property metadata provided for decoy bias auditing. Ensure decoys are property-matched."
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
property_metrics = {}
|
|
42
|
+
ks_pvalues = []
|
|
43
|
+
|
|
44
|
+
for prop in common_props:
|
|
45
|
+
a_vals = np.asarray(active_properties[prop], dtype=float)
|
|
46
|
+
d_vals = np.asarray(decoy_properties[prop], dtype=float)
|
|
47
|
+
|
|
48
|
+
if len(a_vals) < 2 or len(d_vals) < 2:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
ks_res = stats.ks_2samp(a_vals, d_vals)
|
|
52
|
+
w_dist = stats.wasserstein_distance(a_vals, d_vals)
|
|
53
|
+
|
|
54
|
+
property_metrics[prop] = {
|
|
55
|
+
"active_mean": float(np.mean(a_vals)),
|
|
56
|
+
"decoy_mean": float(np.mean(d_vals)),
|
|
57
|
+
"ks_statistic": float(ks_res.statistic),
|
|
58
|
+
"ks_pvalue": float(ks_res.pvalue),
|
|
59
|
+
"wasserstein_distance": float(w_dist)
|
|
60
|
+
}
|
|
61
|
+
ks_pvalues.append(ks_res.pvalue)
|
|
62
|
+
|
|
63
|
+
# If KS p-value is extremely low (< 1e-4) across multiple physical properties, decoys are poorly matched
|
|
64
|
+
significant_biases = sum(1 for p in ks_pvalues if p < 0.001)
|
|
65
|
+
|
|
66
|
+
if significant_biases == 0:
|
|
67
|
+
risk_level = "LOW"
|
|
68
|
+
status = "PASS"
|
|
69
|
+
recommendation = "Decoys and actives are well property-matched. Low risk of artificial enrichment bias."
|
|
70
|
+
elif significant_biases <= 2:
|
|
71
|
+
risk_level = "MODERATE"
|
|
72
|
+
status = "WARNING"
|
|
73
|
+
recommendation = "Moderate distributional discrepancy observed in physicochemical properties between actives and decoys."
|
|
74
|
+
else:
|
|
75
|
+
risk_level = "HIGH"
|
|
76
|
+
status = "WARNING"
|
|
77
|
+
recommendation = "High risk of decoy bias: actives and decoys differ significantly in physical properties (e.g. MW, LogP). Enrichment may be artificially inflated."
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"status": status,
|
|
81
|
+
"risk_level": risk_level,
|
|
82
|
+
"properties_evaluated": len(property_metrics),
|
|
83
|
+
"property_metrics": property_metrics,
|
|
84
|
+
"recommendation": recommendation
|
|
85
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stratified bootstrap confidence interval estimation for virtual screening metrics.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Tuple, Optional
|
|
6
|
+
import numpy as np
|
|
7
|
+
from dockcert.core.enrichment import (
|
|
8
|
+
calculate_roc_auc,
|
|
9
|
+
calculate_pr_auc,
|
|
10
|
+
calculate_bedroc,
|
|
11
|
+
calculate_enrichment_factor
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def bootstrap_enrichment_ci(
|
|
16
|
+
labels: np.ndarray,
|
|
17
|
+
scores: np.ndarray,
|
|
18
|
+
n_resamples: int = 1000,
|
|
19
|
+
confidence_level: float = 0.95,
|
|
20
|
+
lower_is_better: bool = True,
|
|
21
|
+
random_state: Optional[int] = 42
|
|
22
|
+
) -> Dict[str, Tuple[float, float, float]]:
|
|
23
|
+
"""
|
|
24
|
+
Computes stratified non-parametric bootstrap 95% Confidence Intervals for all key metrics.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
labels : np.ndarray
|
|
29
|
+
Binary label array (1 = Active, 0 = Decoy).
|
|
30
|
+
scores : np.ndarray
|
|
31
|
+
Docking scores.
|
|
32
|
+
n_resamples : int, default 1000
|
|
33
|
+
Number of bootstrap iterations.
|
|
34
|
+
confidence_level : float, default 0.95
|
|
35
|
+
Confidence level.
|
|
36
|
+
lower_is_better : bool
|
|
37
|
+
Sorting direction.
|
|
38
|
+
random_state : int, optional
|
|
39
|
+
Seed for reproducibility.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
ci_dict : dict
|
|
44
|
+
Mapping metric_name -> (point_estimate, ci_lower, ci_upper).
|
|
45
|
+
"""
|
|
46
|
+
y_true = np.asarray(labels, dtype=int)
|
|
47
|
+
y_scores = np.asarray(scores, dtype=float)
|
|
48
|
+
|
|
49
|
+
active_idx = np.where(y_true == 1)[0]
|
|
50
|
+
decoy_idx = np.where(y_true == 0)[0]
|
|
51
|
+
|
|
52
|
+
n_act = len(active_idx)
|
|
53
|
+
n_dec = len(decoy_idx)
|
|
54
|
+
|
|
55
|
+
if n_act == 0 or n_dec == 0:
|
|
56
|
+
return {}
|
|
57
|
+
|
|
58
|
+
rng = np.random.default_rng(random_state)
|
|
59
|
+
|
|
60
|
+
# Point estimates
|
|
61
|
+
pe_roc = calculate_roc_auc(y_true, y_scores, lower_is_better)
|
|
62
|
+
pe_pr = calculate_pr_auc(y_true, y_scores, lower_is_better)
|
|
63
|
+
pe_bedroc20 = calculate_bedroc(y_true, y_scores, alpha=20.0, lower_is_better=lower_is_better)
|
|
64
|
+
pe_bedroc80 = calculate_bedroc(y_true, y_scores, alpha=80.5, lower_is_better=lower_is_better)
|
|
65
|
+
pe_ef1, _ = calculate_enrichment_factor(y_true, y_scores, fraction=0.01, lower_is_better=lower_is_better)
|
|
66
|
+
pe_ef5, _ = calculate_enrichment_factor(y_true, y_scores, fraction=0.05, lower_is_better=lower_is_better)
|
|
67
|
+
pe_ef10, _ = calculate_enrichment_factor(y_true, y_scores, fraction=0.10, lower_is_better=lower_is_better)
|
|
68
|
+
|
|
69
|
+
boot_roc = np.empty(n_resamples, dtype=float)
|
|
70
|
+
boot_pr = np.empty(n_resamples, dtype=float)
|
|
71
|
+
boot_bedroc20 = np.empty(n_resamples, dtype=float)
|
|
72
|
+
boot_bedroc80 = np.empty(n_resamples, dtype=float)
|
|
73
|
+
boot_ef1 = np.empty(n_resamples, dtype=float)
|
|
74
|
+
boot_ef5 = np.empty(n_resamples, dtype=float)
|
|
75
|
+
boot_ef10 = np.empty(n_resamples, dtype=float)
|
|
76
|
+
|
|
77
|
+
for b in range(n_resamples):
|
|
78
|
+
sample_act = rng.choice(active_idx, size=n_act, replace=True)
|
|
79
|
+
sample_dec = rng.choice(decoy_idx, size=n_dec, replace=True)
|
|
80
|
+
|
|
81
|
+
sample_idx = np.concatenate([sample_act, sample_dec])
|
|
82
|
+
b_labels = y_true[sample_idx]
|
|
83
|
+
b_scores = y_scores[sample_idx]
|
|
84
|
+
|
|
85
|
+
boot_roc[b] = calculate_roc_auc(b_labels, b_scores, lower_is_better)
|
|
86
|
+
boot_pr[b] = calculate_pr_auc(b_labels, b_scores, lower_is_better)
|
|
87
|
+
boot_bedroc20[b] = calculate_bedroc(b_labels, b_scores, alpha=20.0, lower_is_better=lower_is_better)
|
|
88
|
+
boot_bedroc80[b] = calculate_bedroc(b_labels, b_scores, alpha=80.5, lower_is_better=lower_is_better)
|
|
89
|
+
ef1, _ = calculate_enrichment_factor(b_labels, b_scores, fraction=0.01, lower_is_better=lower_is_better)
|
|
90
|
+
ef5, _ = calculate_enrichment_factor(b_labels, b_scores, fraction=0.05, lower_is_better=lower_is_better)
|
|
91
|
+
ef10, _ = calculate_enrichment_factor(b_labels, b_scores, fraction=0.10, lower_is_better=lower_is_better)
|
|
92
|
+
boot_ef1[b] = ef1
|
|
93
|
+
boot_ef5[b] = ef5
|
|
94
|
+
boot_ef10[b] = ef10
|
|
95
|
+
|
|
96
|
+
alpha_ci = (1.0 - confidence_level) / 2.0
|
|
97
|
+
|
|
98
|
+
def get_ci(point_est, boot_arr):
|
|
99
|
+
low = float(np.percentile(boot_arr, 100.0 * alpha_ci))
|
|
100
|
+
high = float(np.percentile(boot_arr, 100.0 * (1.0 - alpha_ci)))
|
|
101
|
+
return float(point_est), low, high
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"roc_auc": get_ci(pe_roc, boot_roc),
|
|
105
|
+
"pr_auc": get_ci(pe_pr, boot_pr),
|
|
106
|
+
"bedroc_20": get_ci(pe_bedroc20, boot_bedroc20),
|
|
107
|
+
"bedroc_80": get_ci(pe_bedroc80, boot_bedroc80),
|
|
108
|
+
"ef_1pct": get_ci(pe_ef1, boot_ef1),
|
|
109
|
+
"ef_5pct": get_ci(pe_ef5, boot_ef5),
|
|
110
|
+
"ef_10pct": get_ci(pe_ef10, boot_ef10)
|
|
111
|
+
}
|