diff --git a/src/rtichoke/calibration/__init__.py b/src/rtichoke/calibration/__init__.py index b04d51eb..6e89c060 100644 --- a/src/rtichoke/calibration/__init__.py +++ b/src/rtichoke/calibration/__init__.py @@ -8,6 +8,13 @@ _original_create_calibration_curve = _calibration.create_calibration_curve _original_create_calibration_curve_times = _calibration.create_calibration_curve_times +_DEFAULT_TIME_CALIBRATION_HEURISTICS = [ + { + "censoring_heuristic": "adjusted", + "competing_heuristic": "adjusted_as_negative", + } +] + def create_calibration_curve(*args, **kwargs): """Create an interactive calibration plot with a square main panel.""" @@ -18,6 +25,18 @@ def create_calibration_curve(*args, **kwargs): def create_calibration_curve_times(*args, **kwargs): """Create an interactive time-dependent calibration plot with a square main panel.""" + heuristics_sets = kwargs.get("heuristics_sets") + if heuristics_sets is None: + heuristics_sets = [dict(_DEFAULT_TIME_CALIBRATION_HEURISTICS[0])] + kwargs["heuristics_sets"] = heuristics_sets + + if len(heuristics_sets) != 1: + raise ValueError( + "create_calibration_curve_times() currently supports exactly one " + "heuristic set. Multiple heuristic sets would be combined in the " + "same calibration trace because the plot has no heuristic selector." + ) + return enforce_square_calibration_panel( _original_create_calibration_curve_times(*args, **kwargs) ) diff --git a/tests/test_calibration_times_heuristics_contract.py b/tests/test_calibration_times_heuristics_contract.py new file mode 100644 index 00000000..b81d9c9a --- /dev/null +++ b/tests/test_calibration_times_heuristics_contract.py @@ -0,0 +1,74 @@ +import numpy as np +import pytest + +from rtichoke import ( + create_calibration_curve_times as create_calibration_curve_times_top_level, +) +from rtichoke.calibration import create_calibration_curve_times +from rtichoke.calibration.calibration import ( + create_calibration_curve_times as create_calibration_curve_times_direct, +) + + +ENTRY_POINTS = [ + create_calibration_curve_times_top_level, + create_calibration_curve_times, + create_calibration_curve_times_direct, +] + + +def _inputs(): + probs = {"model_1": np.array([0.1, 0.2, 0.3, 0.4])} + reals = np.array([0, 1, 1, 1]) + times = np.array([1.0, 2.0, 3.0, 4.0]) + return probs, reals, times + + +@pytest.mark.parametrize("entry_point", ENTRY_POINTS) +def test_time_calibration_defaults_to_adjusted_heuristics(entry_point): + probs, reals, times = _inputs() + + default_fig = entry_point( + probs, + reals, + times, + fixed_time_horizons=[2.0], + ) + explicit_fig = entry_point( + probs, + reals, + times, + fixed_time_horizons=[2.0], + heuristics_sets=[ + { + "censoring_heuristic": "adjusted", + "competing_heuristic": "adjusted_as_negative", + } + ], + ) + + np.testing.assert_allclose(default_fig.data[1].x, explicit_fig.data[1].x) + np.testing.assert_allclose(default_fig.data[1].y, explicit_fig.data[1].y) + + +@pytest.mark.parametrize("entry_point", ENTRY_POINTS) +def test_time_calibration_rejects_multiple_heuristic_sets(entry_point): + probs, reals, times = _inputs() + + with pytest.raises(ValueError, match="exactly one heuristic set"): + entry_point( + probs, + reals, + times, + fixed_time_horizons=[2.0], + heuristics_sets=[ + { + "censoring_heuristic": "adjusted", + "competing_heuristic": "adjusted_as_negative", + }, + { + "censoring_heuristic": "excluded", + "competing_heuristic": "excluded", + }, + ], + ) diff --git a/user_guide/02-curve-api-compatibility.qmd b/user_guide/02-curve-api-compatibility.qmd index e603b3fd..1a0b3905 100644 --- a/user_guide/02-curve-api-compatibility.qmd +++ b/user_guide/02-curve-api-compatibility.qmd @@ -48,12 +48,7 @@ These statements describe the effect of the heuristics on the estimates. Functio ## Time-dependent calibration heuristics -`create_calibration_curve_times()` differs from its ROC, precision-recall, Gains, Lift, and decision-curve siblings in two important ways: - -1. `heuristics_sets` is currently required rather than defaulted. -2. Calibration explicitly rejects unsupported heuristic combinations (specifically `competing_heuristic="adjusted_as_censored"`) with an `Unsupported calibration heuristics` error instead of silently skipping requested horizons. - -Pass the calibration heuristic explicitly. For adjusted or exclusion-based paths: +`create_calibration_curve_times()` defaults to one adjusted heuristic set: ```python heuristics_sets = [ @@ -64,12 +59,14 @@ heuristics_sets = [ ] ``` +You can still provide a different single heuristic set explicitly. Calibration currently accepts exactly one heuristic set per call because the plot has no heuristic selector; passing several sets raises a clear `ValueError` instead of combining them into one calibration trace. Calibration also rejects unsupported combinations such as `competing_heuristic="adjusted_as_censored"`. + When `calibration_type="smooth"`, you can also specify the `smooth_method`: - `"local_aj"` (default): Gerds' local Aalen-Johansen/KM neighborhood estimation. - `"secondary_cox"`: Secondary Cox regression method (Austin, Harrell & McLernon 2020). - `"pseudo_values"`: Leave-one-out Aalen-Johansen pseudo-observations lowess. -Then call: +The default call is therefore simply: ```python fig = rk.create_calibration_curve_times( @@ -77,7 +74,6 @@ fig = rk.create_calibration_curve_times( reals=reals, times=times, fixed_time_horizons=[3.0, 6.0, 9.0], - heuristics_sets=heuristics_sets, ) ```