diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e3937211..fbcb937a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -93,7 +93,7 @@ jobs: grep -q '.Reactable {' performance-table-reactable.html grep -q 'Real Positive' performance-table-reactable.html mv performance-table-reactable.html great-docs/_site/performance-table-reactable.html - cp -R examples/performance_table_reactable_files great-docs/_site/performance-table-reactable_files + cp -R examples/performance_table_reactable_files great-docs/_site/performance_table_reactable_files - name: Generate rtichoke_viz ROC proof if: github.event.action != 'closed' @@ -102,6 +102,13 @@ jobs: mkdir -p great-docs/_site/rtichoke-viz-roc cp -R rtichoke_viz_roc_proof/. great-docs/_site/rtichoke-viz-roc/ + - name: Generate rtichoke_viz calibration proof + if: github.event.action != 'closed' + run: | + uv run python examples/rtichoke_viz_calibration.py + mkdir -p great-docs/_site/rtichoke-viz-calibration + cp -R rtichoke_viz_calibration_proof/. great-docs/_site/rtichoke-viz-calibration/ + - name: Deploy PR preview uses: rossjrw/pr-preview-action@v1 with: diff --git a/examples/rtichoke_viz_calibration.py b/examples/rtichoke_viz_calibration.py new file mode 100644 index 00000000..15f9c7ff --- /dev/null +++ b/examples/rtichoke_viz_calibration.py @@ -0,0 +1,22 @@ +"""Generate a browser-rendered calibration proof from real rtichoke output.""" + +from pathlib import Path + +import numpy as np + +from rtichoke._viz_browser import _write_calibration_browser_html +from rtichoke.calibration.calibration import _create_calibration_curve_list + +probs = { + "Model A": np.array( + [0.03, 0.08, 0.12, 0.18, 0.25, 0.32, 0.40, 0.50, 0.62, 0.75, 0.88, 0.96] + ) +} +reals = np.array([0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1]) + +calibration_curve_list = _create_calibration_curve_list(probs, reals) +output = _write_calibration_browser_html( + calibration_curve_list, + Path("rtichoke_viz_calibration_proof") / "index.html", +) +print(output) diff --git a/src/rtichoke/_viz_browser.py b/src/rtichoke/_viz_browser.py index ba154dbd..a5db2ee8 100644 --- a/src/rtichoke/_viz_browser.py +++ b/src/rtichoke/_viz_browser.py @@ -5,6 +5,7 @@ import json from importlib.resources import files from pathlib import Path +from typing import Any import polars as pl @@ -16,6 +17,16 @@ } +def _copy_viz_assets(output_dir: Path) -> None: + vendor = files("rtichoke").joinpath("_vendor", "rtichoke_viz") + (output_dir / "rtichoke-viz.js").write_bytes( + vendor.joinpath("rtichoke-viz.js").read_bytes() + ) + (output_dir / "rtichoke-viz.css").write_bytes( + vendor.joinpath("rtichoke-viz.css").read_bytes() + ) + + def _roc_spec_from_performance_data( performance_data: pl.DataFrame, ) -> dict[str, object]: @@ -52,6 +63,48 @@ def _roc_spec_from_performance_data( } +def _calibration_spec_from_curve_list( + calibration_curve_list: dict[str, Any], +) -> dict[str, object]: + """Map existing discrete calibration output to the canonical spec.""" + rows = calibration_curve_list["deciles_dat"].select( + "reference_group", "x", "y", "n_reals", "n" + ) + distribution_rows = calibration_curve_list["histogram_for_calibration"].select( + "reference_group", "mids", "counts" + ) + + return { + "schemaVersion": "1.0", + "type": "calibration", + "data": [ + { + "model": row["reference_group"], + "predicted": row["x"], + "observed": row["y"], + "method": "discrete", + "events": row["n_reals"], + "total": row["n"], + } + for row in rows.to_dicts() + ], + "distribution": [ + { + "model": row["reference_group"], + "midpoint": row["mids"], + "count": row["counts"], + "binWidth": 0.01, + } + for row in distribution_rows.to_dicts() + ], + "x": "predicted", + "y": "observed", + "xAxis": {"label": "Predicted probability", "domain": [0, 1]}, + "yAxis": {"label": "Observed probability", "domain": [0, 1]}, + "references": [{"type": "identity"}], + } + + def _write_roc_browser_html( performance_data: pl.DataFrame, output_path: str | Path, @@ -59,12 +112,7 @@ def _write_roc_browser_html( """Write a standalone proof page that uses the vendored browser renderer.""" output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) - - vendor = files("rtichoke").joinpath("_vendor", "rtichoke_viz") - js_source = vendor.joinpath("rtichoke-viz.js") - css_source = vendor.joinpath("rtichoke-viz.css") - (output.parent / "rtichoke-viz.js").write_bytes(js_source.read_bytes()) - (output.parent / "rtichoke-viz.css").write_bytes(css_source.read_bytes()) + _copy_viz_assets(output.parent) spec_json = json.dumps(_roc_spec_from_performance_data(performance_data)).replace( "", "<\\/" @@ -90,3 +138,38 @@ def _write_roc_browser_html( """ output.write_text(html, encoding="utf-8") return output + + +def _write_calibration_browser_html( + calibration_curve_list: dict[str, Any], + output_path: str | Path, +) -> Path: + """Write a standalone discrete-calibration proof using vendored assets.""" + output = Path(output_path) + output.parent.mkdir(parents=True, exist_ok=True) + _copy_viz_assets(output.parent) + + spec_json = json.dumps( + _calibration_spec_from_curve_list(calibration_curve_list) + ).replace("", "<\\/") + html = f""" + +
+ + +