Skip to content

Commit 577a18b

Browse files
authored
Use the last RR interval in compute_hr (#601)
`compute_hr` loops over `range(len(qrs_inds) - 2)` and then copies the rate at the last beat forward (`heart_rate[qrs_inds[-1]:] = heart_rate[qrs_inds[-1]]`). So the interval between the last two beats is never used: - `compute_hr(14, [0, 2, 6, 12], fs=60)` gives 900 bpm after sample 12 (from the 2-sample interval before it) instead of 600 bpm (from the last interval, 6 samples). - `compute_hr(10, [2, 6], fs=1)` returns only NaN, although the `len(qrs_inds) < 2` check implies two beats are enough. The loop now runs over every RR interval, and the last one fills up to the end of the signal. Everything before the last beat is unchanged. For record 100 with the reference annotations, the only differences are the 8 samples after the last beat. Two tests are added in `TestComputeHr`, and both fail on `main`. `ruff check` is clean and `pytest -m "not network"` passes. Related to #185, but that report was mostly caused by non-beat annotations (see the comments there), so this doesn't close it.
2 parents c449e76 + b9e0542 commit 577a18b

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎docs/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Unreleased
2929
**Fix parameter handling in csv_to_wfdb**
3030
``csv_to_wfdb`` now builds the same ``Record`` whether it writes files or returns the record (``record_only=True``), so all header parameters (such as ``dat_file_name``, ``counter_freq``, ``base_counter``, ``skew``, ``byte_offset``, ``adc_res`` and ``adc_zero``) are written instead of being ignored. Single values of any numeric type (including NumPy scalars and floats) are used for all channels, and an explicit ``0`` is no longer replaced by the default. The initial value and checksum are computed from the digital samples, and the default ADC resolution now follows ``fmt`` in both modes. The ``samps_per_frame``, ``init_value`` and ``checksum`` arguments are deprecated and ignored. The parameter descriptions in the docstring have been corrected.
3131

32+
**Use the last RR interval in compute_hr**
33+
``compute_hr`` now uses the interval between the last two beats for the heart rate after the last beat, instead of repeating the previous rate. With exactly two beats it now returns their rate after the second beat instead of only NaN.
34+
3235
**Use Ruff for formatting and linting**
3336
The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default).
3437

‎src/wfdb/processing/hr.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
def compute_hr(sig_len, qrs_inds, fs):
1010
"""Compute instantaneous heart rate from peak indices.
1111
12+
The rate from each RR interval is assigned to the samples after its second
13+
beat, up to and including the next beat. After the last beat, the rate
14+
from the last RR interval is used up to the end of the signal.
15+
1216
Parameters
1317
----------
1418
sig_len : int
@@ -31,15 +35,15 @@ def compute_hr(sig_len, qrs_inds, fs):
3135
if len(qrs_inds) < 2:
3236
return heart_rate
3337

34-
for i in range(0, len(qrs_inds) - 2):
38+
# The rate from each RR interval holds from just after its second beat up
39+
# to the next beat, or to the end of the signal after the last one.
40+
for i in range(0, len(qrs_inds) - 1):
3541
a = qrs_inds[i]
3642
b = qrs_inds[i + 1]
37-
c = qrs_inds[i + 2]
43+
end = qrs_inds[i + 2] + 1 if i + 2 < len(qrs_inds) else sig_len
3844
rr = (b - a) * (1.0 / fs) * 1000
3945
hr = 60000.0 / rr
40-
heart_rate[b + 1 : c + 1] = hr
41-
42-
heart_rate[qrs_inds[-1] :] = heart_rate[qrs_inds[-1]]
46+
heart_rate[b + 1 : end] = hr
4347

4448
return heart_rate
4549

‎tests/test_processing.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,37 @@ def test_correct_peaks(self):
174174
assert np.array_equal(yz, expected_peaks)
175175

176176

177+
class TestComputeHr:
178+
"""Tests for compute_hr."""
179+
180+
def test_uses_last_rr_interval(self):
181+
"""The rate after the last beat comes from the last RR interval."""
182+
hr = processing.compute_hr(sig_len=14, qrs_inds=[0, 2, 6, 12], fs=60)
183+
184+
expected = np.full(14, np.nan)
185+
expected[3:7] = 60 * 60 / 2
186+
expected[7:13] = 60 * 60 / 4
187+
expected[13:] = 60 * 60 / 6
188+
np.testing.assert_allclose(hr, expected)
189+
190+
def test_two_beats(self):
191+
"""Two beats give one RR interval, so the rate is known after them."""
192+
hr = processing.compute_hr(sig_len=10, qrs_inds=[2, 6], fs=1)
193+
194+
expected = np.full(10, np.nan)
195+
expected[7:] = 60 / 4
196+
np.testing.assert_allclose(hr, expected)
197+
198+
def test_last_beat_past_end(self):
199+
"""A last beat beyond the signal doesn't raise an IndexError."""
200+
hr = processing.compute_hr(sig_len=8, qrs_inds=[0, 2, 6, 12], fs=60)
201+
202+
expected = np.full(8, np.nan)
203+
expected[3:7] = 60 * 60 / 2
204+
expected[7:] = 60 * 60 / 4
205+
np.testing.assert_allclose(hr, expected)
206+
207+
177208
class TestQrs:
178209
"""Testing QRS detectors"""
179210

0 commit comments

Comments
 (0)