Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/compare-annotations-empty.rst
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 8 additions & 3 deletions src/wfdb/processing/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from unittest import mock

import numpy as np
Expand Down Expand Up @@ -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"""
Expand Down
Loading