Skip to content

Commit 1ea9a31

Browse files
authored
Merge pull request #40 from smoia/enh/metrics
Improve peak detection utility and optimize metrics
2 parents 8822555 + 674fa7e commit 1ea9a31

File tree

2 files changed

+48
-57
lines changed

2 files changed

+48
-57
lines changed

physioqc/metrics/multimodal.py

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import warnings
44

55
import numpy as np
6-
import peakdet as pk
6+
from peakdet import Physio
7+
from peakdet.operations import peakfind_physio
78
from scipy import signal
89

9-
from .utils import has_peakfind_physio, physio_or_numpy
10+
from .utils import has_peaks, physio_or_numpy
1011

1112

1213
@physio_or_numpy
@@ -42,8 +43,7 @@ def std(signal):
4243
N-sized array :obj:`numpy.ndarray`
4344
Standard deviation of signal.
4445
"""
45-
std_val = np.std(signal, axis=0)
46-
return std_val
46+
return np.std(signal, axis=0)
4747

4848

4949
@physio_or_numpy
@@ -61,8 +61,7 @@ def mean(signal: np.array):
6161
N-sized array :obj:`numpy.ndarray`
6262
Mean of signal.
6363
"""
64-
mean_val = np.mean(signal, axis=0)
65-
return mean_val
64+
return np.mean(signal, axis=0)
6665

6766

6867
@physio_or_numpy
@@ -80,8 +79,7 @@ def tSNR(signal):
8079
N-sized array :obj:`numpy.ndarray`
8180
Temporal signal to noise ratio of signal.
8281
"""
83-
tSNR_val = np.mean(signal, axis=0) / np.std(signal, axis=0)
84-
return tSNR_val
82+
return np.mean(signal, axis=0) / np.std(signal, axis=0)
8583

8684

8785
@physio_or_numpy
@@ -99,8 +97,7 @@ def CoV(signal):
9997
N-sized array :obj:`numpy.ndarray`
10098
Temporal signal to noise ratio of signal.
10199
"""
102-
cov_val = np.std(signal, axis=0) / np.mean(signal, axis=0)
103-
return cov_val
100+
return np.std(signal, axis=0) / np.mean(signal, axis=0)
104101

105102

106103
@physio_or_numpy
@@ -118,8 +115,7 @@ def min(signal: np.array):
118115
N-sized array :obj:`numpy.ndarray`
119116
min of signal.
120117
"""
121-
min_val = np.min(signal, axis=0)
122-
return min_val
118+
return np.min(signal, axis=0)
123119

124120

125121
@physio_or_numpy
@@ -137,8 +133,7 @@ def max(signal: np.array):
137133
N-sized array :obj:`numpy.ndarray`
138134
max of signal.
139135
"""
140-
max_val = np.max(signal, axis=0)
141-
return max_val
136+
return np.max(signal, axis=0)
142137

143138

144139
@physio_or_numpy
@@ -160,9 +155,8 @@ def iqr(signal: np.array, q_high: float = 75, q_low: float = 25):
160155
iqr of the signal
161156
"""
162157
p_high, p_low = np.percentile(signal, [q_high, q_low], axis=0)
163-
iqr_val = p_high - p_low
164158

165-
return iqr_val
159+
return p_high - p_low
166160

167161

168162
@physio_or_numpy
@@ -181,13 +175,11 @@ def percentile(signal: np.array, perc: float = 2):
181175
np.array
182176
percentile of the signal
183177
"""
184-
perc_val = np.percentile(signal, axis=0, q=perc)
185-
186-
return perc_val
178+
return np.percentile(signal, q=perc, axis=0)
187179

188180

189181
def peak_detection(
190-
data: pk.Physio,
182+
data: Physio,
191183
peak_threshold: float = 0.1,
192184
peak_dist: float = 60.0,
193185
):
@@ -196,7 +188,7 @@ def peak_detection(
196188
197189
Parameters
198190
----------
199-
data : pk.Physio
191+
data : Physio
200192
A peakdet Physio object
201193
peak_threshold : float, optional
202194
Threshold for peak detection, by default 0.1
@@ -205,58 +197,60 @@ def peak_detection(
205197
206198
Returns
207199
-------
208-
pk.Physio
209-
Updated pk.Physio class with peaks etc.
200+
Physio
201+
Updated Physio class with peaks etc.
210202
"""
211-
ph = pk.operations.peakfind_physio(data, thresh=peak_threshold, dist=peak_dist)
212-
213-
return ph
203+
return peakfind_physio(data, thresh=peak_threshold, dist=peak_dist)
214204

215205

216-
def peak_distance(ph: pk.Physio):
217-
"""Calculate the time between peaks (first derivative of onsets).
206+
def peak_distance(
207+
ph: Physio,
208+
peak_threshold: float = 0.1,
209+
peak_dist: float = 60.0,
210+
):
211+
"""Calculate the time (in seconds) between peaks (first derivative of onsets).
218212
219213
Parameters
220214
----------
221-
ph : pk.Physio
222-
A pk.Physio object, that contains peak information.
215+
ph : Physio
216+
A Physio object, that contains peak information.
223217
224218
Returns
225219
-------
226220
np.array
227221
np.array of shape [npeaks, ]
228222
"""
229-
if not has_peakfind_physio(ph):
223+
if not has_peaks(ph):
230224
warnings.warn("Peaks not estimated, estimating")
231-
ph = peak_detection(ph)
232-
233-
diff_peak = np.diff(ph.peaks, axis=0)
225+
ph = peakfind_physio(ph, thresh=peak_threshold, dist=peak_dist)
234226

235-
return diff_peak
227+
return np.diff(ph.peaks, axis=0) / ph.fs
236228

237229

238-
def peak_amplitude(ph: pk.Physio):
230+
def peak_amplitude(
231+
ph: Physio,
232+
peak_threshold: float = 0.1,
233+
peak_dist: float = 60.0,
234+
):
239235
"""Return the amplitude for each peak in the ph.Physio object (peak - trough).
240236
241237
Parameters
242238
----------
243-
ph : pk.Physio
244-
pk.Physio object with peak and trough information.
239+
ph : Physio
240+
Physio object with peak and trough information.
245241
246242
Returns
247243
-------
248244
np.array
249245
np.array of shape [npeaks - 1, ]
250246
"""
251-
if not has_peakfind_physio(ph):
247+
if not has_peaks(ph):
252248
warnings.warn("Peaks not estimated, estimating")
253-
ph = peak_detection(ph)
249+
ph = peakfind_physio(ph, thresh=peak_threshold, dist=peak_dist)
254250
# Assuming that peaks and troughs are aligned. Last peak has no trough.
255251
peak_amp = ph.data[ph.peaks[:-1]]
256252
trough_amp = ph.data[ph.troughs]
257-
peak_amplitude = peak_amp - trough_amp
258-
259-
return peak_amplitude
253+
return peak_amp - trough_amp
260254

261255

262256
def power_spectrum(data):
@@ -309,9 +303,7 @@ def energy(data, lowf=None, highf=None):
309303
# Define frequency band
310304
idx_band = np.logical_and(freqs >= lowf, freqs <= highf)
311305

312-
energy = np.sum(energy_density[idx_band])
313-
314-
return energy
306+
return np.sum(energy_density[idx_band])
315307

316308

317309
def fALFF(data, lowf=0, highf=0.5):
@@ -346,9 +338,7 @@ def fALFF(data, lowf=0, highf=0.5):
346338
total_energy = energy(data)
347339

348340
# Compute the relative energy
349-
rel_energy = band_energy / total_energy
350-
351-
return rel_energy
341+
return band_energy / total_energy
352342

353343

354344
def freq_energy(data, thr=0.5):
@@ -372,6 +362,4 @@ def freq_energy(data, thr=0.5):
372362
The value of the threshold has been selected randomly for now. Please update it with a more meaningful value.
373363
"""
374364
energy_nd = energy(data)
375-
freq = np.argmax(energy_nd > thr)
376-
377-
return freq
365+
return np.argmax(energy_nd > thr)

physioqc/metrics/utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import functools
44
import logging
55

6+
from peakdet.physio import Physio
7+
68
LGR = logging.getLogger(__name__)
79
LGR.setLevel(logging.INFO)
810

@@ -58,8 +60,8 @@ def wrapper(*args, **kwargs):
5860
return wrapper
5961

6062

61-
def has_peakfind_physio(signal) -> bool:
62-
"""Check if "peakfind_physio" is in signal's history.
63+
def has_peaks(signal: Physio) -> bool:
64+
"""Check if the signal is a Physio object and has (at least 2) peaks.
6365
6466
Parameters
6567
----------
@@ -77,8 +79,9 @@ def has_peakfind_physio(signal) -> bool:
7779
Raises error if object does not have a history attribute.
7880
"""
7981
if not hasattr(signal, "history"):
80-
raise AttributeError("Signal has to be a Physio object!")
82+
raise AttributeError("Signal has to be a peakdet Physio object!")
8183

82-
has_peakfind = any(["peakfind_physio" in i for i in signal.history])
84+
if signal._metadata["peaks"].size == 1:
85+
LGR.warn("Signal has only one peak! Better to rerun peak detection.")
8386

84-
return has_peakfind
87+
return signal._metadata["peaks"].size > 1

0 commit comments

Comments
 (0)