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
3 changes: 3 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Check lint
run: uv run ruff check .

- name: Check format
run: uv run ruff format --check .

- name: Build package
run: uv build

Expand Down
33 changes: 27 additions & 6 deletions examples/performance_table_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ def _(mo):
@app.cell
def _(np):
reals = np.array([0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1])
model_a = np.array([0.04, 0.10, 0.20, 0.24, 0.33, 0.42, 0.48, 0.61, 0.70, 0.82, 0.86, 0.94])
model_b = np.array([0.08, 0.18, 0.14, 0.39, 0.30, 0.50, 0.43, 0.57, 0.65, 0.74, 0.76, 0.88])
model_a = np.array(
[0.04, 0.10, 0.20, 0.24, 0.33, 0.42, 0.48, 0.61, 0.70, 0.82, 0.86, 0.94]
)
model_b = np.array(
[0.08, 0.18, 0.14, 0.39, 0.30, 0.50, 0.43, 0.57, 0.65, 0.74, 0.76, 0.88]
)
return model_a, model_b, reals


Expand All @@ -45,20 +49,31 @@ def _(create_performance_table, mo, model_a, reals):
@app.cell
def _(create_performance_table, mo, model_a, model_b, reals):
mo.md("## Multiple models — probability threshold")
create_performance_table(probs={"Model A": model_a, "Model B": model_b}, reals=reals, by=0.10)
create_performance_table(
probs={"Model A": model_a, "Model B": model_b}, reals=reals, by=0.10
)
return


@app.cell
def _(create_performance_table, mo, model_a, model_b, reals):
mo.md("## Multiple models — PPCR")
create_performance_table(probs={"Model A": model_a, "Model B": model_b}, reals=reals, by=0.10, stratified_by=("ppcr",))
create_performance_table(
probs={"Model A": model_a, "Model B": model_b},
reals=reals,
by=0.10,
stratified_by=("ppcr",),
)
return


@app.cell
def _(np):
time_probs = {"Model A": np.array([0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 1.00])}
time_probs = {
"Model A": np.array(
[0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 1.00]
)
}
time_reals = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1, 1])
times = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
return time_probs, time_reals, times
Expand All @@ -67,7 +82,13 @@ def _(np):
@app.cell
def _(create_performance_table_times, mo, time_probs, time_reals, times):
mo.md("## Fixed time horizons — 5 and 10")
create_performance_table_times(probs=time_probs, reals=time_reals, times=times, fixed_time_horizons=[5, 10], by=0.10)
create_performance_table_times(
probs=time_probs,
reals=time_reals,
times=times,
fixed_time_horizons=[5, 10],
by=0.10,
)
return


Expand Down
4 changes: 1 addition & 3 deletions src/rtichoke/calibration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ def _validate_outcome_values(reals, allowed_values):
if not np.all(np.isin(np.asarray(outcome_values), allowed_values)):
if allowed_values == (0, 1):
raise ValueError("Binary outcomes must contain only 0 and 1.")
raise ValueError(
"Time-dependent outcomes must contain only 0, 1, and 2."
)
raise ValueError("Time-dependent outcomes must contain only 0, 1, and 2.")


def create_calibration_curve(*args, **kwargs):
Expand Down
5 changes: 1 addition & 4 deletions src/rtichoke/calibration/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,9 +772,7 @@ def process_single_array(p, r, group_name):
)
else:
smoothed = smooth_state_lowess(p, r)
return smoothed.with_columns(
pl.lit(group_name).alias("reference_group")
)
return smoothed.with_columns(pl.lit(group_name).alias("reference_group"))

if isinstance(reals, dict):
for model_name, prob_array in probs.items():
Expand Down Expand Up @@ -1171,7 +1169,6 @@ def _make_adjusted_deciles_data(
return pl.DataFrame(rows).sort(["reference_group", "decile"])



def _calculate_local_aj_smooth(
df_adj: pl.DataFrame,
horizon: float,
Expand Down
4 changes: 3 additions & 1 deletion src/rtichoke/discrimination/gains.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
_check_if_multiple_populations_are_being_validated_times,
)
from rtichoke.processing.time_reference_lines import _apply_color_values_times
from rtichoke.performance_data.performance_data_times import prepare_performance_data_times
from rtichoke.performance_data.performance_data_times import (
prepare_performance_data_times,
)
import numpy as np
import polars as pl

Expand Down
4 changes: 3 additions & 1 deletion src/rtichoke/performance_data/performance_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def _validate_and_align_binary_inputs(
) -> Union[np.ndarray, Dict[str, np.ndarray]]:
"""Validate binary input alignment and normalize outcome dictionaries."""
if not isinstance(probs, dict) or not probs:
raise ValueError("`probs` must be a non-empty dictionary of probability arrays.")
raise ValueError(
"`probs` must be a non-empty dictionary of probability arrays."
)

groups = list(probs)
for group in groups:
Expand Down
2 changes: 1 addition & 1 deletion src/rtichoke/performance_data/performance_data_times.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def prepare_performance_data_times(
float(horizon): index for index, horizon in enumerate(fixed_time_horizons)
}
heuristic_order = {
f'{heuristics["censoring_heuristic"]}\x1f{heuristics["competing_heuristic"]}': index
f"{heuristics['censoring_heuristic']}\x1f{heuristics['competing_heuristic']}": index
for index, heuristics in enumerate(heuristics_sets)
}

Expand Down
28 changes: 21 additions & 7 deletions src/rtichoke/performance_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
import polars as pl

from rtichoke.performance_data.performance_data import prepare_performance_data
from rtichoke.performance_data.performance_data_times import prepare_performance_data_times
from rtichoke.performance_table_great_tables import render_performance_table_great_tables
from rtichoke.performance_data.performance_data_times import (
prepare_performance_data_times,
)
from rtichoke.performance_table_great_tables import (
render_performance_table_great_tables,
)
from rtichoke.performance_table_reactable import (
DEFAULT_COLORS,
render_performance_table_reactable,
Expand All @@ -33,9 +37,13 @@ def render_performance_table(
):
"""Render prepared performance data with a selected table backend."""
if renderer == "great_tables":
return render_performance_table_great_tables(performance_data, color_values=color_values)
return render_performance_table_great_tables(
performance_data, color_values=color_values
)
if renderer == "reactable":
return render_performance_table_reactable(performance_data, color_values=color_values)
return render_performance_table_reactable(
performance_data, color_values=color_values
)
raise ValueError("renderer must be either 'great_tables' or 'reactable'")


Expand All @@ -48,8 +56,12 @@ def create_performance_table(
renderer: PerformanceTableRenderer = "great_tables",
):
"""Create an R-style rtichoke performance table."""
performance_data = prepare_performance_data(probs=probs, reals=reals, by=by, stratified_by=stratified_by)
return render_performance_table(performance_data, color_values=color_values, renderer=renderer)
performance_data = prepare_performance_data(
probs=probs, reals=reals, by=by, stratified_by=stratified_by
)
return render_performance_table(
performance_data, color_values=color_values, renderer=renderer
)


def create_performance_table_times(
Expand Down Expand Up @@ -88,4 +100,6 @@ def create_performance_table_times(
by=by,
stratified_by=stratified_by,
)
return render_performance_table(performance_data, color_values=color_values, renderer=renderer)
return render_performance_table(
performance_data, color_values=color_values, renderer=renderer
)
98 changes: 77 additions & 21 deletions src/rtichoke/performance_table_great_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,32 @@


DEFAULT_COLORS = (
"#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#07004D",
"#E6AB02", "#FE5F55", "#54494B", "#006E90", "#BC96E6",
"#52050A", "#1F271B", "#BE7C4D", "#63768D", "#08A045",
"#320A28", "#82FF9E", "#2176FF", "#D1603D", "#585123",
"#1b9e77",
"#d95f02",
"#7570b3",
"#e7298a",
"#07004D",
"#E6AB02",
"#FE5F55",
"#54494B",
"#006E90",
"#BC96E6",
"#52050A",
"#1F271B",
"#BE7C4D",
"#63768D",
"#08A045",
"#320A28",
"#82FF9E",
"#2176FF",
"#D1603D",
"#585123",
)


def _bar_css(value: float | None, maximum: float = 1.0, color: str = "lightgreen") -> str:
def _bar_css(
value: float | None, maximum: float = 1.0, color: str = "lightgreen"
) -> str:
"""CSS matching rtichoke R's reactable metric bars."""
if value is None or not np.isfinite(value) or maximum <= 0:
width = 0.0
Expand Down Expand Up @@ -71,11 +89,21 @@ def render_performance_table_great_tables(
"competing_heuristic",
]
source_columns = [
"reference_group", *context_columns, "chosen_cutoff", "sensitivity",
"specificity", "ppv", "npv", "lift", "predicted_positives",
"net_benefit", "ppcr",
"reference_group",
*context_columns,
"chosen_cutoff",
"sensitivity",
"specificity",
"ppv",
"npv",
"lift",
"predicted_positives",
"net_benefit",
"ppcr",
]
data = performance_data.select([c for c in source_columns if c in performance_data.columns])
data = performance_data.select(
[c for c in source_columns if c in performance_data.columns]
)
if "reference_group" in data.columns:
data = data.rename({"reference_group": "Model"})
if "fixed_time_horizon" in data.columns:
Expand All @@ -85,14 +113,20 @@ def render_performance_table_great_tables(
if "competing_heuristic" in data.columns:
data = data.rename({"competing_heuristic": "Competing Event"})

context_sort = [c for c in ("Time", "Censoring", "Competing Event") if c in data.columns]
context_sort = [
c for c in ("Time", "Censoring", "Competing Event") if c in data.columns
]
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]
sort_columns = context_sort + [
c for c in ("Threshold", "Model") 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]
sort_columns = context_sort + [
c for c in ("ppcr", "Model") if c in data.columns
]
if sort_columns:
data = data.sort(sort_columns)

Expand All @@ -111,14 +145,28 @@ def render_performance_table_great_tables(
data = data.drop("net_benefit")

metric_columns = [
c for c in ["sensitivity", "specificity", "ppv", "npv", "lift", "net_benefit"]
c
for c in ["sensitivity", "specificity", "ppv", "npv", "lift", "net_benefit"]
if c in data.columns
]
display_columns = [
c
for c in [
"Model",
"Time",
"Censoring",
"Competing Event",
"Threshold",
"Predicted Positives",
"sensitivity",
"specificity",
"ppv",
"npv",
"lift",
"net_benefit",
]
if c in data.columns
]
display_columns = [c for c in [
"Model", "Time", "Censoring", "Competing Event", "Threshold",
"Predicted Positives", "sensitivity", "specificity", "ppv", "npv",
"lift", "net_benefit",
] if c in data.columns]
display = data.select(display_columns)

labels = {
Expand Down Expand Up @@ -165,9 +213,15 @@ def render_performance_table_great_tables(
models = data.get_column("Model").unique(maintain_order=True).to_list()
for index, model in enumerate(models):
color = color_values[index % len(color_values)]
rows = [i for i, value in enumerate(data.get_column("Model").to_list()) if value == model]
rows = [
i
for i, value in enumerate(data.get_column("Model").to_list())
if value == model
]
table = table.tab_style(
style=style.css(f"color:{color};font-weight:600;text-shadow:0 0 0 currentColor;"),
style=style.css(
f"color:{color};font-weight:600;text-shadow:0 0 0 currentColor;"
),
locations=loc.body(columns="Model", rows=rows),
)

Expand Down Expand Up @@ -196,7 +250,9 @@ def render_performance_table_great_tables(
)
if "net_benefit" in data.columns:
table = table.tab_style(
style=style.css(_net_benefit_css(data[row_index, "net_benefit"], nb_max)),
style=style.css(
_net_benefit_css(data[row_index, "net_benefit"], nb_max)
),
locations=loc.body(columns="net_benefit", rows=[row_index]),
)

Expand Down
Loading
Loading