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.7.0.tar.gz",
f"{prefix}rtichoke-viz-0.9.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.6.0.tar.gz" not in names
assert f"{prefix}rtichoke-viz-0.7.0.tar.gz" not in names
PY

- name: Run tests
Expand Down
217 changes: 216 additions & 1 deletion src/rtichoke/_decision_curve_viz_spec_v2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Canonical static Decision Curve v2 adapter.
"""Canonical Decision Curve v2 adapters.

This module translates already-computed production Decision Curve quantities
into the shared rtichoke_viz contract. It deliberately does not recompute model
Expand All @@ -21,6 +21,12 @@
"n",
}

_REQUIRED_TIMES_COLUMNS = _REQUIRED_COLUMNS | {
"fixed_time_horizon",
"censoring_heuristic",
"competing_heuristic",
}


def _decision_curve_v2_spec_from_performance_data(
performance_data: pl.DataFrame,
Expand Down Expand Up @@ -172,3 +178,212 @@ def _decision_curve_v2_spec_from_performance_data(
"yAxis": {"label": "Net benefit"},
"references": references,
}


def _decision_curve_times_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 time-dependent Decision Curve v2 from production quantities."""
missing = _REQUIRED_TIMES_COLUMNS.difference(performance_data.columns)
if missing:
raise ValueError(
"Time-dependent Decision Curve performance data is missing columns: "
+ ", ".join(sorted(missing))
)

rows = (
performance_data.filter(
pl.col("chosen_cutoff").is_finite() & pl.col("net_benefit").is_finite()
)
.select(
"reference_group",
"fixed_time_horizon",
"censoring_heuristic",
"competing_heuristic",
"chosen_cutoff",
"net_benefit",
"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(
"Time-dependent Decision Curve 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)
}

evaluations: 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)

series_keys = list(
dict.fromkeys(
(
str(row["reference_group"]),
float(row["fixed_time_horizon"]),
str(row["censoring_heuristic"]),
str(row["competing_heuristic"]),
)
for row in rows
)
)
series_ids = {
key: f"series-{index}" for index, key in enumerate(series_keys, start=1)
}
series: list[dict[str, object]] = []
for key in series_keys:
group, horizon, _, _ = key
metadata = evaluation_metadata[group]
display_value = metadata.model or metadata.population
series.append(
{
"id": series_ids[key],
"evaluationId": evaluation_ids[group],
"horizon": horizon,
"display": {
"label": display_value,
"group": display_value,
"role": "model" if metadata.model is not None else "population",
},
}
)

data = []
for row in rows:
key = (
str(row["reference_group"]),
float(row["fixed_time_horizon"]),
str(row["censoring_heuristic"]),
str(row["competing_heuristic"]),
)
data.append(
{
"seriesId": series_ids[key],
"threshold": float(row["chosen_cutoff"]),
"netBenefit": float(row["net_benefit"]),
}
)

# Cutoff 0 event risk (AJ estimate) per (population, horizon)
group_risks = (
performance_data.filter(pl.col("chosen_cutoff") == 0)
.select(
"reference_group",
"fixed_time_horizon",
(pl.col("real_positives") / pl.col("n")).alias("event_risk"),
)
.unique()
.to_dicts()
)
values: dict[tuple[str, float], set[float]] = {}
for row in group_risks:
group = str(row["reference_group"])
metadata = evaluation_metadata.get(group)
if metadata is None:
continue
key = (metadata.population, float(row["fixed_time_horizon"]))
values.setdefault(key, set()).add(float(row["event_risk"]))

populations = list(
dict.fromkeys(metadata.population for metadata in evaluation_metadata.values())
)
horizons = sorted(
float(value)
for value in performance_data["fixed_time_horizon"].unique().to_list()
)
population_horizon_risks: dict[tuple[str, float], float] = {}
for key in (
(population, horizon)
for horizon in horizons
for population in populations
if (population, horizon) in values
):
candidates = values[key]
if len(candidates) != 1:
raise ValueError(
"Time-dependent Decision Curve must have one calculated event risk per "
f"population and horizon: {key[0]} at {key[1]}"
)
population_horizon_risks[key] = next(iter(candidates))

# Thresholds per (population, horizon)
population_horizon_thresholds: dict[tuple[str, float], list[float]] = {}
for row in rows:
group = str(row["reference_group"])
population = evaluation_metadata[group].population
horizon = float(row["fixed_time_horizon"])
threshold = float(row["chosen_cutoff"])
if 0.0 <= threshold < 1.0:
population_horizon_thresholds.setdefault((population, horizon), []).append(
threshold
)

references: list[dict[str, object]] = [
{
"type": "horizontal",
"scope": "global",
"value": 0.0,
"label": "Treat None",
"benchmark": "treat_none",
}
]

for (population, horizon), event_risk in population_horizon_risks.items():
thresholds = sorted(
set(population_horizon_thresholds.get((population, horizon), []))
)
references.append(
{
"type": "path",
"scope": "population_horizon",
"population": population,
"horizon": horizon,
"label": f"Treat All — {population}",
"benchmark": "treat_all",
"points": [
{
"x": threshold,
"y": event_risk
- (1.0 - event_risk) * threshold / (1.0 - threshold),
}
for threshold in thresholds
],
}
)

return {
"schemaVersion": "2.0",
"type": "decision_curve",
"evaluations": evaluations,
"series": series,
"data": data,
"x": "threshold",
"y": "netBenefit",
"xAxis": {
"label": "Probability threshold",
"domain": [min_p_threshold, max_p_threshold],
},
"yAxis": {"label": "Net benefit"},
"references": references,
}
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.7.0
source_commit=b3564d2824ec1791f791fda406c99b3d7865a68f
archive=rtichoke-viz-0.7.0.tar.gz
sha256=f09c30e231a8be39c2e89ba6ae39c90ed8cab67021213e17681a475066a9806e
release=v0.9.0
source_commit=56e2ba95f83c889385c38619571368f74250d428
archive=rtichoke-viz-0.9.0.tar.gz
sha256=6a231c7bc951cdd3f5381e2a0937036a9d9f62b8ea1b36d8dbf81f62c6188ef8
Binary file not shown.
Binary file not shown.
Loading
Loading