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
32 changes: 32 additions & 0 deletions src/rtichoke/performance_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
DEFAULT_COLORS,
render_performance_table_reactable,
)
from rtichoke.processing.evaluation_semantics import _build_evaluation_metadata

PerformanceTableRenderer = Literal["great_tables", "reactable"]

Expand All @@ -30,6 +31,26 @@
]


def _with_evaluation_group_role(
performance_data: pl.DataFrame,
probs: Dict[str, np.ndarray],
reals: Union[np.ndarray, Dict[str, np.ndarray]],
times: Union[np.ndarray, Dict[str, np.ndarray], None] = None,
) -> pl.DataFrame:
"""Attach transient semantic role metadata for table rendering only."""
metadata = _build_evaluation_metadata(
probs,
reals,
np.array([]) if times is None else times,
)
group_role = (
"model"
if all(evaluation.model is not None for evaluation in metadata.values())
else "population"
)
return performance_data.with_columns(pl.lit(group_role).alias("__group_role"))


def render_performance_table(
performance_data: pl.DataFrame,
color_values: Sequence[str] = DEFAULT_COLORS,
Expand Down Expand Up @@ -59,6 +80,11 @@ def create_performance_table(
performance_data = prepare_performance_data(
probs=probs, reals=reals, by=by, stratified_by=stratified_by
)
performance_data = _with_evaluation_group_role(
performance_data,
probs=probs,
reals=reals,
)
return render_performance_table(
performance_data, color_values=color_values, renderer=renderer
)
Expand Down Expand Up @@ -102,6 +128,12 @@ def create_performance_table_times(
by=by,
stratified_by=stratified_by,
)
performance_data = _with_evaluation_group_role(
performance_data,
probs=probs,
reals=reals,
times=normalized_times,
)
return render_performance_table(
performance_data, color_values=color_values, renderer=renderer
)
28 changes: 17 additions & 11 deletions src/rtichoke/performance_table_great_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ def render_performance_table_great_tables(
raise ValueError("performance_data must contain exactly one stratification")
stratified_by = stratifications[0]

group_label = "Model"
if "__group_role" in performance_data.columns:
group_roles = performance_data.get_column("__group_role").unique().to_list()
if group_roles == ["population"]:
group_label = "Population"

context_columns = [
"fixed_time_horizon",
"censoring_heuristic",
Expand All @@ -106,7 +112,7 @@ def render_performance_table_great_tables(
[c for c in source_columns if c in performance_data.columns]
)
if "reference_group" in data.columns:
data = data.rename({"reference_group": "Model"})
data = data.rename({"reference_group": group_label})
if "fixed_time_horizon" in data.columns:
data = data.rename({"fixed_time_horizon": "Time"})
if "censoring_heuristic" in data.columns:
Expand All @@ -120,13 +126,13 @@ def render_performance_table_great_tables(
if stratified_by == "probability_threshold":
data = data.rename({"chosen_cutoff": "Threshold"})
sort_columns = context_sort + [
c for c in ("Threshold", "Model") if c in data.columns
c for c in ("Threshold", group_label) if c in data.columns
]
else:
if "chosen_cutoff" in data.columns:
data = data.drop("chosen_cutoff")
sort_columns = context_sort + [
c for c in ("ppcr", "Model") if c in data.columns
c for c in ("ppcr", group_label) if c in data.columns
]
if sort_columns:
data = data.sort(sort_columns)
Expand All @@ -153,7 +159,7 @@ def render_performance_table_great_tables(
display_columns = [
c
for c in [
"Model",
group_label,
"Time",
"Censoring",
"Competing Event",
Expand All @@ -171,7 +177,7 @@ def render_performance_table_great_tables(
display = data.select(display_columns)

labels = {
"Model": "Model",
group_label: group_label,
"Time": "Time Horizon",
"Censoring": "Censoring",
"Competing Event": "Competing Event",
Expand Down Expand Up @@ -210,20 +216,20 @@ def render_performance_table_great_tables(
locations=loc.body(columns="net_benefit"),
)

if "Model" in data.columns:
models = data.get_column("Model").unique(maintain_order=True).to_list()
for index, model in enumerate(models):
if group_label in data.columns:
groups = data.get_column(group_label).unique(maintain_order=True).to_list()
for index, group in enumerate(groups):
color = color_values[index % len(color_values)]
rows = [
i
for i, value in enumerate(data.get_column("Model").to_list())
if value == model
for i, value in enumerate(data.get_column(group_label).to_list())
if value == group
]
table = table.tab_style(
style=style.css(
f"color:{color};font-weight:600;text-shadow:0 0 0 currentColor;"
),
locations=loc.body(columns="Model", rows=rows),
locations=loc.body(columns=group_label, rows=rows),
)

lift_max = float(
Expand Down
26 changes: 16 additions & 10 deletions src/rtichoke/performance_table_reactable.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _net_benefit_style(value: float | None, maximum: float) -> dict[str, str]:
if value is None or not np.isfinite(value) or maximum <= 0:
return {}
width = max(-1.0, min(float(value) / maximum, 1.0))
position = (0.5 + width / 2) * 100
position = (0.5 + width / 2.0) * 100
if width >= 0:
background = (
"linear-gradient(90deg, transparent 50%, lightgreen 50%, "
Expand Down Expand Up @@ -95,6 +95,12 @@ def render_performance_table_reactable(
raise ValueError("performance_data must contain exactly one stratification")
stratified_by = stratifications[0]

group_label = "Model"
if "__group_role" in performance_data.columns:
group_roles = performance_data.get_column("__group_role").unique().to_list()
if group_roles == ["population"]:
group_label = "Population"

display_columns = [
"reference_group",
"fixed_time_horizon",
Expand All @@ -118,7 +124,7 @@ def render_performance_table_reactable(
[c for c in display_columns if c in performance_data.columns]
)
rename_map = {
"reference_group": "Model",
"reference_group": group_label,
"fixed_time_horizon": "Time",
"censoring_heuristic": "Censoring",
"competing_heuristic": "Competing Event",
Expand All @@ -131,13 +137,13 @@ def render_performance_table_reactable(
if stratified_by == "probability_threshold":
data = data.rename({"chosen_cutoff": "Threshold"})
sort_columns = context_sort + [
c for c in ("Threshold", "Model") if c in data.columns
c for c in ("Threshold", group_label) if c in data.columns
]
else:
if "chosen_cutoff" in data.columns:
data = data.drop("chosen_cutoff")
sort_columns = context_sort + [
c for c in ("ppcr", "Model") if c in data.columns
c for c in ("ppcr", group_label) if c in data.columns
]
if sort_columns:
data = data.sort(sort_columns)
Expand All @@ -150,16 +156,16 @@ def render_performance_table_reactable(
data.get_column("net_benefit").drop_nulls().abs().max() or 1.0,
)

models = (
data.get_column("Model").unique(maintain_order=True).to_list()
if "Model" in data.columns
groups = (
data.get_column(group_label).unique(maintain_order=True).to_list()
if group_label in data.columns
else []
)
colors = {
model: color_values[i % len(color_values)] for i, model in enumerate(models)
group: color_values[i % len(color_values)] for i, group in enumerate(groups)
}

def model_cell(info: CellInfo):
def group_cell(info: CellInfo):
value = info.value
color = colors.get(value, "#aaa")
return html.span(
Expand Down Expand Up @@ -239,7 +245,7 @@ def matrix_style(colors: tuple[str, str, str]):
)
return html.div(nested.to_widget(), style="padding:16px;")

columns = [Column(id="Model", cell=model_cell, min_width=120)]
columns = [Column(id=group_label, cell=group_cell, min_width=120)]
if "Time" in data.columns:
columns.append(
Column(
Expand Down
97 changes: 97 additions & 0 deletions tests/test_performance_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from great_tables import GT
from reactable import Reactable

import rtichoke.performance_table as performance_table_module
from rtichoke.performance_table_reactable import _bar_style, _net_benefit_style

from rtichoke import (
Expand All @@ -27,6 +28,14 @@ def _time_example():
return probs, reals, times


def _capture_table_data(monkeypatch):
monkeypatch.setattr(
performance_table_module,
"render_performance_table",
lambda performance_data, **kwargs: performance_data,
)


def test_create_performance_table_defaults_to_great_tables():
probs, reals = _example()
assert isinstance(create_performance_table(probs, reals, by=0.1), GT)
Expand Down Expand Up @@ -60,6 +69,35 @@ def test_create_performance_table_supports_ppcr_stratification():
)


def test_create_performance_table_propagates_model_role(monkeypatch):
_capture_table_data(monkeypatch)
probs = {
"Model A": np.array([0.1, 0.2, 0.8, 0.9]),
"Model B": np.array([0.2, 0.3, 0.7, 0.8]),
}
reals = np.array([0, 0, 1, 1])

data = performance_table_module.create_performance_table(probs, reals, by=0.25)

assert data.get_column("__group_role").unique().to_list() == ["model"]


def test_create_performance_table_propagates_population_role(monkeypatch):
_capture_table_data(monkeypatch)
probs = {
"Population A": np.array([0.1, 0.2, 0.8, 0.9]),
"Population B": np.array([0.2, 0.3, 0.7, 0.8]),
}
reals = {
"Population A": np.array([0, 0, 1, 1]),
"Population B": np.array([0, 1, 0, 1]),
}

data = performance_table_module.create_performance_table(probs, reals, by=0.25)

assert data.get_column("__group_role").unique().to_list() == ["population"]


def test_create_performance_table_times_defaults_to_great_tables():
probs, reals, times = _time_example()
assert isinstance(
Expand All @@ -80,6 +118,65 @@ def test_create_performance_table_times_supports_reactable():
)


def test_create_performance_table_times_propagates_model_role(monkeypatch):
_capture_table_data(monkeypatch)
probs = {
"Model A": np.array([0.1, 0.2, 0.8, 0.9]),
"Model B": np.array([0.2, 0.3, 0.7, 0.8]),
}
reals = np.array([0, 0, 1, 1])
times = np.array([2.0, 6.0, 3.0, 7.0])

data = performance_table_module.create_performance_table_times(
probs,
reals,
times,
fixed_time_horizons=[5],
by=0.25,
)

assert data.get_column("__group_role").unique().to_list() == ["model"]


def test_create_performance_table_times_propagates_population_role(monkeypatch):
_capture_table_data(monkeypatch)
probs = {
"Population A": np.array([0.1, 0.2, 0.8, 0.9]),
"Population B": np.array([0.2, 0.3, 0.7, 0.8]),
}
reals = {
"Population A": np.array([0, 0, 1, 1]),
"Population B": np.array([0, 1, 0, 1]),
}
times = {
"Population A": np.array([2.0, 6.0, 3.0, 7.0]),
"Population B": np.array([1.0, 4.0, 6.0, 8.0]),
}

data = performance_table_module.create_performance_table_times(
probs,
reals,
times,
fixed_time_horizons=[5],
by=0.25,
)

assert data.get_column("__group_role").unique().to_list() == ["population"]


def test_prepare_performance_data_times_schema_has_no_transient_group_role():
probs, reals, times = _time_example()
data = prepare_performance_data_times(
probs,
reals,
times.astype(float),
fixed_time_horizons=[5],
by=0.1,
)

assert "__group_role" not in data.columns


def test_render_performance_table_preserves_multiple_time_horizons():
probs, reals, times = _time_example()
data = prepare_performance_data_times(
Expand Down
Loading