Skip to content

Commit

Permalink
Fix Comparitor.compare scenario which always assigns test annotation …
Browse files Browse the repository at this point in the history
…to earlier ref sample. fixes #105. fix docstrings.
  • Loading branch information
cx1111 committed Apr 5, 2018
1 parent e4c1234 commit 5e6d787
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 84 deletions.
2 changes: 1 addition & 1 deletion docs/processing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ Annotation Evaluators
---------------------

.. automodule:: wfdb.processing
:members: Comparitor, compare_annotations
:members: Comparitor, compare_annotations, benchmark_mitdb
90 changes: 52 additions & 38 deletions wfdb/io/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,25 +290,32 @@ def adc(self, expanded=False, inplace=False):
the d_signal/e_d_signal attribute will be set, and the
p_signal/e_p_signal field will be set to None.
Input arguments:
- expanded (default=False): Boolean specifying whether to transform the
e_p_signal attribute (True) or the p_signal attribute (False).
- inplace (default=False): Boolean specifying whether to automatically
set the object's corresponding digital signal attribute and set the
physical signal attribute to None (True), or to return the converted
signal as a separate variable without changing the original physical
signal attribute (False).
Possible output argument:
- d_signal: The digital conversion of the signal. Either a 2d numpy
array or a list of 1d numpy arrays.
Example Usage:
import wfdb
record = wfdb.rdsamp('sample-data/100')
d_signal = record.adc()
record.adc(inplace=True)
record.dac(inplace=True)
Parameters
----------
expanded : bool, optional
Whether to transform the `e_p_signal` attribute (True) or
the `p_signal` attribute (False).
inplace : bool, optional
Whether to automatically set the object's corresponding
digital signal attribute and set the physical
signal attribute to None (True), or to return the converted
signal as a separate variable without changing the original
physical signal attribute (False).
Returns
-------
d_signal : numpy array, optional
The digital conversion of the signal. Either a 2d numpy
array or a list of 1d numpy arrays.
Examples:
---------
>>> import wfdb
>>> record = wfdb.rdsamp('sample-data/100')
>>> d_signal = record.adc()
>>> record.adc(inplace=True)
>>> record.dac(inplace=True)
"""

# The digital nan values for each channel
Expand Down Expand Up @@ -379,25 +386,32 @@ def dac(self, expanded=False, return_res=64, inplace=False):
the p_signal/e_p_signal attribute will be set, and the
d_signal/e_d_signal field will be set to None.
Input arguments:
- expanded: Boolean specifying whether to transform the
e_d_signal attribute (True) or the d_signal attribute (False).
- inplace: Boolean specifying whether to automatically
set the object's corresponding physical signal attribute and set the
digital signal attribute to None (True), or to return the converted
signal as a separate variable without changing the original digital
signal attribute (False).
Possible output argument:
- p_signal: The physical conversion of the signal. Either a 2d numpy
array or a list of 1d numpy arrays.
Example Usage:
import wfdb
record = wfdb.rdsamp('sample-data/100', physical=False)
p_signal = record.dac()
record.dac(inplace=True)
record.adc(inplace=True)
Parameters
----------
expanded : bool, optional
Whether to transform the `e_d_signal attribute` (True) or
the `d_signal` attribute (False).
inplace : bool, optional
Whether to automatically set the object's corresponding
physical signal attribute and set the digital signal
attribute to None (True), or to return the converted
signal as a separate variable without changing the original
digital signal attribute (False).
Returns
-------
p_signal : numpy array, optional
The physical conversion of the signal. Either a 2d numpy
array or a list of 1d numpy arrays.
Examples
--------
>>> import wfdb
>>> record = wfdb.rdsamp('sample-data/100', physical=False)
>>> p_signal = record.dac()
>>> record.dac(inplace=True)
>>> record.adc(inplace=True)
"""

# The digital nan values for each channel
Expand Down
3 changes: 3 additions & 0 deletions wfdb/io/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,7 @@ def wrann(record_name, extension, sample, symbol=None, subtype=None, chan=None,
Write a WFDB annotation file.
Specify at least the following:
- The record name of the WFDB record (record_name)
- The annotation file extension (extension)
- The annotation locations in samples relative to the beginning of
Expand Down Expand Up @@ -1090,12 +1091,14 @@ def wrann(record_name, extension, sample, symbol=None, subtype=None, chan=None,
The map of custom defined annotation labels used for this annotation, in
addition to the standard WFDB annotation labels. Custom labels are
defined by two or three fields:
- The integer values used to store custom annotation labels in the file
(optional)
- Their short display symbols
- Their long descriptions.
This input argument may come in four formats:
1. A pandas.DataFrame object with columns:
['label_store', 'symbol', 'description']
2. A pandas.DataFrame object with columns: ['symbol', 'description']
Expand Down
2 changes: 2 additions & 0 deletions wfdb/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ def plot_items(signal=None, ann_samp=None, ann_sym=None, fs=None,
ann_samp: list, optional
A list of annotation locations to plot, with each list item
corresponding to a different channel. List items may be:
- 1d numpy array, with values representing sample indices. Empty
arrays are skipped.
- list, with values representing sample indices. Empty lists
are skipped.
- None. For channels in which nothing is to be plotted.
If `signal` is defined, the annotation locations will be overlaid on
the signals, with the list index corresponding to the signal channel.
The length of `annotation` does not have to match the number of
Expand Down
81 changes: 58 additions & 23 deletions wfdb/processing/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Comparitor(object):
The class to implement and hold comparisons between two sets of
annotations.
See methods `print_summary` and `plot`.
See methods `compare`, `print_summary` and `plot`.
Examples
--------
Expand Down Expand Up @@ -60,7 +60,7 @@ def __init__(self, ref_sample, test_sample, window_width, signal=None):

# The matching test sample number for each reference annotation.
# -1 for indices with no match
self.matching_sample_nums = -1 * np.ones(self.n_ref, dtype='int')
self.matching_sample_nums = np.full(self.n_ref, -1, dtype='int')

self.signal = signal
# TODO: rdann return annotations.where
Expand Down Expand Up @@ -114,17 +114,35 @@ def _calc_stats(self):
self.positive_predictivity = float(self.tp) / self.n_test
self.false_positive_rate = float(self.fp) / self.n_test


def compare(self):
"""
Main comparison function
"""
"""
Note: Make sure to be able to handle these ref/test scenarios:
A:
o----o---o---o
x-------x----x
B:
o----o-----o---o
x--------x--x--x
C:
o------o-----o---o
x-x--------x--x--x
D:
o------o-----o---o
x-x--------x-----x
"""
test_samp_num = 0
ref_samp_num = 0

# Iterate through the reference sample numbers
while ref_samp_num < self.n_ref and test_samp_num < self.n_test:

# Get the closest testing sample number for this reference sample
closest_samp_num, smallest_samp_diff = (
self._get_closest_samp_num(ref_samp_num, test_samp_num))
Expand All @@ -138,25 +156,42 @@ def compare(self):
# to compete for the test sample
closest_samp_num_next = -1

# Found a contested test sample number. Decide which reference
# sample it belongs to.
if closest_samp_num == closest_samp_num_next:
# If the sample is closer to the next reference sample,
# assign it to the next refernece sample.
if smallest_samp_diff_next < smallest_samp_diff:
# Get the next closest sample for this reference sample.
# Can this be empty? Need to catch case where nothing left?
closest_samp_num, smallest_samp_diff = (
self._get_closest_samp_num(ref_samp_num, test_samp_num))

# If no clash, it is straightforward.

# Assign the reference-test pair if close enough
if smallest_samp_diff < self.window_width:
self.matching_sample_nums[ref_samp_num] = closest_samp_num
# Found a contested test sample number. Decide which
# reference sample it belongs to. If the sample is closer to
# the next reference sample, leave it to the next reference
# sample and label this reference sample as unmatched.
if (closest_samp_num == closest_samp_num_next
and smallest_samp_diff_next < smallest_samp_diff):
# Get the next closest sample for this reference sample,
# if not already assigned to a previous sample.
# It will be the previous testing sample number in any
# possible case (scenario D below), or nothing.
if closest_samp_num and (not ref_samp_num or closest_samp_num - 1 != self.matching_sample_nums[ref_samp_num - 1]):
# The previous test annotation is inspected
closest_samp_num = closest_samp_num - 1
smallest_samp_diff = abs(self.ref_sample[ref_samp_num]
- self.test_sample[closest_samp_num])
# Assign the reference-test pair if close enough
if smallest_samp_diff < self.window_width:
self.matching_sample_nums[ref_samp_num] = closest_samp_num
# Set the starting test sample number to inspect
# for the next reference sample.
test_samp_num = closest_samp_num + 1

# Otherwise there is no matching test annotation

# If there is no clash, or the contested test sample is
# closer to the current reference, keep the test sample
# for this reference sample.
else:
# Assign the reference-test pair if close enough
if smallest_samp_diff < self.window_width:
self.matching_sample_nums[ref_samp_num] = closest_samp_num
# Increment the starting test sample number to inspect
# for the next reference sample.
test_samp_num = closest_samp_num + 1

ref_samp_num += 1
test_samp_num = closest_samp_num + 1

self._calc_stats()

Expand Down Expand Up @@ -185,7 +220,7 @@ def _get_closest_samp_num(self, ref_samp_num, start_test_samp_num):
abs_samp_diff = abs(samp_diff)

# Found a better match
if abs(samp_diff) < smallest_samp_diff:
if abs_samp_diff < smallest_samp_diff:
closest_samp_num = test_samp_num
smallest_samp_diff = abs_samp_diff

Expand Down Expand Up @@ -378,7 +413,7 @@ def benchmark_mitdb(detector, verbose=False):
>>> import wfdb
>> from wfdb.processing import benchmark_mitdb, xqrs_detect
>>> comparitors, a, b, c = benchmark_mitdb(xqrs_detect)
>>> comparitors, spec, pp, fpr = benchmark_mitdb(xqrs_detect)
"""
record_list = get_record_list('mitdb')
Expand Down
2 changes: 2 additions & 0 deletions wfdb/processing/peaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def find_peaks(sig):
"""
Find hard peaks and soft peaks in a signal, defined as follows:
- Hard peak: a peak that is either /\ or \/
- Soft peak: a peak that is either /-*\ or \-*/
In this case we define the middle as the peak
Expand Down Expand Up @@ -123,6 +124,7 @@ def correct_peaks(sig, peak_inds, search_radius, smooth_window_size,
peak_dir : str, optional
The expected peak direction: 'up' or 'down', 'both', or
'compare'.
- If 'up', the peaks will be shifted to local maxima
- If 'down', the peaks will be shifted to local minima
- If 'both', the peaks will be shifted to local maxima of the
Expand Down
47 changes: 26 additions & 21 deletions wfdb/processing/qrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class XQRS(object):
The `XQRS.detect` method runs the detection algorithm.
The process works as follows:
- Load the signal and configuration parameters.
- Bandpass filter the signal between 5 and 20 Hz, to get the
filtered signal.
Expand All @@ -30,18 +31,19 @@ class XQRS(object):
or fails, use default parameters.
- Run the main detection. Iterate through the local maxima of
the mwi signal. For each local maxima:
- Check if it is a qrs complex. To be classified as a qrs,
it must come after the refractory period, cross the qrs
detection threshold, and not be classified as a t-wave
if it comes close enough to the previous qrs. If
successfully classified, update running detection
threshold and heart rate parameters.
- If not a qrs, classify it as a noise peak and update
running parameters.
- Before continuing to the next local maxima, if no qrs
was detected within 1.66 times the recent rr interval,
perform backsearch qrs detection. This checks previous
peaks using a lower qrs detection threshold.
- Check if it is a qrs complex. To be classified as a qrs,
it must come after the refractory period, cross the qrs
detection threshold, and not be classified as a t-wave
if it comes close enough to the previous qrs. If
successfully classified, update running detection
threshold and heart rate parameters.
- If not a qrs, classify it as a noise peak and update
running parameters.
- Before continuing to the next local maxima, if no qrs
was detected within 1.66 times the recent rr interval,
perform backsearch qrs detection. This checks previous
peaks using a lower qrs detection threshold.
Examples
--------
Expand Down Expand Up @@ -1171,16 +1173,19 @@ def gqrs_detect(sig=None, fs=None, d_sig=None, adc_gain=None, adc_zero=None,
This function should not be used for signals with fs <= 50Hz
The algorithm theoretically works as follows:
- Load in configuration parameters. They are used to set/initialize the:
- allowed rr interval limits (fixed)
- initial recent rr interval (running)
- qrs width, used for detection filter widths (fixed)
- allowed rt interval limits (fixed)
- initial recent rt interval (running)
- initial peak amplitude detection threshold (running)
- initial qrs amplitude detection threshold (running)
**Note** that this algorithm does not normalize signal amplitudes, and
hence is highly dependent on configuration amplitude parameters.
* allowed rr interval limits (fixed)
* initial recent rr interval (running)
* qrs width, used for detection filter widths (fixed)
* allowed rt interval limits (fixed)
* initial recent rt interval (running)
* initial peak amplitude detection threshold (running)
* initial qrs amplitude detection threshold (running)
* `Note`: this algorithm does not normalize signal amplitudes, and
hence is highly dependent on configuration amplitude parameters.
- Apply trapezoid low-pass filtering to the signal
- Convolve a QRS matched filter with the filtered signal
- Run the learning phase using a calculated signal length: detect qrs and
Expand Down
2 changes: 1 addition & 1 deletion wfdb/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.0.2'
__version__ = '2.0.3'

0 comments on commit 5e6d787

Please sign in to comment.