Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ jobs:
prefix = "rtichoke/_vendor/rtichoke_viz/"
required = {
f"{prefix}VENDORED_FROM",
f"{prefix}rtichoke-viz-0.6.0.tar.gz",
f"{prefix}rtichoke-viz-0.7.0.tar.gz",
f"{prefix}rtichoke-viz.js",
f"{prefix}rtichoke-viz.css",
f"{prefix}rtichoke-viz.schema.json",
f"{prefix}rtichoke-viz-v2.schema.json",
}
assert required <= names
assert f"{prefix}rtichoke-viz-0.5.0.tar.gz" not in names
assert f"{prefix}rtichoke-viz-0.6.0.tar.gz" not in names
PY

- name: Run tests
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<!--next-version-placeholder-->

- Added opt-in canonical browser rendering for static Interventions Avoided using the verified `rtichoke_viz v0.7.0` release while preserving Plotly as the default.
- Fixed Interventions Avoided to apply the per-100 scaling to the full model expression, including the false-negative penalty term.

## v0.1.36 (21/08/2026)
Expand All @@ -19,4 +20,4 @@

## v0.1.0 (27/01/2023)

- First release of `rtichoke`!
- First release of `rtichoke`!
179 changes: 179 additions & 0 deletions src/rtichoke/_interventions_avoided_viz_spec_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""Canonical static Interventions Avoided v2 adapter.

This module translates already-computed production Interventions Avoided
quantities into the shared rtichoke_viz contract. It deliberately does not
recompute model statistics or threshold membership.
"""

from __future__ import annotations

from collections.abc import Mapping

import polars as pl

from rtichoke.processing.evaluation_semantics import _EvaluationMetadata

_REQUIRED_COLUMNS = {
"reference_group",
"chosen_cutoff",
"net_benefit_interventions_avoided",
"real_positives",
"n",
}


def _interventions_avoided_v2_spec_from_performance_data(
performance_data: pl.DataFrame,
evaluation_metadata: Mapping[str, _EvaluationMetadata],
*,
min_p_threshold: float = 0.0,
max_p_threshold: float = 1.0,
) -> dict[str, object]:
"""Build canonical static Interventions Avoided from production quantities."""
missing = _REQUIRED_COLUMNS.difference(performance_data.columns)
if missing:
raise ValueError(
"Interventions Avoided performance data is missing columns: "
+ ", ".join(sorted(missing))
)

rows = (
performance_data.filter(
pl.col("chosen_cutoff").is_finite()
& pl.col("net_benefit_interventions_avoided").is_finite()
)
.select(
"reference_group",
"chosen_cutoff",
"net_benefit_interventions_avoided",
"real_positives",
"n",
)
.to_dicts()
)

row_groups = {str(row["reference_group"]) for row in rows}
missing_metadata = row_groups.difference(evaluation_metadata)
if missing_metadata:
raise ValueError(
"Interventions Avoided rows are missing evaluation metadata: "
+ ", ".join(sorted(missing_metadata))
)

ordered_groups = [group for group in evaluation_metadata if group in row_groups]
evaluation_ids = {
group: f"evaluation-{index}"
for index, group in enumerate(ordered_groups, start=1)
}
series_ids = {
group: f"series-{index}" for index, group in enumerate(ordered_groups, start=1)
}

evaluations: list[dict[str, object]] = []
series: list[dict[str, object]] = []
for group in ordered_groups:
metadata = evaluation_metadata[group]
evaluation: dict[str, object] = {
"id": evaluation_ids[group],
"population": metadata.population,
}
if metadata.model is not None:
evaluation["model"] = metadata.model
evaluations.append(evaluation)

display_value = metadata.model or metadata.population
series.append(
{
"id": series_ids[group],
"evaluationId": evaluation_ids[group],
"display": {
"label": display_value,
"group": display_value,
"role": "model" if metadata.model is not None else "population",
},
}
)

data = [
{
"seriesId": series_ids[str(row["reference_group"])],
"threshold": float(row["chosen_cutoff"]),
"interventionsAvoided": float(row["net_benefit_interventions_avoided"]),
}
for row in rows
]

prevalence_values: dict[str, set[float]] = {}
population_thresholds: dict[str, list[float]] = {}
for row in rows:
group = str(row["reference_group"])
population = evaluation_metadata[group].population
n = float(row["n"])
if n <= 0:
raise ValueError("Interventions Avoided population size must be positive.")
prevalence_values.setdefault(population, set()).add(
float(row["real_positives"]) / n
)
threshold = float(row["chosen_cutoff"])
if 0.0 < threshold <= 1.0:
population_thresholds.setdefault(population, []).append(threshold)

populations = list(
dict.fromkeys(metadata.population for metadata in evaluation_metadata.values())
)
references: list[dict[str, object]] = [
{
"type": "horizontal",
"scope": "global",
"value": 0.0,
"label": "Treat All",
"benchmark": "treat_all",
}
]
for population in populations:
values = prevalence_values.get(population, set())
if not values:
continue
if len(values) != 1:
raise ValueError(
f"Population {population!r} has inconsistent prevalence values."
)
prevalence = next(iter(values))
thresholds = sorted(set(population_thresholds.get(population, [])))
references.append(
{
"type": "path",
"scope": "population",
"population": population,
"label": f"Treat None — {population}",
"benchmark": "treat_none",
"points": [
{
"x": threshold,
"y": 100.0
* (
1.0
- prevalence
- prevalence * (1.0 - threshold) / threshold
),
}
for threshold in thresholds
],
}
)

return {
"schemaVersion": "2.0",
"type": "interventions_avoided",
"evaluations": evaluations,
"series": series,
"data": data,
"x": "threshold",
"y": "interventionsAvoided",
"xAxis": {
"label": "Probability Threshold",
"domain": [min_p_threshold, max_p_threshold],
},
"yAxis": {"label": "Interventions Avoided (per 100)"},
"references": references,
}
1 change: 1 addition & 0 deletions src/rtichoke/_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def write_html(self, path: str | Path) -> Path:
"gains": "renderGainsV2",
"lift": "renderLiftV2",
"decision_curve": "renderDecisionCurveV2",
"interventions_avoided": "renderInterventionsAvoidedV2",
}.get(str(self.spec.get("type")))
if render_export is None:
raise ValueError(
Expand Down
8 changes: 4 additions & 4 deletions src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repository=https://github.com/uriahf/rtichoke_viz
release=v0.6.0
source_commit=3abb3f07a598c3e22d5362a3f88e52bb6b52b083
archive=rtichoke-viz-0.6.0.tar.gz
sha256=625613c7f692ff50b7757a27bb6caf84e311971bde92593141393dbd897af3a2
release=v0.7.0
source_commit=b3564d2824ec1791f791fda406c99b3d7865a68f
archive=rtichoke-viz-0.7.0.tar.gz
sha256=f09c30e231a8be39c2e89ba6ae39c90ed8cab67021213e17681a475066a9806e
Binary file not shown.
Binary file not shown.
Loading
Loading