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
133 changes: 133 additions & 0 deletions src/rtichoke/_viz_spec_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,139 @@ def _precision_recall_v2_spec_from_performance_data(
return spec


def _precision_recall_times_v2_spec_from_performance_data(
performance_data: pl.DataFrame,
evaluation_metadata: Mapping[str, _EvaluationMetadata],
) -> dict[str, object]:
"""Build canonical time-dependent precision-recall from calculated production data."""
required = _REQUIRED_PRECISION_RECALL_COLUMNS | {
"fixed_time_horizon",
"censoring_heuristic",
"competing_heuristic",
}
missing = required.difference(performance_data.columns)
if missing:
raise ValueError(
"Time-dependent precision-recall performance data is missing columns: "
+ ", ".join(sorted(missing))
)

finite_rows = performance_data.filter(
pl.col("chosen_cutoff").is_finite()
& pl.col("sensitivity").is_finite()
& pl.col("ppv").is_finite()
)

rows = finite_rows.select(
"reference_group",
"fixed_time_horizon",
"censoring_heuristic",
"competing_heuristic",
"chosen_cutoff",
"sensitivity",
"ppv",
).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 precision-recall 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 = []
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 = []
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],
"cutoff": row["chosen_cutoff"],
"sensitivity": row["sensitivity"],
"ppv": row["ppv"],
}
)

risks = _gains_population_horizon_risk(performance_data, evaluation_metadata)
references = []
for (population, horizon), risk in risks.items():
references.append(
{
"type": "horizontal",
"scope": "population_horizon",
"population": population,
"horizon": horizon,
"value": risk,
"label": "Prevalence",
}
)

return {
"schemaVersion": "2.0",
"type": "precision_recall",
"evaluations": evaluations,
"series": series,
"data": data,
"x": "sensitivity",
"y": "ppv",
"xAxis": {"label": "Sensitivity", "domain": [0, 1]},
"yAxis": {"label": "Positive Predictive Value", "domain": [0, 1]},
"references": references,
}


def _gains_v2_spec_from_performance_data(
performance_data: pl.DataFrame,
evaluation_metadata: Mapping[str, _EvaluationMetadata],
Expand Down
41 changes: 36 additions & 5 deletions src/rtichoke/discrimination/precision_recall.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""

from typing import Any, Dict, List, Sequence, Union
from plotly.graph_objs._figure import Figure
from rtichoke.processing.binary_color_values import _apply_color_values_binary
from rtichoke.processing.plotly_helper_functions import (
_create_rtichoke_plotly_curve_binary,
Expand All @@ -16,8 +15,14 @@
import polars as pl

from rtichoke._renderers import RtichokeBrowserChart, _validate_renderer
from rtichoke._viz_spec_v2 import _precision_recall_v2_spec_from_performance_data
from rtichoke._viz_spec_v2 import (
_precision_recall_times_v2_spec_from_performance_data,
_precision_recall_v2_spec_from_performance_data,
)
from rtichoke.performance_data.performance_data import prepare_performance_data
from rtichoke.performance_data.performance_data_times import (
prepare_performance_data_times,
)
from rtichoke.processing.evaluation_semantics import (
_EvaluationMetadata,
_build_evaluation_metadata,
Expand Down Expand Up @@ -241,7 +246,8 @@ def create_precision_recall_curve_times(
"#D1603D",
"#585123",
],
) -> Figure:
renderer: str = "plotly",
) -> Any:
"""Creates a time-dependent Precision-Recall curve.

Generates a Precision-Recall curve for time-to-event models, evaluating
Expand All @@ -268,12 +274,37 @@ def create_precision_recall_curve_times(
The width and height of the plot in pixels. Defaults to 600.
color_values : List[str], optional
A list of hex color strings for the plot lines.
renderer : {"plotly", "browser", "rtichoke_viz"}, optional
Rendering backend. ``"plotly"`` remains the default. ``"browser"`` and
its ``"rtichoke_viz"`` alias return a canonical offline browser chart.

Returns
-------
Figure
A Plotly ``Figure`` object for the time-dependent Precision-Recall curve.
Figure or RtichokeBrowserChart
A Plotly ``Figure`` or canonical offline browser chart depending on ``renderer``.
"""
selected_renderer = _validate_renderer(renderer)
if selected_renderer != "plotly":
if selected_renderer == "matplotlib":
raise ValueError(
"Precision-Recall supports 'plotly', 'browser', and 'rtichoke_viz' "
"renderers."
)
performance_data = prepare_performance_data_times(
probs,
reals,
times,
fixed_time_horizons=fixed_time_horizons,
heuristics_sets=heuristics_sets,
by=by,
stratified_by=stratified_by,
)
evaluation_metadata = _build_evaluation_metadata(probs, reals, times)
spec = _precision_recall_times_v2_spec_from_performance_data(
performance_data,
evaluation_metadata,
)
return RtichokeBrowserChart(spec=spec, size=size)

fig = _create_rtichoke_plotly_curve_times_reference_safe(
probs,
Expand Down
5 changes: 3 additions & 2 deletions tests/test_precision_recall_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,10 @@ def test_default_and_explicit_plotly_behavior_are_unchanged():
assert pio.to_json(default_plot) == pio.to_json(explicit_plot)


def test_time_dependent_precision_recall_api_does_not_gain_renderer_selection():
def test_time_dependent_precision_recall_api_has_renderer_selection():
parameters = inspect.signature(create_precision_recall_curve_times).parameters
assert "renderer" not in parameters
assert "renderer" in parameters
assert parameters["renderer"].default == "plotly"


def test_vendored_v050_contains_static_precision_recall_contract_and_export():
Expand Down
Loading
Loading