From 237037f443fedfb434b64b2e7bc480b57de7e9c2 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 13:55:10 +0300 Subject: [PATCH 1/8] Characterize time-dependent evaluation semantics --- tests/test_time_semantic_characterization.py | 282 +++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 tests/test_time_semantic_characterization.py diff --git a/tests/test_time_semantic_characterization.py b/tests/test_time_semantic_characterization.py new file mode 100644 index 00000000..d86d6436 --- /dev/null +++ b/tests/test_time_semantic_characterization.py @@ -0,0 +1,282 @@ +import numpy as np +import polars as pl +import pytest + +from rtichoke import ( + create_calibration_curve_times, + create_decision_curve_times, + create_gains_curve_times, + create_lift_curve_times, + create_precision_recall_curve_times, + create_roc_curve_times, + prepare_performance_data_times, +) +from rtichoke.processing.plotly_helper_functions import ( + _check_if_multiple_populations_are_being_validated_times, +) +from rtichoke.processing.time_reference_lines import _replace_reference_data_times + +HORIZONS = [5.0, 10.0] +HEURISTICS = [ + { + "censoring_heuristic": "adjusted", + "competing_heuristic": "adjusted_as_negative", + } +] +COLORS = ["#111111", "#222222"] + +PROBS_A = np.array([0.05, 0.15, 0.35, 0.55, 0.75, 0.95]) +PROBS_B = np.array([0.10, 0.25, 0.45, 0.65, 0.80, 0.90]) +REALS_SHARED = np.array([1, 0, 1, 0, 1, 0]) +TIMES_SHARED = np.array([3.0, 12.0, 8.0, 13.0, 14.0, 15.0]) + + +def _performance(probs, reals, times): + return prepare_performance_data_times( + probs=probs, + reals=reals, + times=times, + fixed_time_horizons=HORIZONS, + heuristics_sets=HEURISTICS, + by=0.25, + ) + + +def _series_keys(performance_data): + return set( + performance_data.select("reference_group", "fixed_time_horizon") + .unique() + .iter_rows() + ) + + +def _references(performance_data, curve): + curve_list = { + "fixed_time_horizons": HORIZONS, + "reference_data": pl.DataFrame(), + } + return _replace_reference_data_times( + curve_list, + performance_data, + curve=curve, + min_p_threshold=0.1, + max_p_threshold=0.9, + )["reference_data"] + + +def _reference_names_by_horizon(reference_data): + return { + horizon: set( + reference_data.filter(pl.col("fixed_time_horizon") == horizon)[ + "reference_group" + ].unique() + ) + for horizon in HORIZONS + } + + +def test_one_model_one_population_is_one_series_per_horizon(): + performance_data = _performance( + {"model": PROBS_A}, REALS_SHARED, TIMES_SHARED + ) + + assert _series_keys(performance_data) == { + ("model", 5.0), + ("model", 10.0), + } + + +@pytest.mark.parametrize( + "creator", + [ + create_roc_curve_times, + create_precision_recall_curve_times, + create_gains_curve_times, + create_lift_curve_times, + create_decision_curve_times, + ], +) +def test_multiple_models_keep_labels_and_colors_across_horizons(creator): + fig = creator( + probs={"Model A": PROBS_A, "Model B": PROBS_B}, + reals=REALS_SHARED, + times=TIMES_SHARED, + fixed_time_horizons=HORIZONS, + heuristics_sets=HEURISTICS, + by=0.25, + color_values=COLORS, + ) + + for model, color in zip(("Model A", "Model B"), COLORS): + traces = [trace for trace in fig.data if trace.name == model] + assert len(traces) == len(HORIZONS) + assert {trace.line.color for trace in traces} == {color} + + +def test_interventions_avoided_keeps_labels_and_colors_across_horizons(): + fig = create_decision_curve_times( + probs={"Model A": PROBS_A, "Model B": PROBS_B}, + reals=REALS_SHARED, + times=TIMES_SHARED, + fixed_time_horizons=HORIZONS, + heuristics_sets=HEURISTICS, + decision_type="interventions avoided", + by=0.25, + color_values=COLORS, + ) + + for model, color in zip(("Model A", "Model B"), COLORS): + traces = [trace for trace in fig.data if trace.name == model] + assert len(traces) == len(HORIZONS) + assert {trace.line.color for trace in traces} == {color} + + +def test_multiple_models_share_reference_context_at_each_horizon(): + performance_data = _performance( + {"Model A": PROBS_A, "Model B": PROBS_B}, + REALS_SHARED, + TIMES_SHARED, + ) + + aj = ( + performance_data.filter(pl.col("chosen_cutoff") == 0) + .select("reference_group", "fixed_time_horizon", "real_positives", "n") + .unique() + .with_columns((pl.col("real_positives") / pl.col("n")).alias("aj_estimate")) + ) + assert not _check_if_multiple_populations_are_being_validated_times(aj) + + expected = { + "roc": {"random_guess"}, + "precision recall": {"random_guess"}, + "gains": {"random_guess", "perfect_model"}, + "lift": {"random_guess", "perfect_model"}, + "decision": {"treat_none", "treat_all"}, + "interventions avoided": {"treat_all", "treat_none"}, + } + for curve, names in expected.items(): + assert _reference_names_by_horizon(_references(performance_data, curve)) == { + 5.0: names, + 10.0: names, + } + + +def test_different_prevalence_populations_are_distinct_series_and_references(): + probs = {"Population low": PROBS_A, "Population high": PROBS_B} + reals = { + "Population low": np.array([1, 0, 0, 0, 0, 0]), + "Population high": np.array([1, 1, 1, 0, 0, 0]), + } + times = { + "Population low": np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), + "Population high": np.array([2.0, 7.0, 9.0, 12.0, 13.0, 14.0]), + } + performance_data = _performance(probs, reals, times) + + assert _series_keys(performance_data) == { + ("Population low", 5.0), + ("Population high", 5.0), + ("Population low", 10.0), + ("Population high", 10.0), + } + + pr_names = _reference_names_by_horizon( + _references(performance_data, "precision recall") + ) + assert pr_names == { + 5.0: {"random_guess_Population low", "random_guess_Population high"}, + 10.0: {"random_guess_Population low", "random_guess_Population high"}, + } + + +def test_equal_prevalence_populations_remain_series_but_references_collapse(): + probs = {"Population A": PROBS_A, "Population B": PROBS_B} + reals = { + "Population A": np.array([1, 1, 0, 0, 0, 0]), + "Population B": np.array([1, 1, 0, 0, 0, 0]), + } + times = { + "Population A": np.array([3.0, 8.0, 12.0, 13.0, 14.0, 15.0]), + "Population B": np.array([4.0, 9.0, 12.0, 13.0, 14.0, 15.0]), + } + performance_data = _performance(probs, reals, times) + + assert _series_keys(performance_data) == { + ("Population A", 5.0), + ("Population B", 5.0), + ("Population A", 10.0), + ("Population B", 10.0), + } + + pr_names = _reference_names_by_horizon( + _references(performance_data, "precision recall") + ) + assert pr_names == {5.0: {"random_guess"}, 10.0: {"random_guess"}} + + +def test_paired_model_population_inputs_are_generic_series_per_horizon(): + pair_names = ["Model A @ Population A", "Model B @ Population B"] + probs = dict(zip(pair_names, (PROBS_A, PROBS_B))) + reals = { + pair_names[0]: np.array([1, 0, 0, 0, 0, 0]), + pair_names[1]: np.array([1, 1, 1, 0, 0, 0]), + } + times = { + pair_names[0]: np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), + pair_names[1]: np.array([2.0, 7.0, 9.0, 12.0, 13.0, 14.0]), + } + performance_data = _performance(probs, reals, times) + + assert _series_keys(performance_data) == { + (pair_names[0], 5.0), + (pair_names[1], 5.0), + (pair_names[0], 10.0), + (pair_names[1], 10.0), + } + assert "model" not in performance_data.columns + assert "population" not in performance_data.columns + + +def test_time_calibration_has_one_identity_line_per_horizon_and_group_series(): + fig = create_calibration_curve_times( + probs={"Population A": PROBS_A, "Population B": PROBS_B}, + reals={ + "Population A": np.array([1, 1, 0, 0, 0, 0]), + "Population B": np.array([1, 1, 0, 0, 0, 0]), + }, + times={ + "Population A": np.array([3.0, 8.0, 12.0, 13.0, 14.0, 15.0]), + "Population B": np.array([4.0, 9.0, 12.0, 13.0, 14.0, 15.0]), + }, + fixed_time_horizons=HORIZONS, + heuristics_sets=HEURISTICS, + calibration_type="discrete", + color_values=COLORS, + ) + + identity = [trace for trace in fig.data if trace.name == "Perfectly Calibrated"] + assert len(identity) == len(HORIZONS) + for group, color in zip(("Population A", "Population B"), COLORS): + traces = [trace for trace in fig.data if trace.name == group] + assert len(traces) == 2 * len(HORIZONS) + curve_traces = [trace for trace in traces if trace.type == "scatter"] + assert len(curve_traces) == len(HORIZONS) + assert {trace.marker.color for trace in curve_traces} == {color} + + +def test_time_performance_data_preserves_group_and_horizon_dimensions(): + performance_data = _performance( + {"Model A": PROBS_A, "Model B": PROBS_B}, + REALS_SHARED, + TIMES_SHARED, + ) + + assert {"reference_group", "fixed_time_horizon"}.issubset( + performance_data.columns + ) + assert _series_keys(performance_data) == { + ("Model A", 5.0), + ("Model B", 5.0), + ("Model A", 10.0), + ("Model B", 10.0), + } From 9a58d9416670805cdaa8f2f3327fc1fb1e2cab16 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 13:57:17 +0300 Subject: [PATCH 2/8] Format time semantic characterization tests --- tests/test_time_semantic_characterization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_time_semantic_characterization.py b/tests/test_time_semantic_characterization.py index d86d6436..2ad95e18 100644 --- a/tests/test_time_semantic_characterization.py +++ b/tests/test_time_semantic_characterization.py @@ -76,9 +76,7 @@ def _reference_names_by_horizon(reference_data): def test_one_model_one_population_is_one_series_per_horizon(): - performance_data = _performance( - {"model": PROBS_A}, REALS_SHARED, TIMES_SHARED - ) + performance_data = _performance({"model": PROBS_A}, REALS_SHARED, TIMES_SHARED) assert _series_keys(performance_data) == { ("model", 5.0), From 2fc334322310cff310621aedd859df54fe16d7aa Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 13:58:21 +0300 Subject: [PATCH 3/8] Temporarily show Ruff formatting --- .github/workflows/python-package.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1bc44d40..9d5e2c1a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -31,8 +31,10 @@ jobs: - name: Check lint run: uv run ruff check . - - name: Check format - run: uv run ruff format --check . + - name: Show Ruff formatting + run: | + uv run ruff format tests/test_time_semantic_characterization.py + cat tests/test_time_semantic_characterization.py - name: Check types run: uv run ty check src/rtichoke From 2e1ed14cf0dd7685aabcec9a84eeacf7b28e2b1a Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 14:00:38 +0300 Subject: [PATCH 4/8] Restore package workflow --- .github/workflows/python-package.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9d5e2c1a..1bc44d40 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -31,10 +31,8 @@ jobs: - name: Check lint run: uv run ruff check . - - name: Show Ruff formatting - run: | - uv run ruff format tests/test_time_semantic_characterization.py - cat tests/test_time_semantic_characterization.py + - name: Check format + run: uv run ruff format --check . - name: Check types run: uv run ty check src/rtichoke From 8d8ab21ff397524b925abd8b05fce9423cc3c7a8 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 14:01:23 +0300 Subject: [PATCH 5/8] Refine time semantic characterization --- tests/test_time_semantic_characterization.py | 170 ++++++++++++++----- 1 file changed, 127 insertions(+), 43 deletions(-) diff --git a/tests/test_time_semantic_characterization.py b/tests/test_time_semantic_characterization.py index 2ad95e18..2c4d3e4b 100644 --- a/tests/test_time_semantic_characterization.py +++ b/tests/test_time_semantic_characterization.py @@ -2,6 +2,7 @@ import polars as pl import pytest +import rtichoke.performance_table as performance_table_module from rtichoke import ( create_calibration_curve_times, create_decision_curve_times, @@ -75,6 +76,32 @@ def _reference_names_by_horizon(reference_data): } +def _shared_reference_names(): + return { + "roc": {"random_guess"}, + "precision recall": {"random_guess"}, + "gains": {"random_guess", "perfect_model"}, + "lift": {"random_guess", "perfect_model"}, + "decision": {"treat_none", "treat_all"}, + "interventions avoided": {"treat_all", "treat_none"}, + } + + +def _population_reference_names(populations): + return { + "roc": {"random_guess"}, + "precision recall": {f"random_guess_{population}" for population in populations}, + "gains": {"random_guess"} + | {f"perfect_model_{population}" for population in populations}, + "lift": {"random_guess"} + | {f"perfect_model_{population}" for population in populations}, + "decision": {"treat_none"} + | {f"treat_all_{population}" for population in populations}, + "interventions avoided": {"treat_all"} + | {f"treat_none_{population}" for population in populations}, + } + + def test_one_model_one_population_is_one_series_per_horizon(): performance_data = _performance({"model": PROBS_A}, REALS_SHARED, TIMES_SHARED) @@ -94,7 +121,9 @@ def test_one_model_one_population_is_one_series_per_horizon(): create_decision_curve_times, ], ) -def test_multiple_models_keep_labels_and_colors_across_horizons(creator): +def test_multiple_models_keep_series_labels_colors_and_legends_across_horizons( + creator, +): fig = creator( probs={"Model A": PROBS_A, "Model B": PROBS_B}, reals=REALS_SHARED, @@ -106,12 +135,22 @@ def test_multiple_models_keep_labels_and_colors_across_horizons(creator): ) for model, color in zip(("Model A", "Model B"), COLORS): - traces = [trace for trace in fig.data if trace.name == model] - assert len(traces) == len(HORIZONS) - assert {trace.line.color for trace in traces} == {color} - - -def test_interventions_avoided_keeps_labels_and_colors_across_horizons(): + series = [ + trace + for trace in fig.data + if trace.name == model and trace.mode == "markers+lines" + ] + assert len(series) == len(HORIZONS) + assert {trace.line.color for trace in series} == {color} + assert all(trace.showlegend is True for trace in series) + assert sum(trace.visible is True for trace in series) == 1 + + cutoff_markers = [trace for trace in fig.data if trace.name == f"{model} @ cutoff"] + assert len(cutoff_markers) == len(HORIZONS) + assert all(trace.showlegend is False for trace in cutoff_markers) + + +def test_interventions_avoided_keeps_series_semantics_across_horizons(): fig = create_decision_curve_times( probs={"Model A": PROBS_A, "Model B": PROBS_B}, reals=REALS_SHARED, @@ -124,9 +163,13 @@ def test_interventions_avoided_keeps_labels_and_colors_across_horizons(): ) for model, color in zip(("Model A", "Model B"), COLORS): - traces = [trace for trace in fig.data if trace.name == model] - assert len(traces) == len(HORIZONS) - assert {trace.line.color for trace in traces} == {color} + series = [ + trace + for trace in fig.data + if trace.name == model and trace.mode == "markers+lines" + ] + assert len(series) == len(HORIZONS) + assert {trace.line.color for trace in series} == {color} def test_multiple_models_share_reference_context_at_each_horizon(): @@ -144,47 +187,38 @@ def test_multiple_models_share_reference_context_at_each_horizon(): ) assert not _check_if_multiple_populations_are_being_validated_times(aj) - expected = { - "roc": {"random_guess"}, - "precision recall": {"random_guess"}, - "gains": {"random_guess", "perfect_model"}, - "lift": {"random_guess", "perfect_model"}, - "decision": {"treat_none", "treat_all"}, - "interventions avoided": {"treat_all", "treat_none"}, - } - for curve, names in expected.items(): + for curve, names in _shared_reference_names().items(): assert _reference_names_by_horizon(_references(performance_data, curve)) == { 5.0: names, 10.0: names, } -def test_different_prevalence_populations_are_distinct_series_and_references(): - probs = {"Population low": PROBS_A, "Population high": PROBS_B} +def test_different_prevalence_populations_own_references_at_each_horizon(): + populations = ("Population low", "Population high") + probs = {populations[0]: PROBS_A, populations[1]: PROBS_B} reals = { - "Population low": np.array([1, 0, 0, 0, 0, 0]), - "Population high": np.array([1, 1, 1, 0, 0, 0]), + populations[0]: np.array([1, 0, 0, 0, 0, 0]), + populations[1]: np.array([1, 1, 1, 0, 0, 0]), } times = { - "Population low": np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), - "Population high": np.array([2.0, 7.0, 9.0, 12.0, 13.0, 14.0]), + populations[0]: np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), + populations[1]: np.array([2.0, 4.0, 9.0, 12.0, 13.0, 14.0]), } performance_data = _performance(probs, reals, times) assert _series_keys(performance_data) == { - ("Population low", 5.0), - ("Population high", 5.0), - ("Population low", 10.0), - ("Population high", 10.0), + (populations[0], 5.0), + (populations[1], 5.0), + (populations[0], 10.0), + (populations[1], 10.0), } - pr_names = _reference_names_by_horizon( - _references(performance_data, "precision recall") - ) - assert pr_names == { - 5.0: {"random_guess_Population low", "random_guess_Population high"}, - 10.0: {"random_guess_Population low", "random_guess_Population high"}, - } + for curve, names in _population_reference_names(populations).items(): + assert _reference_names_by_horizon(_references(performance_data, curve)) == { + 5.0: names, + 10.0: names, + } def test_equal_prevalence_populations_remain_series_but_references_collapse(): @@ -206,10 +240,35 @@ def test_equal_prevalence_populations_remain_series_but_references_collapse(): ("Population B", 10.0), } - pr_names = _reference_names_by_horizon( + for curve, names in _shared_reference_names().items(): + assert _reference_names_by_horizon(_references(performance_data, curve)) == { + 5.0: names, + 10.0: names, + } + + +def test_reference_scope_can_switch_when_prevalence_diverges_by_horizon(): + populations = ("Population low", "Population high") + performance_data = _performance( + {populations[0]: PROBS_A, populations[1]: PROBS_B}, + { + populations[0]: np.array([1, 0, 0, 0, 0, 0]), + populations[1]: np.array([1, 1, 1, 0, 0, 0]), + }, + { + populations[0]: np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), + populations[1]: np.array([2.0, 7.0, 9.0, 12.0, 13.0, 14.0]), + }, + ) + + references = _reference_names_by_horizon( _references(performance_data, "precision recall") ) - assert pr_names == {5.0: {"random_guess"}, 10.0: {"random_guess"}} + assert references[5.0] == {"random_guess"} + assert references[10.0] == { + "random_guess_Population low", + "random_guess_Population high", + } def test_paired_model_population_inputs_are_generic_series_per_horizon(): @@ -221,7 +280,7 @@ def test_paired_model_population_inputs_are_generic_series_per_horizon(): } times = { pair_names[0]: np.array([3.0, 12.0, 13.0, 14.0, 15.0, 16.0]), - pair_names[1]: np.array([2.0, 7.0, 9.0, 12.0, 13.0, 14.0]), + pair_names[1]: np.array([2.0, 4.0, 9.0, 12.0, 13.0, 14.0]), } performance_data = _performance(probs, reals, times) @@ -254,11 +313,14 @@ def test_time_calibration_has_one_identity_line_per_horizon_and_group_series(): identity = [trace for trace in fig.data if trace.name == "Perfectly Calibrated"] assert len(identity) == len(HORIZONS) + assert sum(trace.visible is True for trace in identity) == 1 + for group, color in zip(("Population A", "Population B"), COLORS): traces = [trace for trace in fig.data if trace.name == group] - assert len(traces) == 2 * len(HORIZONS) curve_traces = [trace for trace in traces if trace.type == "scatter"] + histogram_traces = [trace for trace in traces if trace.type == "bar"] assert len(curve_traces) == len(HORIZONS) + assert len(histogram_traces) == len(HORIZONS) assert {trace.marker.color for trace in curve_traces} == {color} @@ -269,12 +331,34 @@ def test_time_performance_data_preserves_group_and_horizon_dimensions(): TIMES_SHARED, ) - assert {"reference_group", "fixed_time_horizon"}.issubset( - performance_data.columns - ) + assert {"reference_group", "fixed_time_horizon"}.issubset(performance_data.columns) assert _series_keys(performance_data) == { ("Model A", 5.0), ("Model B", 5.0), ("Model A", 10.0), ("Model B", 10.0), } + + +def test_time_performance_table_preserves_group_and_horizon_dimensions(monkeypatch): + monkeypatch.setattr( + performance_table_module, + "render_performance_table", + lambda performance_data, **kwargs: performance_data, + ) + + table_data = performance_table_module.create_performance_table_times( + probs={"Model A": PROBS_A, "Model B": PROBS_B}, + reals=REALS_SHARED, + times=TIMES_SHARED, + fixed_time_horizons=HORIZONS, + heuristics_sets=HEURISTICS, + by=0.25, + ) + + assert _series_keys(table_data) == { + ("Model A", 5.0), + ("Model B", 5.0), + ("Model A", 10.0), + ("Model B", 10.0), + } From ecc95873a0b2f6bb26c0027be12084e3d5090a74 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 14:02:29 +0300 Subject: [PATCH 6/8] Temporarily show Ruff output --- .github/workflows/python-package.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1bc44d40..e1c0dc23 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12", "3.13", "3.14"] + python-version: ["3.12"] permissions: contents: read @@ -31,8 +31,10 @@ jobs: - name: Check lint run: uv run ruff check . - - name: Check format - run: uv run ruff format --check . + - name: Show Ruff formatting + run: | + uv run ruff format tests/test_time_semantic_characterization.py + cat tests/test_time_semantic_characterization.py - name: Check types run: uv run ty check src/rtichoke From 59cf58f2710c9c9cc750344fdfe94416ac30007a Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 14:04:07 +0300 Subject: [PATCH 7/8] Apply Ruff formatting --- tests/test_time_semantic_characterization.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_time_semantic_characterization.py b/tests/test_time_semantic_characterization.py index 2c4d3e4b..49d96369 100644 --- a/tests/test_time_semantic_characterization.py +++ b/tests/test_time_semantic_characterization.py @@ -90,7 +90,9 @@ def _shared_reference_names(): def _population_reference_names(populations): return { "roc": {"random_guess"}, - "precision recall": {f"random_guess_{population}" for population in populations}, + "precision recall": { + f"random_guess_{population}" for population in populations + }, "gains": {"random_guess"} | {f"perfect_model_{population}" for population in populations}, "lift": {"random_guess"} @@ -145,7 +147,9 @@ def test_multiple_models_keep_series_labels_colors_and_legends_across_horizons( assert all(trace.showlegend is True for trace in series) assert sum(trace.visible is True for trace in series) == 1 - cutoff_markers = [trace for trace in fig.data if trace.name == f"{model} @ cutoff"] + cutoff_markers = [ + trace for trace in fig.data if trace.name == f"{model} @ cutoff" + ] assert len(cutoff_markers) == len(HORIZONS) assert all(trace.showlegend is False for trace in cutoff_markers) From 604fd673c1949e99741fd9c9be7c4661386bed59 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Fri, 21 Aug 2026 14:04:21 +0300 Subject: [PATCH 8/8] Restore package workflow --- .github/workflows/python-package.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e1c0dc23..1bc44d40 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.12"] + python-version: ["3.12", "3.13", "3.14"] permissions: contents: read @@ -31,10 +31,8 @@ jobs: - name: Check lint run: uv run ruff check . - - name: Show Ruff formatting - run: | - uv run ruff format tests/test_time_semantic_characterization.py - cat tests/test_time_semantic_characterization.py + - name: Check format + run: uv run ruff format --check . - name: Check types run: uv run ty check src/rtichoke