Reporting¶
QSAR reports in Markdown, HTML and JSON, QMRF-style OECD reporting, and the Plotly figures that go in them.
Note
All plotting returns plotly.graph_objects.Figure. Nothing here
calls .show() or writes a file, so the same figure composes into a
notebook, a dashboard and an HTML report. Static export
(fig.write_image) needs kaleido — pip install qsarkit-learn[reporting].
Figures¶
>>> from qsarkit.models import QSARRegressor
>>> from qsarkit.reporting import plot_predicted_vs_observed, plot_residuals
>>> X, y = demo_fingerprints(256), DEMO_Y
>>> y_pred = QSARRegressor("rf", random_state=0).fit(X, y).predict(X)
>>> figure = plot_predicted_vs_observed(y, y_pred)
>>> type(figure).__name__
'Figure'
>>> type(plot_residuals(y, y_pred)).__name__
'Figure'
The Williams plot is the standard OECD-facing diagnostic, putting leverage against standardized residual so influential points and outliers are visible in one picture:
>>> import numpy as np
>>> from qsarkit.applicability import LeverageAD
>>> from qsarkit.reporting import plot_williams
>>> leverage = LeverageAD().fit(X).score_samples(X)
>>> type(plot_williams(leverage, y, y_pred)).__name__
'Figure'
Embedding figures in a page needs Plotly’s JavaScript exactly once, which
figure_to_html() handles:
>>> from qsarkit.reporting import figure_to_html
>>> first = figure_to_html(figure) # includes plotly.js
>>> rest = figure_to_html(figure, include_plotlyjs=False) # subsequent figures
>>> len(first) > len(rest)
True
Model reports¶
>>> from qsarkit.metrics import qsar_regression_report
>>> from qsarkit.reporting import QSARReport
>>> report = (
... QSARReport(title="Demo model", endpoint="pIC50")
... .add_dataset_section(n_compounds=24, n_train=18, n_test=6)
... .add_model_section(QSARRegressor("rf"), descriptors="Morgan r=2, 256 bits")
... .add_validation_section(qsar_regression_report(y, y_pred))
... )
>>> markdown = report.to_markdown()
>>> markdown.splitlines()[0]
'# Demo model'
>>> "pIC50" in markdown
True
Five output formats¶
The same report object renders every way you might need it, and each carries the tables:
>>> print(report.to_text(width=44).splitlines()[1])
Demo model
>>> report.to_html().startswith("<!DOCTYPE html>")
True
>>> set(report.to_dict()) >= {"title", "sections"}
True
Plain text for a terminal or an email; Markdown for a repository; HTML for sharing; JSON for a downstream system; PDF for a submission:
>>> import os, tempfile
>>> out = os.path.join(tempfile.mkdtemp(), "report.pdf")
>>> _ = report.to_pdf(out)
>>> os.path.getsize(out) > 0
True
to_pdf needs reportlab; embedding plots into PDF or Markdown
additionally needs kaleido (pip install qsarkit-learn[reporting]).
Figures and tables¶
A section can carry prose, a property table, a DataFrame and figures, and all four survive into every format:
>>> import pandas as pd
>>> _ = report.add_section(
... "Outliers",
... content={"n_flagged": 2},
... text="Two compounds sit outside the applicability domain.",
... table=pd.DataFrame({"compound": ["a", "b"], "residual": [1.4, -1.9]}),
... figures=[figure],
... )
>>> text = report.to_text()
>>> "n_flagged" in text and "residual" in text and "[figure:" in text
True
Nested values are flattened rather than printed as a Python repr, which matters because a validation report routinely contains one:
>>> from qsarkit.reporting import QSARReport
>>> small = QSARReport().add_section("S", {"criteria": {"passed": True, "r2": 0.95}})
>>> "passed=yes; r2=0.95" in small.to_text()
True
OECD reporting¶
OECDReportBuilder structures a report around the five validation
principles:
>>> from qsarkit.reporting import OECDReportBuilder
>>> builder = OECDReportBuilder(title="QMRF for demo model", endpoint="pIC50")
>>> [p[0] for p in builder.PRINCIPLES]
[1, 2, 3, 4, 5]
Its most useful behaviour is refusing to be quiet about what you skipped:
>>> builder.add_evidence(1, True, {"endpoint": "pIC50, cell-based assay"})
...
<qsarkit.reporting...OECDReportBuilder object at ...>
>>> builder.unaddressed
[2, 3, 4, 5]
A submission fails review over a principle nobody noticed was missing, so the builder tracks them explicitly rather than letting an omission look like an absence of problems.
API¶
QSAR model reports in Markdown, HTML and JSON.
A model is reproducible only if the record says what data it was built
on, how that data was curated, which descriptors and algorithm were
used, how it was validated and where it applies.
OECDReportBuilder structures that against the five OECD
validation principles, and marks explicitly any principle that has not
been addressed.
All plots return Plotly figures and embed directly in the HTML output.
Examples
>>> from qsarkit.reporting import QSARReport
>>> report = QSARReport(title="EGFR pIC50 model", endpoint="pIC50")
>>> _ = report.add_section("Dataset", {"n_compounds": 1200})
>>> "EGFR" in report.to_markdown()
True
References
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. https://doi.org/10.1787/9789264085442-en
European Commission Joint Research Centre. “QSAR Model Reporting Format (QMRF).” https://joint-research-centre.ec.europa.eu/scientific-tools-databases/qsar-toolbox_en
Tropsha, A. (2010). “Best Practices for QSAR Model Development, Validation, and Exploitation.” Mol. Inform., 29(6-7), 476-488. https://doi.org/10.1002/minf.201000061
- qsarkit.reporting.plot_calibration_curve(y_true, y_prob, n_bins=10, strategy='uniform', title='Calibration (reliability diagram)')[source]¶
Reliability diagram: observed frequency against predicted probability.
A perfectly calibrated classifier lies on the diagonal. Above it the model is under-confident, below it over-confident. Bubble size shows how many compounds each point rests on, because a bin holding three compounds says very little.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Binary labels.y_prob (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Predicted probability of the positive class.n_bins (
int) – Number of bins.strategy (
str) – Binning strategy; seecalibration_curve().title (
str)
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_calibration_curve >>> rng = np.random.default_rng(0) >>> p = rng.uniform(size=500) >>> y = (rng.uniform(size=500) < p).astype(int) >>> figure = plot_calibration_curve(y, p) >>> type(figure).__name__ 'Figure'
Examples
>>> from qsarkit.reporting import figure_to_html, plot_roc_curve >>> import numpy as np >>> figure = plot_roc_curve(np.array([0, 0, 1, 1]), np.array([0.1, 0.2, 0.8, 0.9])) >>> html = figure_to_html(figure) >>> "plotly" in html True
Plotly’s JavaScript must be included exactly once per page, so pass
include_plotlyjs=Falsefor every figure after the first:>>> first = figure_to_html(figure) >>> rest = figure_to_html(figure, include_plotlyjs=False) >>> len(first) > len(rest) True
References
Niculescu-Mizil, A. & Caruana, R. (2005). “Predicting Good Probabilities with Supervised Learning.” ICML 2005, 625-632. https://doi.org/10.1145/1102351.1102430
- qsarkit.reporting.plot_qq(residuals, title='Normal Q-Q plot of residuals')[source]¶
Normal Q-Q plot, for checking the assumption behind every RMSE.
Points on the line mean normal residuals. An S-shape means heavy tails; a bend at one end means skew, usually from a few badly mispredicted compounds that \(R^2\) will not name.
- Parameters:
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_qq >>> rng = np.random.default_rng(0) >>> figure = plot_qq(rng.normal(size=200)) >>> type(figure).__name__ 'Figure'
References
Wilk, M. B. & Gnanadesikan, R. (1968). “Probability Plotting Methods for the Analysis of Data.” Biometrika, 55(1), 1-17. https://doi.org/10.1093/biomet/55.1.1
- qsarkit.reporting.plot_threshold_sweep(y_true, y_score, criteria=None, title='Criterion against decision threshold', pos_label=None)[source]¶
How each selection criterion varies with the decision threshold.
Shows what the conventional 0.5 cut costs, and whether the optimum is a sharp peak or a broad plateau – a broad one means the exact threshold hardly matters, which is worth knowing before tuning it.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Binary labels.y_score (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Scores or probabilities.criteria (
Optional[Sequence[str]]) – Which curves to draw. Defaults to("youden_j", "mcc", "f1", "balanced_accuracy"); any key ofthreshold_sweep()is allowed.title (
str)pos_label (
Optional[Any]) – Which label is the positive class. Required for string labels.
- Return type:
Figure- Raises:
ValueError – If a requested criterion is not produced by the sweep.
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_threshold_sweep >>> rng = np.random.default_rng(0) >>> y = np.zeros(300, dtype=int); y[:30] = 1 >>> scores = rng.beta(2, 8, size=300) + y * 0.3 >>> figure = plot_threshold_sweep(y, scores) >>> type(figure).__name__ 'Figure'
References
Chicco, D. & Jurman, G. (2020). “The Advantages of the Matthews Correlation Coefficient (MCC) over F1 Score and Accuracy.” BMC Genomics, 21, 6. https://doi.org/10.1186/s12864-019-6413-7
- qsarkit.reporting.plot_precision_recall(y_true, y_score, title='Precision-recall curve', pos_label=None)[source]¶
Precision-recall curve, with the base rate as the honest baseline.
Preferable to ROC on imbalanced data: ROC’s specificity axis is dominated by the inactive majority, so a model can look excellent while its top-ranked compounds are mostly false positives. The baseline here is the base rate, which is what random ranking achieves.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Binary labels.y_score (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Scores or probabilities.title (
str)pos_label (
Optional[Any]) – Which label is the positive class. Required for string labels; seethreshold_sweep().
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_precision_recall >>> rng = np.random.default_rng(0) >>> y = np.zeros(300, dtype=int); y[:30] = 1 >>> scores = rng.beta(2, 8, size=300) + y * 0.3 >>> figure = plot_precision_recall(y, scores) >>> type(figure).__name__ 'Figure'
References
Saito, T. & Rehmsmeier, M. (2015). “The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets.” PLoS ONE, 10(3), e0118432. https://doi.org/10.1371/journal.pone.0118432
Davis, J. & Goadrich, M. (2006). “The Relationship Between Precision-Recall and ROC Curves.” ICML 2006, 233-240. https://doi.org/10.1145/1143844.1143874
- qsarkit.reporting.plot_atom_contributions(mol, atom_weights, title='Per-atom contributions', size=(450, 450))[source]¶
Draw atom-level attributions on the structure, as an RDKit SVG.
A thin wrapper over
draw_atom_weights(), provided here so a report can assemble every figure from one module. Unlike the other plotting functions this returns SVG text rather than a Plotly figure, because the depiction is a chemical drawing and RDKit draws those properly.- Parameters:
mol (
Any) – The molecule.atom_weights (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Per-atom attribution, e.g. fromAttributionAtomMapper.title (
str) – Prepended as an SVG<title>, which becomes the tooltip.
- Returns:
SVG text, ready to embed in an HTML report or display in a notebook.
- Return type:
Examples
>>> import numpy as np >>> from rdkit import Chem >>> from qsarkit.reporting import plot_atom_contributions >>> mol = Chem.MolFromSmiles("CC(=O)Nc1ccc(Cl)cc1") >>> svg = plot_atom_contributions(mol, np.linspace(-1, 1, mol.GetNumAtoms())) >>> "<svg" in svg True
References
Riniker, S. & Landrum, G. A. (2013). “Similarity Maps.” J. Cheminform., 5, 43. https://doi.org/10.1186/1758-2946-5-43
- class qsarkit.reporting.QSARReport(title='QSAR model report', author='', endpoint='')[source]¶
Bases:
objectAssemble a complete, auditable report for a QSAR model.
A model is only reproducible if the record says what data it was built on, how that data was curated, which descriptors and algorithm were used, how it was validated and where it applies. This collects all of that into one object that renders to Markdown, HTML or JSON.
Sections are added in whatever order suits the model; the renderers preserve that order.
- Parameters:
Examples
>>> report = QSARReport(title="EGFR pIC50 model", endpoint="pIC50") >>> _ = report.add_section("Dataset", {"n_compounds": 1200}) >>> "EGFR" in report.to_markdown() True
References
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. https://doi.org/10.1787/9789264085442-en
Tropsha, A. (2010). “Best Practices for QSAR Model Development, Validation, and Exploitation.” Mol. Inform., 29(6-7), 476-488. https://doi.org/10.1002/minf.201000061
Patlewicz, G. et al. (2008). “An Evaluation of the Implementation of the OECD (Q)SAR Application Toolbox.” SAR QSAR Environ. Res., 19(5-6), 397-412. https://doi.org/10.1080/10629360802083848
- add_section(title, content=None, text='', figures=None, table=None)[source]¶
Append a section.
- Parameters:
- Returns:
self, so calls can be chained.- Return type:
- add_dataset_section(n_compounds, n_train=None, n_test=None, source='', curation=None)[source]¶
Add the dataset section, optionally including a curation log.
- Parameters:
- Return type:
- add_model_section(model, descriptors='', hyperparameters=None)[source]¶
Add the algorithm section (OECD principle 2).
- add_validation_section(metrics, y_scrambling=None, figures=None)[source]¶
Add the validation section (OECD principle 4).
- Parameters:
- Return type:
- add_applicability_section(domain, coverage=None, report=None, figures=None)[source]¶
Add the applicability-domain section (OECD principle 3).
- to_markdown(path=None, include_figures=True)[source]¶
Render as Markdown, optionally writing it to
path.- Parameters:
path (
Union[str,Path,None]) – Write the Markdown here as well as returning it. When given, figures are written as PNG files in a sibling directory and linked relatively, which is what a Markdown file in a repository needs.include_figures (
bool) – Embed the report’s figures. Requireskaleido(pip install qsarkit-learn[reporting]); passFalsefor a text-and-tables document without it.
- Returns:
The Markdown text.
- Return type:
- Raises:
OptionalDependencyError – If
include_figuresis set, the report has figures, andkaleidois missing.
- to_html(path=None, include_plotlyjs='cdn')[source]¶
Render as a self-describing HTML document with embedded figures.
Assembled directly rather than through a template engine, so the core report has no optional dependency at all.
- to_text(path=None, width=78)[source]¶
Render as plain text, optionally writing it to
path.The format for a terminal, a log, or an email: no markup, ASCII tables, and figures listed by title rather than dropped silently.
- Parameters:
- Returns:
The plain-text report.
- Return type:
Examples
>>> from qsarkit.reporting import QSARReport >>> report = QSARReport(title="Demo", endpoint="pIC50") >>> _ = report.add_dataset_section(n_compounds=24, n_train=18, n_test=6) >>> print(report.to_text(width=40)) ======================================== Demo ======================================== Endpoint: pIC50 Created: ... Dataset ---------------------------------------- n_compounds 24 n_train 18 n_test 6
- to_pdf(path, include_figures=True, page_size='A4')[source]¶
Render as a PDF with tables and embedded plots.
- Parameters:
path (
Union[str,Path]) – Output file. Unlike the other renderers this one is file-only: a PDF is binary and there is nothing useful to return as a string.include_figures (
bool) – Rasterize and embed the report’s Plotly figures. Requireskaleido(pip install qsarkit-learn[reporting]). PassFalseto produce a tables-and-text PDF without it.page_size (
str)
- Returns:
The path written.
- Return type:
- Raises:
OptionalDependencyError – If
reportlabis missing, or ifinclude_figuresis set, the report has figures, andkaleidois missing. The second case raises rather than quietly dropping the plots: a report silently missing its evidence is worse than no report.ValueError – If
page_sizeis not recognized.
Examples
>>> import tempfile, os >>> from qsarkit.reporting import QSARReport >>> report = QSARReport(title="Demo", endpoint="pIC50") >>> _ = report.add_dataset_section(n_compounds=24) >>> out = os.path.join(tempfile.mkdtemp(), "report.pdf") >>> _ = report.to_pdf(out) >>> os.path.getsize(out) > 0 True
References
ReportLab documentation: https://docs.reportlab.com/
- class qsarkit.reporting.ReportSection(title, content=<factory>, text='', figures=<factory>, table=None)[source]¶
Bases:
objectOne titled section of a report.
- Variables:
- class qsarkit.reporting.OECDReportBuilder(title='QMRF report', author='', endpoint='')[source]¶
Bases:
objectBuild a QMRF-style report against the five OECD principles.
The QSAR Model Reporting Format is the structure regulators expect, and its sections map onto the five validation principles. This assembles one from the artefacts the rest of the package produces, and — importantly — records explicitly when a principle has not been addressed, since a silent omission is what makes a submission fail review.
- Parameters:
Examples
>>> builder = OECDReportBuilder(title="EGFR model", endpoint="pIC50") >>> report = builder.build() >>> "OECD" in report.to_markdown() True
Principles you have not addressed are tracked, and appear in the report as explicit gaps:
>>> builder.unaddressed [1, 2, 3, 4, 5] >>> _ = builder.add_evidence(1, True, {"endpoint": "pIC50, CHEMBL204"}) >>> builder.unaddressed [2, 3, 4, 5]
build()returns aQSARReport, which renders to plain text, Markdown, HTML, JSON and PDF – tables and plots included:>>> report = builder.build() >>> print(report.to_text(width=48).splitlines()[1]) EGFR model >>> report.to_html().startswith("<!DOCTYPE html>") True >>> sorted(report.to_dict()) ['author', 'created', 'endpoint', 'sections', 'title']
References
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. https://doi.org/10.1787/9789264085442-en
OECD (2004). “The Report from the Expert Group on (Q)SARs on the Principles for the Validation of (Q)SARs.” ENV/JM/MONO(2004)24.
European Commission Joint Research Centre. “QSAR Model Reporting Format (QMRF).” https://joint-research-centre.ec.europa.eu/scientific-tools-databases/qsar-toolbox_en
- PRINCIPLES¶
- from_validation(validation)[source]¶
Populate principles 3 and 4 from a validation report.
- Parameters:
validation (
Dict[str,Any]) – A mapping carrying validation metrics, e.g. fromqsarkit.validation.- Return type:
- qsarkit.reporting.plot_predicted_vs_observed(y_true, y_pred, title='Predicted vs observed', labels=None)[source]¶
Scatter predictions against observations, with the identity line.
The first plot to look at. A good model’s points hug the diagonal; systematic curvature, fanning, or a slope visibly different from 1 are all visible here and invisible in a single R² number.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Observed values.y_pred (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Predicted values.title (
str) – Figure title.labels (
Optional[Sequence[str]]) – Per-point hover labels, e.g. compound identifiers.
- Return type:
Figure
Examples
>>> import numpy as np >>> fig = plot_predicted_vs_observed([1.0, 2.0, 3.0], [1.1, 1.9, 3.2]) >>> len(fig.data) 2
References
Gramatica, P. (2007). “Principles of QSAR Models Validation.” QSAR Comb. Sci., 26(5), 694-701. https://doi.org/10.1002/qsar.200610151
Tropsha, A. (2010). “Best Practices for QSAR Model Development, Validation, and Exploitation.” Mol. Inform., 29(6-7), 476-488. https://doi.org/10.1002/minf.201000061
- qsarkit.reporting.plot_residuals(y_true, y_pred, title='Residuals', standardized=True)[source]¶
Plot residuals against predicted values.
Where the predicted-vs-observed plot shows whether the model is right, this shows how it is wrong. Residuals should look like a structureless band around zero; a funnel means the error grows with potency, and a curve means a missing non-linear term.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])y_pred (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])title (
str) – Figure title.standardized (
bool) – Divide residuals by their standard deviation, which puts the conventional +/-3 sigma warning lines on a meaningful scale.
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_residuals >>> rng = np.random.default_rng(0) >>> truth = rng.normal(size=50) >>> figure = plot_residuals(truth, truth + rng.normal(scale=0.2, size=50)) >>> type(figure).__name__ 'Figure'
Standardized residuals put the conventional +/-3 sigma lines on a fixed scale, so an outlier is visible without knowing the endpoint’s units:
>>> raw = plot_residuals(truth, truth, standardized=False) >>> raw.layout.yaxis.title.text is not None True
References
Gramatica, P. (2007). QSAR Comb. Sci., 26(5), 694-701. https://doi.org/10.1002/qsar.200610151
Draper, N. R. & Smith, H. (1998). “Applied Regression Analysis,” 3rd ed. Wiley. https://doi.org/10.1002/9781118625590
- qsarkit.reporting.plot_williams(leverage, y_true, y_pred, h_star=None, residual_limit=3.0, title='Williams plot')[source]¶
Williams plot: standardized residuals against leverage.
The standard regulatory diagnostic, and the one plot that separates the two distinct ways a prediction can be untrustworthy. Points to the right of
h*are structural outliers the model is extrapolating to; points outside +/-3 sigma are response outliers the model simply gets wrong. A point in both regions should not be reported at all.- Parameters:
leverage (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Hat-matrix diagonal, e.g. fromLeverageAD.y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])y_pred (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])h_star (
Optional[float]) – Warning leverage. Defaults to the conventional3(p+1)/nestimated from the data when omitted.residual_limit (
float) – Standardized-residual warning level.title (
str) – Figure title.
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.applicability import LeverageAD >>> from qsarkit.reporting import plot_williams >>> rng = np.random.default_rng(0) >>> X = rng.normal(size=(40, 4)) >>> truth = X[:, 0] * 2 + rng.normal(scale=0.2, size=40) >>> leverage = LeverageAD().fit(X).score_samples(X) >>> figure = plot_williams(leverage, truth, truth + rng.normal(scale=0.2, size=40)) >>> type(figure).__name__ 'Figure'
The two lines are what make it a Williams plot:
h_starmarks the leverage threshold andresidual_limitthe residual one, so a point beyond either is influential, an outlier, or both.>>> figure = plot_williams(leverage, truth, truth, h_star=0.3, residual_limit=2.5) >>> len(figure.layout.shapes) >= 2 True
References
Gramatica, P. (2007). QSAR Comb. Sci., 26(5), 694-701. https://doi.org/10.1002/qsar.200610151
Atkinson, A. C. (1985). “Plots, Transformations and Regression.” Oxford University Press.
OECD (2007). Guidance Document No. 69, ENV/JM/MONO(2007)2. https://doi.org/10.1787/9789264085442-en
- qsarkit.reporting.plot_roc_curve(y_true, y_score, title='ROC curve')[source]¶
Receiver operating characteristic curve with its AUC.
- Parameters:
y_true (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Binary labels.y_score (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Scores or predicted probabilities.title (
str) – Figure title.
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_roc_curve >>> scores = np.linspace(1.0, 0.0, 100) >>> labels = np.zeros(100); labels[:10] = 1 # actives ranked first >>> figure = plot_roc_curve(labels, scores) >>> type(figure).__name__ 'Figure' >>> figure.data[1].name # AUC shown in the legend 'ROC (AUC = 1.000)'
On an imbalanced screening set prefer
plot_precision_recall(): ROC’s specificity axis is dominated by the inactive majority, so a model can look excellent while its top-ranked compounds are mostly false positives.References
Fawcett, T. (2006). “An Introduction to ROC Analysis.” Pattern Recognit. Lett., 27(8), 861-874. https://doi.org/10.1016/j.patrec.2005.10.010
Truchon, J.-F. & Bayly, C. I. (2007). “Evaluating Virtual Screening Methods.” J. Chem. Inf. Model., 47(2), 488-508. https://doi.org/10.1021/ci600426e
- qsarkit.reporting.plot_learning_curve(train_sizes, train_scores, test_scores, title='Learning curve')[source]¶
Plot training and validation score against training-set size.
Answers whether more data would help. A gap that stays wide as the curves flatten means the model is over-fitting and needs regularization, not compounds; curves still rising means measuring more compounds is the better investment.
- Parameters:
train_sizes (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])train_scores (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])test_scores (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])title (
str) – Figure title.
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_learning_curve >>> sizes = np.array([10, 20, 40, 80]) >>> train = np.array([[0.99, 0.98], [0.97, 0.96], [0.95, 0.94], [0.93, 0.92]]) >>> test = np.array([[0.40, 0.45], [0.55, 0.58], [0.68, 0.70], [0.74, 0.75]]) >>> figure = plot_learning_curve(sizes, train, test) >>> type(figure).__name__ 'Figure'
Read the gap, not the level. A training score far above the validation score that stays apart as data is added means the model is memorizing; converging curves mean more data would help.
References
Perlich, C. (2010). “Learning Curves in Machine Learning.” In Encyclopedia of Machine Learning. Springer. https://doi.org/10.1007/978-0-387-30164-8_452
scikit-learn learning curve documentation: https://scikit-learn.org/stable/modules/learning_curve.html
- qsarkit.reporting.plot_feature_importance(names, importances, errors=None, top_n=20, title='Feature importance')[source]¶
Horizontal bar chart of the most important descriptors.
- Parameters:
importances (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]])errors (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Error bars, e.g. the standard deviation across permutation repeats.top_n (
int) – Number of features shown.title (
str) – Figure title.
- Return type:
Figure
Examples
>>> import numpy as np >>> from qsarkit.reporting import plot_feature_importance >>> names = ["MolWt", "MolLogP", "TPSA", "NumHDonors"] >>> figure = plot_feature_importance(names, [0.4, 0.3, 0.2, 0.1]) >>> type(figure).__name__ 'Figure'
Error bars turn a ranking into a claim you can judge. Two features whose intervals overlap are not distinguishable by this much data:
>>> figure = plot_feature_importance( ... names, [0.4, 0.3, 0.2, 0.1], errors=[0.05, 0.08, 0.06, 0.03]) >>> len(figure.data) 1
top_ntruncates a long list, which is the usual case with fingerprints:>>> many = [f"bit_{i}" for i in range(500)] >>> figure = plot_feature_importance(many, np.linspace(0, 1, 500), top_n=10) >>> len(figure.data[0].y) 10
References
Breiman, L. (2001). “Random Forests.” Mach. Learn., 45, 5-32. https://doi.org/10.1023/A:1010933404324
Lundberg, S. M. et al. (2020). “From Local Explanations to Global Understanding with Explainable AI for Trees.” Nat. Mach. Intell., 2, 56-67. https://doi.org/10.1038/s42256-019-0138-9
References¶
OECD (2007). “Guidance Document on the Validation of (Quantitative) Structure-Activity Relationship [(Q)SAR] Models,” ENV/JM/MONO(2007)2. doi:10.1787/9789264085442-en
European Chemicals Agency (2016). “Practical Guide: How to Use and Report (Q)SARs.” doi:10.2823/81818
Gramatica, P. (2007). “Principles of QSAR Models Validation: Internal and External.” QSAR Comb. Sci., 26(5), 694-701. doi:10.1002/qsar.200610151