From 2524be3bb9fef32523b58b6302f5cbd52b38be64 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 21:37:31 +0300 Subject: [PATCH 1/5] Run Ruff formatter baseline --- .github/workflows/ruff-format-baseline.yml | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/ruff-format-baseline.yml diff --git a/.github/workflows/ruff-format-baseline.yml b/.github/workflows/ruff-format-baseline.yml new file mode 100644 index 00000000..bfe3da15 --- /dev/null +++ b/.github/workflows/ruff-format-baseline.yml @@ -0,0 +1,37 @@ +name: Ruff format baseline + +on: + push: + branches: + - modernize/ruff-format-baseline + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: astral-sh/setup-uv@v6 + + - name: Install the project + run: uv sync --all-extras --dev + + - name: Apply Ruff format + run: uv run ruff format . + + - name: Commit formatter baseline + run: | + if git diff --quiet; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -- '*.py' + git commit -m "Apply Ruff format baseline" + git push From 3c6939397bf0fa2c4291ae88d6e14174f710a28e Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 21:38:01 +0300 Subject: [PATCH 2/5] Run Ruff baseline from draft PR --- .github/workflows/ruff-format-baseline.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff-format-baseline.yml b/.github/workflows/ruff-format-baseline.yml index bfe3da15..f5c9e965 100644 --- a/.github/workflows/ruff-format-baseline.yml +++ b/.github/workflows/ruff-format-baseline.yml @@ -1,9 +1,9 @@ name: Ruff format baseline on: - push: + pull_request: branches: - - modernize/ruff-format-baseline + - main permissions: contents: write @@ -15,6 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.head_ref }} token: ${{ secrets.GITHUB_TOKEN }} - uses: astral-sh/setup-uv@v6 From e8124ce34b5be81285c8f06358cc1ae90b49514a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:38:35 +0000 Subject: [PATCH 3/5] Apply Ruff format baseline --- examples/performance_table_demo.py | 33 +++- src/rtichoke/calibration/__init__.py | 4 +- src/rtichoke/calibration/calibration.py | 5 +- src/rtichoke/discrimination/gains.py | 4 +- .../performance_data/performance_data.py | 4 +- .../performance_data_times.py | 2 +- src/rtichoke/performance_table.py | 28 ++- .../performance_table_great_tables.py | 98 +++++++--- src/rtichoke/performance_table_reactable.py | 170 +++++++++++++----- src/rtichoke/processing/combinations.py | 4 +- .../processing/time_input_validation.py | 16 +- .../processing/time_reference_lines.py | 4 +- tests/test_calibration.py | 8 +- tests/test_calibration_interactive_aspect.py | 9 +- tests/test_decision_curve_dcurves_parity.py | 12 +- tests/test_gains_times.py | 6 +- tests/test_performance_table.py | 50 +++++- tests/test_reference_line_audit.py | 24 ++- tests/test_stratification_isolation.py | 8 +- tests/test_time_input_validation.py | 4 +- tests/test_time_reference_lines.py | 4 +- 21 files changed, 356 insertions(+), 141 deletions(-) diff --git a/examples/performance_table_demo.py b/examples/performance_table_demo.py index fe2b8900..4a1da3bc 100644 --- a/examples/performance_table_demo.py +++ b/examples/performance_table_demo.py @@ -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 @@ -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 @@ -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 diff --git a/src/rtichoke/calibration/__init__.py b/src/rtichoke/calibration/__init__.py index afcfae45..f41ee24b 100644 --- a/src/rtichoke/calibration/__init__.py +++ b/src/rtichoke/calibration/__init__.py @@ -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): diff --git a/src/rtichoke/calibration/calibration.py b/src/rtichoke/calibration/calibration.py index 05954caf..15879759 100644 --- a/src/rtichoke/calibration/calibration.py +++ b/src/rtichoke/calibration/calibration.py @@ -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(): @@ -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, diff --git a/src/rtichoke/discrimination/gains.py b/src/rtichoke/discrimination/gains.py index 0f3099a1..8155f683 100644 --- a/src/rtichoke/discrimination/gains.py +++ b/src/rtichoke/discrimination/gains.py @@ -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 diff --git a/src/rtichoke/performance_data/performance_data.py b/src/rtichoke/performance_data/performance_data.py index 814939a3..1e37d053 100644 --- a/src/rtichoke/performance_data/performance_data.py +++ b/src/rtichoke/performance_data/performance_data.py @@ -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: diff --git a/src/rtichoke/performance_data/performance_data_times.py b/src/rtichoke/performance_data/performance_data_times.py index 57b77b48..e7590d4c 100644 --- a/src/rtichoke/performance_data/performance_data_times.py +++ b/src/rtichoke/performance_data/performance_data_times.py @@ -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) } diff --git a/src/rtichoke/performance_table.py b/src/rtichoke/performance_table.py index 63445b92..91bbac25 100644 --- a/src/rtichoke/performance_table.py +++ b/src/rtichoke/performance_table.py @@ -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, @@ -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'") @@ -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( @@ -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 + ) diff --git a/src/rtichoke/performance_table_great_tables.py b/src/rtichoke/performance_table_great_tables.py index afaf55f4..410c22be 100644 --- a/src/rtichoke/performance_table_great_tables.py +++ b/src/rtichoke/performance_table_great_tables.py @@ -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 @@ -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: @@ -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) @@ -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 = { @@ -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), ) @@ -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]), ) diff --git a/src/rtichoke/performance_table_reactable.py b/src/rtichoke/performance_table_reactable.py index b60ae62a..1a332982 100644 --- a/src/rtichoke/performance_table_reactable.py +++ b/src/rtichoke/performance_table_reactable.py @@ -12,14 +12,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_style(value: float | None, maximum: float = 1.0, color: str = "lightgreen") -> dict[str, str]: +def _bar_style( + value: float | None, maximum: float = 1.0, color: str = "lightgreen" +) -> dict[str, str]: if value is None or not np.isfinite(value) or maximum <= 0: return {} width = min(abs(float(value)) / maximum, 1.0) * 100 @@ -77,12 +95,27 @@ def render_performance_table_reactable( stratified_by = stratifications[0] display_columns = [ - "reference_group", "fixed_time_horizon", "censoring_heuristic", - "competing_heuristic", "chosen_cutoff", "sensitivity", "specificity", - "ppv", "npv", "lift", "predicted_positives", "net_benefit", "ppcr", - "true_positives", "true_negatives", "false_positives", "false_negatives", + "reference_group", + "fixed_time_horizon", + "censoring_heuristic", + "competing_heuristic", + "chosen_cutoff", + "sensitivity", + "specificity", + "ppv", + "npv", + "lift", + "predicted_positives", + "net_benefit", + "ppcr", + "true_positives", + "true_negatives", + "false_positives", + "false_negatives", ] - data = performance_data.select([c for c in display_columns if c in performance_data.columns]) + data = performance_data.select( + [c for c in display_columns if c in performance_data.columns] + ) rename_map = { "reference_group": "Model", "fixed_time_horizon": "Time", @@ -91,14 +124,20 @@ def render_performance_table_reactable( } data = data.rename({k: v for k, v in rename_map.items() if k in data.columns}) - 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) @@ -107,17 +146,25 @@ def render_performance_table_reactable( if "net_benefit" in data.columns: nb_max = 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 else [] - colors = {model: color_values[i % len(color_values)] for i, model in enumerate(models)} + models = ( + data.get_column("Model").unique(maintain_order=True).to_list() + if "Model" in data.columns + else [] + ) + colors = { + model: color_values[i % len(color_values)] for i, model in enumerate(models) + } def model_cell(info: CellInfo): value = info.value color = colors.get(value, "#aaa") return html.span( - html.span(style=( - "display:inline-block;margin-right:8px;width:9px;height:9px;" - f"background-color:{color};border-radius:50%;" - )), + html.span( + style=( + "display:inline-block;margin-right:8px;width:9px;height:9px;" + f"background-color:{color};border-radius:50%;" + ) + ), str(value), ) @@ -139,12 +186,14 @@ def confusion_matrix(info: RowInfo): real_negative = fp + tn total = tp + fp + fn + tn - matrix = pl.DataFrame({ - "Outcome": ["Predicted Positive", "Predicted Negative", " "], - "Real Positive": [tp, fn, real_positive], - "Real Negative": [fp, tn, real_negative], - "Total": [predicted_positive, predicted_negative, total], - }) + matrix = pl.DataFrame( + { + "Outcome": ["Predicted Positive", "Predicted Negative", " "], + "Real Positive": [tp, fn, real_positive], + "Real Negative": [fp, tn, real_negative], + "Total": [predicted_positive, predicted_negative, total], + } + ) def matrix_cell(info: CellInfo) -> str: if info.value is None or total == 0: @@ -152,7 +201,9 @@ def matrix_cell(info: CellInfo) -> str: return f"{info.value} ({float(info.value) / total * 100:.2f}%)" def matrix_style(colors: tuple[str, str, str]): - return lambda info: _bar_style(info.value, float(total), colors[info.row_index]) + return lambda info: _bar_style( + info.value, float(total), colors[info.row_index] + ) nested = Reactable( matrix, @@ -186,30 +237,57 @@ def matrix_style(colors: tuple[str, str, str]): columns = [Column(id="Model", cell=model_cell, min_width=120)] if "Time" in data.columns: - columns.append(Column(id="Time", name="Time Horizon", format=ColFormat(digits=2), min_width=100)) + columns.append( + Column( + id="Time", + name="Time Horizon", + format=ColFormat(digits=2), + min_width=100, + ) + ) if "Censoring" in data.columns: columns.append(Column(id="Censoring", name="Censoring", min_width=110)) if "Competing Event" in data.columns: - columns.append(Column(id="Competing Event", name="Competing Event", min_width=140)) + columns.append( + Column(id="Competing Event", name="Competing Event", min_width=140) + ) if stratified_by == "probability_threshold": - columns.append(Column(id="Threshold", name="Probability Threshold", format=ColFormat(digits=2), min_width=130)) - columns.extend([ - Column(id="ppcr", name="Predicted Positives", cell=ppcr_cell, min_width=150, - style=lambda info: _bar_style(info.value, 1.0, "#d3d3d3")), - Column(id="predicted_positives", show=False), - _metric_column("sensitivity", "Sens"), - _metric_column("specificity", "Spec"), - _metric_column("ppv", "PPV"), - _metric_column("npv", "NPV"), - _metric_column("lift", "Lift", float(lift_max)), - Column(id="net_benefit", name="Net Benefit", format=ColFormat(digits=2), - style=lambda info: _net_benefit_style(info.value, float(nb_max)), - show=stratified_by == "probability_threshold"), - Column(id="true_positives", show=False), - Column(id="true_negatives", show=False), - Column(id="false_positives", show=False), - Column(id="false_negatives", show=False), - ]) + columns.append( + Column( + id="Threshold", + name="Probability Threshold", + format=ColFormat(digits=2), + min_width=130, + ) + ) + columns.extend( + [ + Column( + id="ppcr", + name="Predicted Positives", + cell=ppcr_cell, + min_width=150, + style=lambda info: _bar_style(info.value, 1.0, "#d3d3d3"), + ), + Column(id="predicted_positives", show=False), + _metric_column("sensitivity", "Sens"), + _metric_column("specificity", "Spec"), + _metric_column("ppv", "PPV"), + _metric_column("npv", "NPV"), + _metric_column("lift", "Lift", float(lift_max)), + Column( + id="net_benefit", + name="Net Benefit", + format=ColFormat(digits=2), + style=lambda info: _net_benefit_style(info.value, float(nb_max)), + show=stratified_by == "probability_threshold", + ), + Column(id="true_positives", show=False), + Column(id="true_negatives", show=False), + Column(id="false_positives", show=False), + Column(id="false_negatives", show=False), + ] + ) metric_columns = ["sensitivity", "specificity", "ppv", "npv", "lift"] if stratified_by == "probability_threshold": diff --git a/src/rtichoke/processing/combinations.py b/src/rtichoke/processing/combinations.py index 24969e64..8262880e 100644 --- a/src/rtichoke/processing/combinations.py +++ b/src/rtichoke/processing/combinations.py @@ -19,9 +19,7 @@ def create_strata_combinations(stratified_by: str, by: float, breaks) -> pl.Data if stratified_by == "probability_threshold": cutoff_values = np.asarray(breaks) bin_edges = ( - cutoff_values - if cutoff_values[-1] == 1.0 - else np.append(cutoff_values, 1.0) + cutoff_values if cutoff_values[-1] == 1.0 else np.append(cutoff_values, 1.0) ) upper_bound = bin_edges[1:] lower_bound = bin_edges[:-1] diff --git a/src/rtichoke/processing/time_input_validation.py b/src/rtichoke/processing/time_input_validation.py index dd1cbe59..05d5906e 100644 --- a/src/rtichoke/processing/time_input_validation.py +++ b/src/rtichoke/processing/time_input_validation.py @@ -20,9 +20,7 @@ def _validate_time_outcome_values( values = reals.values() if isinstance(reals, dict) else [reals] for outcome_values in values: if not np.all(np.isin(np.asarray(outcome_values), [0, 1, 2])): - 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 _validate_time_input_alignment( @@ -32,7 +30,9 @@ def _validate_time_input_alignment( ) -> None: """Validate supported array/dict layouts before time-dependent processing.""" 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." + ) _validate_probability_values(probs) _validate_time_outcome_values(reals) @@ -83,14 +83,18 @@ def _validate_time_input_alignment( group = groups[0] if reals_is_dict: if group not in reals: - raise ValueError(f"`reals` is missing the key {group!r} required by `probs`.") + raise ValueError( + f"`reals` is missing the key {group!r} required by `probs`." + ) reals_values = reals[group] else: reals_values = reals if times_is_dict: if group not in times: - raise ValueError(f"`times` is missing the key {group!r} required by `probs`.") + raise ValueError( + f"`times` is missing the key {group!r} required by `probs`." + ) times_values = times[group] else: times_values = times diff --git a/src/rtichoke/processing/time_reference_lines.py b/src/rtichoke/processing/time_reference_lines.py index ccecb7c6..8774aa78 100644 --- a/src/rtichoke/processing/time_reference_lines.py +++ b/src/rtichoke/processing/time_reference_lines.py @@ -6,7 +6,9 @@ import polars as pl from plotly.graph_objs._figure import Figure -from rtichoke.performance_data.performance_data_times import prepare_performance_data_times +from rtichoke.performance_data.performance_data_times import ( + prepare_performance_data_times, +) from rtichoke.processing.plotly_helper_functions import ( _check_if_multiple_populations_are_being_validated_times, _create_plotly_curve_times, diff --git a/tests/test_calibration.py b/tests/test_calibration.py index 2afcb948..19b43947 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -45,9 +45,7 @@ def test_create_calibration_curve_multiple_populations_unequal_sizes(): } for calibration_type in ("discrete", "smooth"): - fig = create_calibration_curve( - probs, reals, calibration_type=calibration_type - ) + fig = create_calibration_curve(probs, reals, calibration_type=calibration_type) assert {trace.name for trace in fig.data if trace.name} >= {"Train", "Test"} @@ -56,6 +54,8 @@ def test_calibration_limits_keep_padding_without_leaving_probability_scale(): near_one = pl.DataFrame({"x": [0.30, 0.99], "y": [0.40, 0.98]}) mid_range = pl.DataFrame({"x": [0.20, 0.80], "y": [0.25, 0.75]}) - assert _define_limits_for_calibration_plot(near_zero) == pytest.approx([0.0, 0.7345]) + assert _define_limits_for_calibration_plot(near_zero) == pytest.approx( + [0.0, 0.7345] + ) assert _define_limits_for_calibration_plot(near_one) == pytest.approx([0.2655, 1.0]) assert _define_limits_for_calibration_plot(mid_range) == pytest.approx([0.17, 0.83]) diff --git a/tests/test_calibration_interactive_aspect.py b/tests/test_calibration_interactive_aspect.py index 38655141..19df6347 100644 --- a/tests/test_calibration_interactive_aspect.py +++ b/tests/test_calibration_interactive_aspect.py @@ -1,6 +1,9 @@ import numpy as np -from rtichoke.calibration import create_calibration_curve, create_calibration_curve_times +from rtichoke.calibration import ( + create_calibration_curve, + create_calibration_curve_times, +) def _assert_square_main_panel(fig): @@ -23,9 +26,7 @@ def test_interactive_calibration_main_panel_is_square(): reals = np.array([0, 1] * 10) for calibration_type in ("discrete", "smooth"): - fig = create_calibration_curve( - probs, reals, calibration_type=calibration_type - ) + fig = create_calibration_curve(probs, reals, calibration_type=calibration_type) _assert_square_main_panel(fig) _assert_tall_widget_geometry(fig) assert list(fig.layout.xaxis.range) == list(fig.layout.yaxis.range) diff --git a/tests/test_decision_curve_dcurves_parity.py b/tests/test_decision_curve_dcurves_parity.py index c31cd709..74dc14bb 100644 --- a/tests/test_decision_curve_dcurves_parity.py +++ b/tests/test_decision_curve_dcurves_parity.py @@ -82,15 +82,13 @@ def test_survival_decision_curve_matches_dcurves_issue_127() -> None: assert_allclose(rtichoke_prevalence, dcurves_prevalence, rtol=0, atol=1e-10) assert_allclose( - rtichoke_result["true_positives"].to_numpy() - / rtichoke_result["n"].to_numpy(), + rtichoke_result["true_positives"].to_numpy() / rtichoke_result["n"].to_numpy(), dcurves_model["tp_rate"].to_numpy(), rtol=0, atol=1e-10, ) assert_allclose( - rtichoke_result["false_positives"].to_numpy() - / rtichoke_result["n"].to_numpy(), + rtichoke_result["false_positives"].to_numpy() / rtichoke_result["n"].to_numpy(), dcurves_model["fp_rate"].to_numpy(), rtol=0, atol=1e-10, @@ -114,9 +112,9 @@ def test_survival_decision_curve_includes_prediction_equal_to_threshold() -> Non ) dcurves_result = _dcurves_survival_dca(data, [0.20]) - dcurves_model = dcurves_result[ - dcurves_result["model"] == "cancerpredmarker" - ].iloc[0] + dcurves_model = dcurves_result[dcurves_result["model"] == "cancerpredmarker"].iloc[ + 0 + ] rtichoke_result = _rtichoke_survival_dca(data, by=0.10).filter( pl.col("chosen_cutoff") == 0.20 diff --git a/tests/test_gains_times.py b/tests/test_gains_times.py index 8cb697c6..5a42311c 100644 --- a/tests/test_gains_times.py +++ b/tests/test_gains_times.py @@ -77,9 +77,9 @@ def test_gains_reference_is_population_and_horizon_specific(): "fixed_time_horizons": [5.0, 10.0], "reference_data": pl.DataFrame(), } - reference_data = _replace_gains_reference_data_times( - curve_list, performance_data - )["reference_data"] + reference_data = _replace_gains_reference_data_times(curve_list, performance_data)[ + "reference_data" + ] expected_y_at_point_one = { ("perfect_model_population_a", 5.0): 0.1 / 0.2, diff --git a/tests/test_performance_table.py b/tests/test_performance_table.py index dd9d4479..5bcfc4ff 100644 --- a/tests/test_performance_table.py +++ b/tests/test_performance_table.py @@ -34,7 +34,9 @@ def test_create_performance_table_defaults_to_great_tables(): def test_create_performance_table_supports_reactable(): probs, reals = _example() - assert isinstance(create_performance_table(probs, reals, by=0.1, renderer="reactable"), Reactable) + assert isinstance( + create_performance_table(probs, reals, by=0.1, renderer="reactable"), Reactable + ) def test_render_performance_table_accepts_prepared_polars_data(): @@ -53,33 +55,60 @@ def test_render_performance_table_rejects_empty_data(): def test_create_performance_table_supports_ppcr_stratification(): probs, reals = _example() - assert isinstance(create_performance_table(probs, reals, by=0.1, stratified_by=("ppcr",)), GT) + assert isinstance( + create_performance_table(probs, reals, by=0.1, stratified_by=("ppcr",)), GT + ) def test_create_performance_table_times_defaults_to_great_tables(): probs, reals, times = _time_example() - assert isinstance(create_performance_table_times(probs, reals, times, fixed_time_horizons=[5, 10], by=0.1), GT) + assert isinstance( + create_performance_table_times( + probs, reals, times, fixed_time_horizons=[5, 10], by=0.1 + ), + GT, + ) def test_create_performance_table_times_supports_reactable(): probs, reals, times = _time_example() - assert isinstance(create_performance_table_times(probs, reals, times, fixed_time_horizons=[5], by=0.1, renderer="reactable"), Reactable) + assert isinstance( + create_performance_table_times( + probs, reals, times, fixed_time_horizons=[5], by=0.1, renderer="reactable" + ), + Reactable, + ) def test_render_performance_table_preserves_multiple_time_horizons(): probs, reals, times = _time_example() - data = prepare_performance_data_times(probs, reals, times.astype(float), fixed_time_horizons=[5, 10], by=0.1) - assert sorted(data.get_column("fixed_time_horizon").unique().to_list()) == [5.0, 10.0] + data = prepare_performance_data_times( + probs, reals, times.astype(float), fixed_time_horizons=[5, 10], by=0.1 + ) + assert sorted(data.get_column("fixed_time_horizon").unique().to_list()) == [ + 5.0, + 10.0, + ] assert isinstance(render_performance_table(data), GT) def test_create_performance_table_times_supports_multiple_heuristic_sets(): probs, reals, times = _time_example() heuristics_sets = [ - {"censoring_heuristic": "adjusted", "competing_heuristic": "adjusted_as_negative"}, + { + "censoring_heuristic": "adjusted", + "competing_heuristic": "adjusted_as_negative", + }, {"censoring_heuristic": "excluded", "competing_heuristic": "excluded"}, ] - data = prepare_performance_data_times(probs, reals, times.astype(float), fixed_time_horizons=[5], heuristics_sets=heuristics_sets, by=0.1) + data = prepare_performance_data_times( + probs, + reals, + times.astype(float), + fixed_time_horizons=[5], + heuristics_sets=heuristics_sets, + by=0.1, + ) assert data.get_column("censoring_heuristic").n_unique() == 2 assert data.get_column("competing_heuristic").n_unique() == 2 assert isinstance(render_performance_table(data), GT) @@ -100,7 +129,10 @@ def test_reactable_metric_bar_matches_r_colors_and_geometry(): assert style["backgroundPosition"] == "center" -@pytest.mark.parametrize(("value", "color", "extent"), [(0.5, "lightgreen", "75.0%"), (-0.5, "pink", "25.0%")]) +@pytest.mark.parametrize( + ("value", "color", "extent"), + [(0.5, "lightgreen", "75.0%"), (-0.5, "pink", "25.0%")], +) def test_reactable_net_benefit_bar_matches_r_diverging_scale(value, color, extent): style = _net_benefit_style(value, 1.0) assert color in style["background"] diff --git a/tests/test_reference_line_audit.py b/tests/test_reference_line_audit.py index 012eec28..98ad6f21 100644 --- a/tests/test_reference_line_audit.py +++ b/tests/test_reference_line_audit.py @@ -23,13 +23,17 @@ def _binary_inputs(): def _visible_trace(fig, name): - matches = [trace for trace in fig.data if trace.name == name and trace.visible is not False] + matches = [ + trace for trace in fig.data if trace.name == name and trace.visible is not False + ] assert len(matches) == 1 return matches[0] def _y_at_x(trace, x): - matches = [float(y) for tx, y in zip(trace.x, trace.y) if float(tx) == pytest.approx(x)] + matches = [ + float(y) for tx, y in zip(trace.x, trace.y) if float(tx) == pytest.approx(x) + ] assert len(matches) == 1 return matches[0] @@ -116,7 +120,15 @@ def test_time_gains_reference_is_population_and_horizon_specific(): ) traces = {(t.name, t.visible): t for t in fig.data} - assert _y_at_x(traces[("perfect_model_population_a", True)], 0.01) == pytest.approx(0.06) - assert _y_at_x(traces[("perfect_model_population_b", True)], 0.01) == pytest.approx(0.04) - assert _y_at_x(traces[("perfect_model_population_a", False)], 0.01) == pytest.approx(0.03) - assert _y_at_x(traces[("perfect_model_population_b", False)], 0.01) == pytest.approx(0.02) + assert _y_at_x(traces[("perfect_model_population_a", True)], 0.01) == pytest.approx( + 0.06 + ) + assert _y_at_x(traces[("perfect_model_population_b", True)], 0.01) == pytest.approx( + 0.04 + ) + assert _y_at_x( + traces[("perfect_model_population_a", False)], 0.01 + ) == pytest.approx(0.03) + assert _y_at_x( + traces[("perfect_model_population_b", False)], 0.01 + ) == pytest.approx(0.02) diff --git a/tests/test_stratification_isolation.py b/tests/test_stratification_isolation.py index fe29d21a..72ecdaef 100644 --- a/tests/test_stratification_isolation.py +++ b/tests/test_stratification_isolation.py @@ -45,9 +45,7 @@ def test_binary_combined_stratification_preserves_each_component(): stratified_by=[stratification], by=0.25, ) - from_combined = combined.filter( - combined["stratified_by"] == stratification - ) + from_combined = combined.filter(combined["stratified_by"] == stratification) assert_frame_equal( _sort_binary(from_combined), _sort_binary(isolated), @@ -97,9 +95,7 @@ def test_time_combined_stratification_preserves_each_component_across_groups_and stratified_by=[stratification], by=0.25, ) - from_combined = combined.filter( - combined["stratified_by"] == stratification - ) + from_combined = combined.filter(combined["stratified_by"] == stratification) assert_frame_equal( _sort_times(from_combined), _sort_times(isolated), diff --git a/tests/test_time_input_validation.py b/tests/test_time_input_validation.py index 3b34bf57..fd84be26 100644 --- a/tests/test_time_input_validation.py +++ b/tests/test_time_input_validation.py @@ -43,7 +43,9 @@ def test_multiple_groups_reject_mixed_dict_and_array_outcomes(): def test_multiple_population_lengths_must_match_within_group(): bad_times = {**TIMES, "test": TIMES["test"][:-1]} - with pytest.raises(ValueError, match="Input lengths must match within group 'test'"): + with pytest.raises( + ValueError, match="Input lengths must match within group 'test'" + ): _call(times=bad_times) diff --git a/tests/test_time_reference_lines.py b/tests/test_time_reference_lines.py index ad5485f5..beea86e6 100644 --- a/tests/test_time_reference_lines.py +++ b/tests/test_time_reference_lines.py @@ -51,7 +51,9 @@ def _public_curve_inputs(): def _trace(fig, name: str, visible: bool): - matches = [trace for trace in fig.data if trace.name == name and trace.visible is visible] + matches = [ + trace for trace in fig.data if trace.name == name and trace.visible is visible + ] assert len(matches) == 1 return matches[0] From 2f4951da39a491e34cd66c253e2778a28e8a3141 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 21:38:58 +0300 Subject: [PATCH 4/5] Enforce Ruff format in package CI --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6d670a54..849ccfe6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -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 From 63942e005059a9709010d8ded32a5b656b189cc3 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 21:39:03 +0300 Subject: [PATCH 5/5] Remove Ruff baseline helper workflow --- .github/workflows/ruff-format-baseline.yml | 38 ---------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/ruff-format-baseline.yml diff --git a/.github/workflows/ruff-format-baseline.yml b/.github/workflows/ruff-format-baseline.yml deleted file mode 100644 index f5c9e965..00000000 --- a/.github/workflows/ruff-format-baseline.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Ruff format baseline - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} - - - uses: astral-sh/setup-uv@v6 - - - name: Install the project - run: uv sync --all-extras --dev - - - name: Apply Ruff format - run: uv run ruff format . - - - name: Commit formatter baseline - run: | - if git diff --quiet; then - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -- '*.py' - git commit -m "Apply Ruff format baseline" - git push