From 1bde1223232823cb3b0f2d6fd1f7e382ed6b8a87 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 --- src/wfdb/processing/evaluate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wfdb/processing/evaluate.py b/src/wfdb/processing/evaluate.py index 6c8e41ac..cc3d75b8 100644 --- a/src/wfdb/processing/evaluate.py +++ b/src/wfdb/processing/evaluate.py @@ -118,8 +118,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): """Main comparison function. From 9a234026dec589e1e3835a82c3abebe591ecc111 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 | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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""" From dec7750d6227b09c46f07062e163638e7d10166f Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Mon, 2 Nov 2026 09:45:25 -0500 Subject: [PATCH 3/3] Fix empty test annotations in compare_annotations np.array(range(0)) is a float array, which can't be used as an index, so an empty set of test annotations still failed with an IndexError before reaching the zero division check. Use np.arange instead, and add a changelog entry. --- changelog.d/compare-annotations-empty.rst | 2 ++ src/wfdb/processing/evaluate.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/compare-annotations-empty.rst 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 cc3d75b8..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, ) @@ -119,7 +119,9 @@ def _calc_stats(self): # No tn attribute denom_sens = self.tp + self.fn - self.sensitivity = float(self.tp) / denom_sens if denom_sens > 0 else float("nan") + 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") )