From 49905a297934e6ca8ef0ec93c7ea527a41d5e8fe Mon Sep 17 00:00:00 2001 From: SexyERIC0723 Date: Wed, 8 Apr 2026 17:23:05 +0100 Subject: [PATCH 1/3] fix: guard against ZeroDivisionError in Comparitor._calc_stats When there are no reference annotations (tp + fn == 0) or no test annotations (n_test == 0), computing sensitivity and positive predictivity raises ZeroDivisionError. Return NaN instead, matching the convention used by sklearn.metrics for undefined ratios. Fixes #278 --- wfdb/processing/evaluate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wfdb/processing/evaluate.py b/wfdb/processing/evaluate.py index 3e960286..78a8c695 100644 --- a/wfdb/processing/evaluate.py +++ b/wfdb/processing/evaluate.py @@ -123,8 +123,11 @@ 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): """ From 1a7c9305ef4ed68f0d1e9e8734e7a6272275e72d Mon Sep 17 00:00:00 2001 From: SexyERIC0723 Date: Wed, 8 Apr 2026 17:33:53 +0100 Subject: [PATCH 2/3] test: add edge-case tests for empty annotation comparisons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover three boundary scenarios that previously raised ZeroDivisionError: - Empty reference, non-empty test → sensitivity=NaN, PPV=0.0 - Non-empty reference, empty test → sensitivity=0.0, PPV=NaN - Both empty → both NaN --- tests/test_processing.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_processing.py b/tests/test_processing.py index 0f371660..cb451e4d 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -199,3 +199,33 @@ def test_xqrs(self): assert comparitor.sensitivity > 0.99 assert comparitor.positive_predictivity > 0.99 + + def test_compare_annotations_empty_ref(self): + """When reference annotations are empty, sensitivity should be NaN.""" + import math + + 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.""" + import math + + 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.""" + import math + + comparitor = processing.compare_annotations( + np.array([]), np.array([]), 5 + ) + assert math.isnan(comparitor.sensitivity) + assert math.isnan(comparitor.positive_predictivity) From 387e12cdf50f8308b7655b6e3eac8ce9f6a5a257 Mon Sep 17 00:00:00 2001 From: SexyERIC0723 Date: Wed, 8 Apr 2026 17:45:41 +0100 Subject: [PATCH 3/3] fix: move edge-case tests to module level for pytest discovery The existing test_qrs class uses lowercase naming which pytest's default collection does not pick up. Move the new empty-annotation tests to module level so they are actually discovered and run in CI. --- tests/test_processing.py | 56 +++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index cb451e4d..35d87961 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -200,32 +200,40 @@ def test_xqrs(self): assert comparitor.sensitivity > 0.99 assert comparitor.positive_predictivity > 0.99 - def test_compare_annotations_empty_ref(self): - """When reference annotations are empty, sensitivity should be NaN.""" - import math + pass - 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.""" - import math +# Module-level tests for empty-annotation edge cases (pytest collects these +# directly, unlike the lowercase class above which is skipped by default). - 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_empty_ref(): + """When reference annotations are empty, sensitivity should be NaN.""" + import math - def test_compare_annotations_both_empty(self): - """When both annotation arrays are empty, both metrics should be NaN.""" - import math + comparitor = processing.compare_annotations( + np.array([]), np.array([10, 20, 30]), 5 + ) + assert math.isnan(comparitor.sensitivity) + assert comparitor.positive_predictivity == 0.0 - comparitor = processing.compare_annotations( - np.array([]), np.array([]), 5 - ) - assert math.isnan(comparitor.sensitivity) - assert math.isnan(comparitor.positive_predictivity) + +def test_compare_annotations_empty_test(): + """When test annotations are empty, PPV should be NaN.""" + import math + + 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(): + """When both annotation arrays are empty, both metrics should be NaN.""" + import math + + comparitor = processing.compare_annotations( + np.array([]), np.array([]), 5 + ) + assert math.isnan(comparitor.sensitivity) + assert math.isnan(comparitor.positive_predictivity)