diff --git a/src/ruptures/metrics/hausdorff.py b/src/ruptures/metrics/hausdorff.py index 8e09efad..78df4bda 100644 --- a/src/ruptures/metrics/hausdorff.py +++ b/src/ruptures/metrics/hausdorff.py @@ -13,11 +13,19 @@ def hausdorff(bkps1, bkps2): bkps2 (list): list of the last index of each regime. Returns: - float: Hausdorff distance. + float: Hausdorff distance. Returns ``np.inf`` when one partition + has no intermediate changepoints (i.e. it predicts a single segment) + and the other does. """ sanity_check(bkps1, bkps2) bkps1_arr = np.array(bkps1[:-1]).reshape(-1, 1) bkps2_arr = np.array(bkps2[:-1]).reshape(-1, 1) + if bkps1_arr.size == 0 or bkps2_arr.size == 0: + # If one partition has no intermediate changepoints and the other does, + # the sets are not comparable; return infinity by convention. + if bkps1_arr.size == 0 and bkps2_arr.size == 0: + return 0.0 + return np.inf pw_dist = cdist(bkps1_arr, bkps2_arr) res = max(pw_dist.min(axis=0).max(), pw_dist.min(axis=1).max()) return res diff --git a/src/ruptures/metrics/timeerror.py b/src/ruptures/metrics/timeerror.py index 11732af5..536d46d1 100644 --- a/src/ruptures/metrics/timeerror.py +++ b/src/ruptures/metrics/timeerror.py @@ -17,11 +17,18 @@ def meantime(true_bkps, my_bkps): partition) Returns: - float: mean time error. + float: mean time error. Returns ``np.inf`` when ``my_bkps`` contains + no intermediate changepoints (i.e. it predicts a single segment) but + ``true_bkps`` does. """ sanity_check(true_bkps, my_bkps) true_bkps_arr = np.array(true_bkps[:-1]).reshape(-1, 1) my_bkps_arr = np.array(my_bkps[:-1]).reshape(-1, 1) + if my_bkps_arr.size == 0 or true_bkps_arr.size == 0: + # At least one partition has no intermediate changepoints. + if my_bkps_arr.size == 0 and true_bkps_arr.size == 0: + return 0.0 + return np.inf pw_dist = cdist(true_bkps_arr, my_bkps_arr) dist_from_true = pw_dist.min(axis=0) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index b9cca471..214387b1 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -50,6 +50,30 @@ def test_precision_recall(b_mb, margin): p, r = precision_recall(b, [b[-1]], margin=margin) +def test_hausdorff_empty_intermediate_bkps(): + """Hausdorff must not crash when a partition has no intermediate + breakpoints.""" + # Both no intermediate breakpoints -> distance 0 + assert hausdorff([500], [500]) == 0.0 + # One side empty, the other not -> infinity + import math + + assert math.isinf(hausdorff([500], [200, 500])) + assert math.isinf(hausdorff([200, 500], [500])) + + +def test_meantime_empty_intermediate_bkps(): + """Meantime must not crash when a partition has no intermediate + breakpoints.""" + # Both no intermediate breakpoints -> distance 0 + assert meantime([500], [500]) == 0.0 + # One side empty -> infinity + import math + + assert math.isinf(meantime([500], [200, 500])) + assert math.isinf(meantime([200, 500], [500])) + + @pytest.mark.parametrize( "metric", [hamming, hausdorff, meantime, precision_recall, randindex] )