diff --git a/changelog.d/compare-annotations-empty.rst b/changelog.d/compare-annotations-empty.rst new file mode 100644 index 00000000..cc3de7d8 --- /dev/null +++ b/changelog.d/compare-annotations-empty.rst @@ -0,0 +1,2 @@ +**Allow empty annotations in compare_annotations** + ``compare_annotations`` (``Comparitor.compare``) no longer fails when the reference or test annotations are empty, for example when a detector finds no beats. The sensitivity or positive predictivity that cannot be computed is NaN. diff --git a/src/wfdb/processing/evaluate.py b/src/wfdb/processing/evaluate.py index 6c8e41ac..40ebc67f 100644 --- a/src/wfdb/processing/evaluate.py +++ b/src/wfdb/processing/evaluate.py @@ -99,7 +99,7 @@ def _calc_stats(self): ] # Test annotation indices that were unmatched to a reference annotation self.unmatched_test_inds = np.setdiff1d( - np.array(range(self.n_test)), + np.arange(self.n_test), self.matched_test_inds, assume_unique=True, ) @@ -118,8 +118,13 @@ def _calc_stats(self): self.fn = self.n_ref - self.tp # No tn attribute - self.sensitivity = float(self.tp) / float(self.tp + self.fn) - self.positive_predictivity = float(self.tp) / self.n_test + denom_sens = self.tp + self.fn + self.sensitivity = ( + float(self.tp) / denom_sens if denom_sens > 0 else float("nan") + ) + self.positive_predictivity = ( + float(self.tp) / self.n_test if self.n_test > 0 else float("nan") + ) def compare(self): """Main comparison function. diff --git a/tests/test_processing.py b/tests/test_processing.py index 353b71de..21e5f876 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -1,3 +1,4 @@ +import math from unittest import mock import numpy as np @@ -334,6 +335,28 @@ def test_random_annotations(self): assert np.all(np.abs(ref[matches != -1] - test[matched]) < window_width) assert 0 <= comparitor.fp <= len(test) + def test_compare_annotations_empty_ref(self): + """When reference annotations are empty, sensitivity should be NaN.""" + comparitor = processing.compare_annotations( + np.array([]), np.array([10, 20, 30]), 5 + ) + assert math.isnan(comparitor.sensitivity) + assert comparitor.positive_predictivity == 0.0 + + def test_compare_annotations_empty_test(self): + """When test annotations are empty, PPV should be NaN.""" + comparitor = processing.compare_annotations( + np.array([10, 20, 30]), np.array([]), 5 + ) + assert comparitor.sensitivity == 0.0 + assert math.isnan(comparitor.positive_predictivity) + + def test_compare_annotations_both_empty(self): + """When both annotation arrays are empty, both metrics should be NaN.""" + comparitor = processing.compare_annotations(np.array([]), np.array([]), 5) + assert math.isnan(comparitor.sensitivity) + assert math.isnan(comparitor.positive_predictivity) + class TestQrs: """Testing QRS detectors"""