From 03733d6ebe7d577185a6d6b030e38a752a547cdb Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:39:29 +0300 Subject: [PATCH 1/8] fix: keep time reference prevalence horizon-specific --- .../processing/time_reference_lines.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/rtichoke/processing/time_reference_lines.py diff --git a/src/rtichoke/processing/time_reference_lines.py b/src/rtichoke/processing/time_reference_lines.py new file mode 100644 index 00000000..716b0097 --- /dev/null +++ b/src/rtichoke/processing/time_reference_lines.py @@ -0,0 +1,110 @@ +"""Helpers for horizon-specific time-dependent reference curves.""" + +from typing import Dict, Sequence, Union + +import numpy as np +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.processing.plotly_helper_functions import ( + _check_if_multiple_populations_are_being_validated_times, + _create_plotly_curve_times, + _create_reference_lines_data, + _create_rtichoke_curve_list_times, +) + + +def _get_reference_aj_estimates_times(performance_data: pl.DataFrame) -> pl.DataFrame: + """Return one event-risk estimate per reference group and horizon. + + At probability threshold 0 everyone is classified positive, so + ``real_positives / n`` is the horizon-specific event probability. Using + only cutoff 0 avoids mixing that estimate with cutoff-specific values from + the opposite boundary. + """ + return ( + 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")) + .select("reference_group", "fixed_time_horizon", "aj_estimate") + .sort(["reference_group", "fixed_time_horizon"]) + ) + + +def _replace_reference_data_times( + curve_list: dict, + performance_data: pl.DataFrame, + curve: str, + min_p_threshold: float = 0.0, + max_p_threshold: float = 1.0, +) -> dict: + """Replace prevalence-dependent references with cutoff-0 estimates.""" + aj_estimates = _get_reference_aj_estimates_times(performance_data) + references = [] + + for horizon in curve_list["fixed_time_horizons"]: + aj_horizon = aj_estimates.filter(pl.col("fixed_time_horizon") == horizon) + multiple_populations = _check_if_multiple_populations_are_being_validated_times( + aj_horizon + ) + references.append( + _create_reference_lines_data( + curve=curve, + aj_estimates_from_performance_data=aj_horizon, + multiple_populations=multiple_populations, + min_p_threshold=min_p_threshold, + max_p_threshold=max_p_threshold, + ).with_columns(pl.lit(horizon).alias("fixed_time_horizon")) + ) + + curve_list["reference_data"] = ( + pl.concat(references, how="vertical") if references else pl.DataFrame() + ) + return curve_list + + +def _create_rtichoke_plotly_curve_times_reference_safe( + probs: Dict[str, np.ndarray], + reals: Union[np.ndarray, Dict[str, np.ndarray]], + times: Union[np.ndarray, Dict[str, np.ndarray]], + fixed_time_horizons: list[float], + heuristics_sets: list[Dict], + min_p_threshold: float = 0, + max_p_threshold: float = 1, + by: float = 0.01, + stratified_by: Sequence[str] = ("probability_threshold",), + size: int = 600, + color_values=None, + curve: str = "precision recall", +) -> Figure: + """Create a time-dependent curve with horizon-specific reference data.""" + performance_data = prepare_performance_data_times( + probs, + reals, + times, + by=by, + fixed_time_horizons=fixed_time_horizons, + heuristics_sets=heuristics_sets, + stratified_by=stratified_by, + ) + + # Preserve the existing plotting behavior here; this helper only corrects + # construction of prevalence-dependent reference lines. + curve_list = _create_rtichoke_curve_list_times( + performance_data, + stratified_by=stratified_by[0], + curve=curve, + min_p_threshold=min_p_threshold, + max_p_threshold=max_p_threshold, + ) + curve_list = _replace_reference_data_times( + curve_list, + performance_data, + curve=curve, + min_p_threshold=min_p_threshold, + max_p_threshold=max_p_threshold, + ) + + return _create_plotly_curve_times(curve_list) From 368a8af48939bc09f17b94b9a62e0a9587a3db39 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:39:50 +0300 Subject: [PATCH 2/8] fix: use horizon-specific PR references --- src/rtichoke/discrimination/precision_recall.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rtichoke/discrimination/precision_recall.py b/src/rtichoke/discrimination/precision_recall.py index 7770cbdc..83ccf695 100644 --- a/src/rtichoke/discrimination/precision_recall.py +++ b/src/rtichoke/discrimination/precision_recall.py @@ -5,10 +5,12 @@ from typing import Dict, List, Sequence, Union from plotly.graph_objs._figure import Figure from rtichoke.processing.plotly_helper_functions import ( - _create_rtichoke_plotly_curve_times, _create_rtichoke_plotly_curve_binary, _plot_rtichoke_curve_binary, ) +from rtichoke.processing.time_reference_lines import ( + _create_rtichoke_plotly_curve_times_reference_safe, +) import numpy as np import polars as pl @@ -187,7 +189,7 @@ def create_precision_recall_curve_times( A Plotly ``Figure`` object for the time-dependent Precision-Recall curve. """ - fig = _create_rtichoke_plotly_curve_times( + fig = _create_rtichoke_plotly_curve_times_reference_safe( probs, reals, times, From d8264778e079a1427c53fa9f424cd0a57561f306 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:40:13 +0300 Subject: [PATCH 3/8] fix: use horizon-specific lift references --- src/rtichoke/discrimination/lift.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rtichoke/discrimination/lift.py b/src/rtichoke/discrimination/lift.py index a8460b12..4efc3256 100644 --- a/src/rtichoke/discrimination/lift.py +++ b/src/rtichoke/discrimination/lift.py @@ -5,10 +5,12 @@ from typing import Dict, List, Sequence, Union from plotly.graph_objs._figure import Figure from rtichoke.processing.plotly_helper_functions import ( - _create_rtichoke_plotly_curve_times, _create_rtichoke_plotly_curve_binary, _plot_rtichoke_curve_binary, ) +from rtichoke.processing.time_reference_lines import ( + _create_rtichoke_plotly_curve_times_reference_safe, +) import numpy as np import polars as pl @@ -186,7 +188,7 @@ def create_lift_curve_times( A Plotly ``Figure`` object for the time-dependent Lift curve. """ - fig = _create_rtichoke_plotly_curve_times( + fig = _create_rtichoke_plotly_curve_times_reference_safe( probs, reals, times, From a2ee5d8e83ff6fa19fc6f44e8d7b9792d6bba049 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:40:37 +0300 Subject: [PATCH 4/8] fix: use horizon-specific decision references --- src/rtichoke/utility/decision.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rtichoke/utility/decision.py b/src/rtichoke/utility/decision.py index 12a0592a..ab218b74 100644 --- a/src/rtichoke/utility/decision.py +++ b/src/rtichoke/utility/decision.py @@ -6,9 +6,11 @@ from plotly.graph_objs._figure import Figure from rtichoke.processing.plotly_helper_functions import ( _create_rtichoke_plotly_curve_binary, - _create_rtichoke_plotly_curve_times, _plot_rtichoke_curve_binary, ) +from rtichoke.processing.time_reference_lines import ( + _create_rtichoke_plotly_curve_times_reference_safe, +) import numpy as np import polars as pl @@ -234,7 +236,7 @@ def create_decision_curve_times( else: curve = "interventions avoided" - fig = _create_rtichoke_plotly_curve_times( + fig = _create_rtichoke_plotly_curve_times_reference_safe( probs, reals, times, From 18cf0aedc0814745804af9090958c98437d07f30 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:40:53 +0300 Subject: [PATCH 5/8] test: lock time reference prevalence by horizon --- tests/test_time_reference_lines.py | 105 +++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_time_reference_lines.py diff --git a/tests/test_time_reference_lines.py b/tests/test_time_reference_lines.py new file mode 100644 index 00000000..3e1c9765 --- /dev/null +++ b/tests/test_time_reference_lines.py @@ -0,0 +1,105 @@ +import polars as pl +import pytest + +from rtichoke.processing.time_reference_lines import ( + _get_reference_aj_estimates_times, + _replace_reference_data_times, +) + + +def _performance_data_with_boundary_drift() -> pl.DataFrame: + return pl.DataFrame( + { + "reference_group": [ + "population_a", + "population_a", + "population_b", + "population_b", + "population_a", + "population_a", + "population_b", + "population_b", + ], + "fixed_time_horizon": [5.0, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0, 10.0], + "chosen_cutoff": [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], + "real_positives": [20.0, 5.0, 40.0, 10.0, 30.0, 7.0, 60.0, 15.0], + "n": [100.0] * 8, + } + ) + + +def test_reference_prevalence_uses_cutoff_zero_per_population_and_horizon(): + aj = _get_reference_aj_estimates_times(_performance_data_with_boundary_drift()) + + assert aj.height == 4 + expected = { + ("population_a", 5.0): 0.2, + ("population_b", 5.0): 0.4, + ("population_a", 10.0): 0.3, + ("population_b", 10.0): 0.6, + } + for (population, horizon), value in expected.items(): + actual = aj.filter( + (pl.col("reference_group") == population) + & (pl.col("fixed_time_horizon") == horizon) + )["aj_estimate"].item() + assert actual == pytest.approx(value) + + +@pytest.mark.parametrize( + ("curve", "reference_group", "x", "expected"), + [ + ("precision recall", "random_guess_population_a", 0.1, 0.2), + ("precision recall", "random_guess_population_b", 0.1, 0.4), + ("lift", "perfect_model_population_a", 0.1, 5.0), + ("lift", "perfect_model_population_b", 0.1, 2.5), + ("decision", "treat_all_population_a", 0.1, 0.11111111111111112), + ("decision", "treat_all_population_b", 0.1, 0.33333333333333337), + ], +) +def test_prevalence_dependent_references_are_population_specific_at_horizon_five( + curve, reference_group, x, expected +): + curve_list = { + "fixed_time_horizons": [5.0, 10.0], + "reference_data": pl.DataFrame(), + } + reference_data = _replace_reference_data_times( + curve_list, + _performance_data_with_boundary_drift(), + curve=curve, + )["reference_data"] + + y = reference_data.filter( + (pl.col("reference_group") == reference_group) + & (pl.col("fixed_time_horizon") == 5.0) + & (pl.col("x") == x) + )["y"].item() + + assert y == pytest.approx(expected) + + +def test_precision_recall_reference_changes_with_horizon(): + curve_list = { + "fixed_time_horizons": [5.0, 10.0], + "reference_data": pl.DataFrame(), + } + reference_data = _replace_reference_data_times( + curve_list, + _performance_data_with_boundary_drift(), + curve="precision recall", + )["reference_data"] + + p5 = reference_data.filter( + (pl.col("reference_group") == "random_guess_population_a") + & (pl.col("fixed_time_horizon") == 5.0) + & (pl.col("x") == 0.1) + )["y"].item() + p10 = reference_data.filter( + (pl.col("reference_group") == "random_guess_population_a") + & (pl.col("fixed_time_horizon") == 10.0) + & (pl.col("x") == 0.1) + )["y"].item() + + assert p5 == pytest.approx(0.2) + assert p10 == pytest.approx(0.3) From a812629e8e4d2cb72b8e39e88a82707b622d78f2 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 10:50:15 +0300 Subject: [PATCH 6/8] refactor: simplify time reference helper --- .../processing/time_reference_lines.py | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/rtichoke/processing/time_reference_lines.py b/src/rtichoke/processing/time_reference_lines.py index 716b0097..3b56e496 100644 --- a/src/rtichoke/processing/time_reference_lines.py +++ b/src/rtichoke/processing/time_reference_lines.py @@ -16,13 +16,7 @@ def _get_reference_aj_estimates_times(performance_data: pl.DataFrame) -> pl.DataFrame: - """Return one event-risk estimate per reference group and horizon. - - At probability threshold 0 everyone is classified positive, so - ``real_positives / n`` is the horizon-specific event probability. Using - only cutoff 0 avoids mixing that estimate with cutoff-specific values from - the opposite boundary. - """ + """Return the cutoff-0 event risk for each group and horizon.""" return ( performance_data.filter(pl.col("chosen_cutoff") == 0) .select("reference_group", "fixed_time_horizon", "real_positives", "n") @@ -40,28 +34,25 @@ def _replace_reference_data_times( min_p_threshold: float = 0.0, max_p_threshold: float = 1.0, ) -> dict: - """Replace prevalence-dependent references with cutoff-0 estimates.""" + """Rebuild prevalence-dependent references from cutoff-0 event risk.""" aj_estimates = _get_reference_aj_estimates_times(performance_data) references = [] for horizon in curve_list["fixed_time_horizons"]: aj_horizon = aj_estimates.filter(pl.col("fixed_time_horizon") == horizon) - multiple_populations = _check_if_multiple_populations_are_being_validated_times( - aj_horizon - ) references.append( _create_reference_lines_data( curve=curve, aj_estimates_from_performance_data=aj_horizon, - multiple_populations=multiple_populations, + multiple_populations=( + _check_if_multiple_populations_are_being_validated_times(aj_horizon) + ), min_p_threshold=min_p_threshold, max_p_threshold=max_p_threshold, ).with_columns(pl.lit(horizon).alias("fixed_time_horizon")) ) - curve_list["reference_data"] = ( - pl.concat(references, how="vertical") if references else pl.DataFrame() - ) + curve_list["reference_data"] = pl.concat(references, how="vertical") return curve_list @@ -79,7 +70,7 @@ def _create_rtichoke_plotly_curve_times_reference_safe( color_values=None, curve: str = "precision recall", ) -> Figure: - """Create a time-dependent curve with horizon-specific reference data.""" + """Create a time-dependent curve with corrected reference prevalence.""" performance_data = prepare_performance_data_times( probs, reals, @@ -89,9 +80,6 @@ def _create_rtichoke_plotly_curve_times_reference_safe( heuristics_sets=heuristics_sets, stratified_by=stratified_by, ) - - # Preserve the existing plotting behavior here; this helper only corrects - # construction of prevalence-dependent reference lines. curve_list = _create_rtichoke_curve_list_times( performance_data, stratified_by=stratified_by[0], @@ -99,12 +87,12 @@ def _create_rtichoke_plotly_curve_times_reference_safe( min_p_threshold=min_p_threshold, max_p_threshold=max_p_threshold, ) - curve_list = _replace_reference_data_times( - curve_list, - performance_data, - curve=curve, - min_p_threshold=min_p_threshold, - max_p_threshold=max_p_threshold, + return _create_plotly_curve_times( + _replace_reference_data_times( + curve_list, + performance_data, + curve=curve, + min_p_threshold=min_p_threshold, + max_p_threshold=max_p_threshold, + ) ) - - return _create_plotly_curve_times(curve_list) From c7d7d7b57c9bcae15a54191e76f48848edf80bf2 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 11:17:51 +0300 Subject: [PATCH 7/8] test: cover public time-dependent reference curves --- tests/test_time_reference_lines.py | 95 ++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/test_time_reference_lines.py b/tests/test_time_reference_lines.py index 3e1c9765..b13c3d4f 100644 --- a/tests/test_time_reference_lines.py +++ b/tests/test_time_reference_lines.py @@ -1,6 +1,12 @@ +import numpy as np import polars as pl import pytest +from rtichoke import ( + create_decision_curve_times, + create_lift_curve_times, + create_precision_recall_curve_times, +) from rtichoke.processing.time_reference_lines import ( _get_reference_aj_estimates_times, _replace_reference_data_times, @@ -28,6 +34,28 @@ def _performance_data_with_boundary_drift() -> pl.DataFrame: ) +def _public_curve_inputs(): + probs = { + "population_a": np.array([0.05, 0.15, 0.35, 0.55, 0.75, 0.95]), + "population_b": np.array([0.10, 0.30, 0.50, 0.70]), + } + reals = { + "population_a": np.array([0, 1, 0, 1, 0, 1]), + "population_b": np.array([0, 1, 1, 1]), + } + times = { + "population_a": np.array([2.0, 3.0, 6.0, 7.0, 11.0, 12.0]), + "population_b": np.array([1.0, 4.0, 8.0, 9.0]), + } + return probs, reals, times + + +def _trace(fig, name: str, visible: bool): + matches = [trace for trace in fig.data if trace.name == name and trace.visible is visible] + assert len(matches) == 1 + return matches[0] + + def test_reference_prevalence_uses_cutoff_zero_per_population_and_horizon(): aj = _get_reference_aj_estimates_times(_performance_data_with_boundary_drift()) @@ -103,3 +131,70 @@ def test_precision_recall_reference_changes_with_horizon(): assert p5 == pytest.approx(0.2) assert p10 == pytest.approx(0.3) + + +def test_public_precision_recall_references_are_population_and_horizon_specific(): + probs, reals, times = _public_curve_inputs() + fig = create_precision_recall_curve_times( + probs, reals, times, fixed_time_horizons=[5.0, 10.0], by=0.1 + ) + + a5 = _trace(fig, "random_guess_population_a", True) + b5 = _trace(fig, "random_guess_population_b", True) + a10 = _trace(fig, "random_guess_population_a", False) + b10 = _trace(fig, "random_guess_population_b", False) + + assert float(a5.y[0]) == pytest.approx(1 / 3) + assert float(b5.y[0]) == pytest.approx(1 / 2) + assert float(a10.y[0]) == pytest.approx(1 / 2) + assert float(b10.y[0]) == pytest.approx(3 / 4) + + +def test_public_lift_references_are_population_and_horizon_specific(): + probs, reals, times = _public_curve_inputs() + fig = create_lift_curve_times( + probs, reals, times, fixed_time_horizons=[5.0, 10.0], by=0.1 + ) + + a5 = _trace(fig, "perfect_model_population_a", True) + b5 = _trace(fig, "perfect_model_population_b", True) + a10 = _trace(fig, "perfect_model_population_a", False) + b10 = _trace(fig, "perfect_model_population_b", False) + + assert float(a5.y[0]) == pytest.approx(3.0) + assert float(b5.y[0]) == pytest.approx(2.0) + assert float(a10.y[0]) == pytest.approx(2.0) + assert float(b10.y[0]) == pytest.approx(4 / 3) + + +def test_public_decision_references_are_population_and_horizon_specific(): + probs, reals, times = _public_curve_inputs() + fig = create_decision_curve_times( + probs, + reals, + times, + fixed_time_horizons=[5.0, 10.0], + by=0.1, + min_p_threshold=0.1, + max_p_threshold=0.9, + ) + + a5 = _trace(fig, "treat_all_population_a", True) + b5 = _trace(fig, "treat_all_population_b", True) + a10 = _trace(fig, "treat_all_population_a", False) + b10 = _trace(fig, "treat_all_population_b", False) + + # Reference x-grid starts at 0.1 after threshold filtering. + assert float(a5.x[0]) == pytest.approx(0.1) + assert float(b5.x[0]) == pytest.approx(0.1) + assert float(a10.x[0]) == pytest.approx(0.1) + assert float(b10.x[0]) == pytest.approx(0.1) + + def treat_all(p): + x = 0.1 + return p - (1 - p) * x / (1 - x) + + assert float(a5.y[0]) == pytest.approx(treat_all(1 / 3)) + assert float(b5.y[0]) == pytest.approx(treat_all(1 / 2)) + assert float(a10.y[0]) == pytest.approx(treat_all(1 / 2)) + assert float(b10.y[0]) == pytest.approx(treat_all(3 / 4)) From b1df21e3378bb2758c1a30a5f38be75a9cc995d1 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 20 Aug 2026 11:18:27 +0300 Subject: [PATCH 8/8] test: isolate reference routing from censoring adjustment --- tests/test_time_reference_lines.py | 33 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/test_time_reference_lines.py b/tests/test_time_reference_lines.py index b13c3d4f..ad5485f5 100644 --- a/tests/test_time_reference_lines.py +++ b/tests/test_time_reference_lines.py @@ -40,12 +40,12 @@ def _public_curve_inputs(): "population_b": np.array([0.10, 0.30, 0.50, 0.70]), } reals = { - "population_a": np.array([0, 1, 0, 1, 0, 1]), - "population_b": np.array([0, 1, 1, 1]), + "population_a": np.array([1, 0, 1, 0, 1, 0]), + "population_b": np.array([1, 1, 0, 1]), } times = { - "population_a": np.array([2.0, 3.0, 6.0, 7.0, 11.0, 12.0]), - "population_b": np.array([1.0, 4.0, 8.0, 9.0]), + "population_a": np.array([3.0, 11.0, 7.0, 12.0, 13.0, 14.0]), + "population_b": np.array([4.0, 8.0, 11.0, 12.0]), } return probs, reals, times @@ -144,10 +144,10 @@ def test_public_precision_recall_references_are_population_and_horizon_specific( a10 = _trace(fig, "random_guess_population_a", False) b10 = _trace(fig, "random_guess_population_b", False) - assert float(a5.y[0]) == pytest.approx(1 / 3) - assert float(b5.y[0]) == pytest.approx(1 / 2) - assert float(a10.y[0]) == pytest.approx(1 / 2) - assert float(b10.y[0]) == pytest.approx(3 / 4) + assert float(a5.y[0]) == pytest.approx(1 / 6) + assert float(b5.y[0]) == pytest.approx(1 / 4) + assert float(a10.y[0]) == pytest.approx(2 / 6) + assert float(b10.y[0]) == pytest.approx(2 / 4) def test_public_lift_references_are_population_and_horizon_specific(): @@ -161,10 +161,10 @@ def test_public_lift_references_are_population_and_horizon_specific(): a10 = _trace(fig, "perfect_model_population_a", False) b10 = _trace(fig, "perfect_model_population_b", False) - assert float(a5.y[0]) == pytest.approx(3.0) - assert float(b5.y[0]) == pytest.approx(2.0) - assert float(a10.y[0]) == pytest.approx(2.0) - assert float(b10.y[0]) == pytest.approx(4 / 3) + assert float(a5.y[0]) == pytest.approx(6.0) + assert float(b5.y[0]) == pytest.approx(4.0) + assert float(a10.y[0]) == pytest.approx(3.0) + assert float(b10.y[0]) == pytest.approx(2.0) def test_public_decision_references_are_population_and_horizon_specific(): @@ -184,7 +184,6 @@ def test_public_decision_references_are_population_and_horizon_specific(): a10 = _trace(fig, "treat_all_population_a", False) b10 = _trace(fig, "treat_all_population_b", False) - # Reference x-grid starts at 0.1 after threshold filtering. assert float(a5.x[0]) == pytest.approx(0.1) assert float(b5.x[0]) == pytest.approx(0.1) assert float(a10.x[0]) == pytest.approx(0.1) @@ -194,7 +193,7 @@ def treat_all(p): x = 0.1 return p - (1 - p) * x / (1 - x) - assert float(a5.y[0]) == pytest.approx(treat_all(1 / 3)) - assert float(b5.y[0]) == pytest.approx(treat_all(1 / 2)) - assert float(a10.y[0]) == pytest.approx(treat_all(1 / 2)) - assert float(b10.y[0]) == pytest.approx(treat_all(3 / 4)) + assert float(a5.y[0]) == pytest.approx(treat_all(1 / 6)) + assert float(b5.y[0]) == pytest.approx(treat_all(1 / 4)) + assert float(a10.y[0]) == pytest.approx(treat_all(2 / 6)) + assert float(b10.y[0]) == pytest.approx(treat_all(2 / 4))