Skip to content

Commit becd585

Browse files
author
Nicolas Marc Simon Legrand
committed
Transfert ready to ECG
1 parent c31064b commit becd585

19 files changed

+291
-229
lines changed

.coverage

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
!coverage.py: This is a private format, don't read it directly!{"lines":{"C:\\Users\\au646069\\github\\systole\\systole\\__init__.py":[1,2,3,4,5,6,8],"C:\\Users\\au646069\\github\\systole\\systole\\detection.py":[3,4,5,6,7,8,12,105,168,215,282,322,55,59,60,61,62,63,66,69,70,306,309,312,313,314,317,319,72,74,75,76,77,78,80,83,84,85,86,87,90,93,96,97,99,102,130,131,133,135,136,143,144,150,151,152,154,155,156,158,159,162,163,165,245,247,248,249,250,251,252,254,253,255,258,259,260,261,262,263,264,265,266,269,270,271,272,273,274,276,277,275,279,186,187,190,191,193,195,197,200,201,203,206,207,208,209,210,212,56],"C:\\Users\\au646069\\github\\systole\\systole\\utils.py":[3,4,5,8,50,79,137,180,108,112,115,117,120,121,126,128,130,131,132,134,123,30,33,34,41,42,43,46,47,35,36,152,154,158,162,163,165,168,171,174,176,208,213,215,216,218,219,220,221,224,225,229,230,233,234,235,239,242,245],"C:\\Users\\au646069\\github\\systole\\systole\\plotting.py":[3,4,5,6,7,8,9,10,13,85,116,157,298,420,357,360,361,363,364,367,370,372,374,376,377,389,390,393,394,395,398,406,409,411,412,413,414,415,417,378,381,382,384,385,379,380,400,401,402,403,451,452,461,463,467,469,471,473,475,477,101,102,103,104,105,106,109,110,111,113,40,42,43,46,53,56,59,60,61,63,64,67,70,71,74,78,79,80,82,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,153,192,193,200,201,202,203,204,205,207,208,214,215,216,219,220,221,223,224,227,228,229,230,233,234,235,236,237,238,241,242,243,244,245,246,249,250,251,253,254,255,256,257,262,263,264,265,268,269,270,271,272,275,276,277,278,279,282,283,284,286,287,288,289,290,291,292,294],"C:\\Users\\au646069\\github\\systole\\systole\\hrv.py":[3,4,5,6,7,10,35,64,98,177,250,339,274,275,276,277,279,282,283,284,287,289,291,292,293,294,298,299,300,301,302,303,306,307,308,311,312,313,315,316,317,320,321,322,325,326,328,329,330,331,333,334,336,206,207,208,209,211,214,215,216,219,221,223,224,225,226,228,230,231,232,233,234,235,236,237,238,239,240,241,242,243,245,247,25,27,31,32,353,354,355,356,357,358,360,362,50,52,56,59,61,88,90,93,95,124,126,130,133,136,139,142,145,148,151,154,157,160,163,166,167,168,169,171,173],"C:\\Users\\au646069\\github\\systole\\systole\\datasets\\__init__.py":[1,2,3,4,6,8,13,16,18,23,32,47,51,69,82,84,64,66,19,20,21,48,34,38,39,42,43,45,24,25,26,30,28],"C:\\Users\\au646069\\github\\systole\\systole\\reports.py":[3,4,5,6,7,10,23,26,29,30,31,33,35,36,37,39,40,41,42,43,44,46,47,48,49,50,51,52,55,57,58,59,60,61,62,64,65,67,68,69,70,71,72,73,75],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\__init__.py":[1],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_detection.py":[3,4,5,6,8,11,13,20,28,37,45,22,23,24,25,26,38,39,40,41,42,30,31,32,33,34,35,15,16,17,18],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_hrv.py":[3,4,5,6,7,9,11,14,16,21,26,31,37,44,50,57,46,47,48,39,40,41,42,18,19,52,53,54,23,24,28,29,33,34,35],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_plotting.py":[3,4,5,6,7,9,10,12,13,14,18,19,20,21,22,23,24,25,26,27,28,31,32,33,34,36,38,42,46,50,57,68,74,82,59,60,61,62,63,64,65,66,70,71,72,43,44,39,40,47,48,75,76,77,78,79],"C:\\Users\\au646069\\github\\systole\\systole\\recording.py":[3,4,5,6,9,98,99,121,187,206,230,242,254,266,285,310,332,101,102,103,104,107,108,109,110,111,112,113,114,115,116,117,143,144,147,148,149,152,153,158,159,160,163,164,179,183,185,155,166,169,172,175,176,180,181,323,324,325,326,327,195,196,197,198,199,200,201,202,204,328,329,274,275,276,278,279,280,283,330,219,220,223,226,228,238,240,250,252,262,264,294,296,297,298,335,336,338,339,340,341,342,345],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_recording.py":[3,4,5,6,7,8,9,12,13,16,18,39,19,20,21,24,25,26,27,29,30,32,33,35,36],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_reports.py":[3,4,5,6,9,11,16,12,13],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_utils.py":[3,4,5,6,7,8,11,13,24,34,43,50,26,27,28,29,30,31,32,14,15,16,17,18,19,20,21,22,36,38,39,40,41,45,46,47,48]}}
1+
!coverage.py: This is a private format, don't read it directly!{"lines":{"C:\\Users\\au646069\\github\\systole\\systole\\__init__.py":[1,2,3,4,5,6,8],"C:\\Users\\au646069\\github\\systole\\systole\\detection.py":[3,4,5,6,7,8,12,105,168,215,282,322,55,59,60,61,62,63,66,69,70,306,309,312,313,314,317,319,72,74,75,76,77,78,80,83,84,85,86,87,90,93,96,97,99,102,130,131,133,135,136,143,144,150,151,152,154,155,156,158,159,162,163,165,245,247,248,249,250,251,252,254,253,255,258,259,260,261,262,263,264,265,266,269,270,271,272,273,274,276,277,275,279,186,187,190,191,193,195,197,200,201,203,206,207,208,209,210,212,56],"C:\\Users\\au646069\\github\\systole\\systole\\utils.py":[3,4,5,8,50,79,139,182,108,110,114,117,119,122,123,128,130,132,133,134,136,125,30,33,34,41,42,43,46,47,35,36,154,156,160,164,165,167,170,173,176,178,210,215,217,218,220,221,222,223,226,227,231,232,235,236,237,241,244,247],"C:\\Users\\au646069\\github\\systole\\systole\\plotting.py":[3,4,5,6,7,8,9,10,11,14,87,121,179,320,394,516,453,456,457,459,460,463,466,468,470,472,473,485,486,489,490,491,494,502,505,507,508,509,510,511,513,474,477,478,480,481,475,476,496,497,498,499,547,548,557,559,563,565,567,569,571,573,103,104,105,106,107,108,109,110,113,114,115,116,118,41,43,54,57,60,61,62,63,65,66,69,72,73,76,80,81,82,84,42,45,52,138,145,146,147,148,149,151,152,153,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,175,349,350,351,352,354,357,358,359,362,364,366,367,368,369,371,373,374,375,376,377,378,379,380,381,382,383,384,385,386,388,390,214,215,222,223,224,225,226,227,229,230,236,237,238,241,242,243,245,246,249,250,251,252,255,256,257,258,259,260,263,264,265,266,267,268,271,272,273,275,276,277,278,279,284,285,286,287,290,291,292,293,294,297,298,299,300,301,304,305,306,308,309,310,311,312,313,314,316,139,140,141,142,143],"C:\\Users\\au646069\\github\\systole\\systole\\hrv.py":[3,4,5,6,9,34,63,97,175,264,199,200,201,202,204,207,208,209,212,214,216,217,218,219,223,224,225,226,227,228,231,232,233,236,237,238,240,241,242,245,246,247,250,251,253,254,255,256,258,259,261,24,26,30,31,278,279,280,281,282,283,285,287,49,51,55,58,60,87,89,92,94,123,125,129,132,135,138,141,144,147,150,153,156,159,162,165,166,167,168,170,172],"C:\\Users\\au646069\\github\\systole\\systole\\datasets\\__init__.py":[1,2,3,4,6,8,13,16,18,23,32,47,51,69,82,84,64,66,19,20,21,48,34,38,39,42,43,45,24,25,26,30,28],"C:\\Users\\au646069\\github\\systole\\systole\\reports.py":[3,4,5,6,7,10,22,23,24,27,30,33,34,35,37,39,40,41,43,44,45,46,47,48,50,51,52,53,54,55,56,59,61,62,63,64,65,66,68,69,71,72,73,74,75,76,77,79],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\__init__.py":[1],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_detection.py":[3,4,5,6,8,11,13,20,28,37,45,22,23,24,25,26,38,39,40,41,42,30,31,32,33,34,35,15,16,17,18],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_hrv.py":[3,4,5,6,7,9,11,14,16,21,26,31,37,43,50,39,40,41,18,19,45,46,47,23,24,28,29,33,34,35],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_plotting.py":[3,4,5,6,7,9,11,12,13,17,18,19,20,21,22,23,24,25,26,27,30,31,32,33,35,37,45,49,53,60,68,79,85,93,70,71,72,73,74,75,76,77,81,82,83,46,47,38,39,40,41,42,43,50,51,62,63,64,65,66,86,87,88,89,90],"C:\\Users\\au646069\\github\\systole\\systole\\recording.py":[3,4,5,6,9,98,99,121,187,206,230,242,254,266,285,310,341,101,102,103,104,107,108,109,110,111,112,113,114,115,116,117,143,144,147,148,149,152,153,158,159,160,163,164,179,183,185,155,166,169,172,175,176,180,181,327,328,329,330,331,195,196,197,198,199,200,201,202,204,332,333,274,275,276,278,279,280,283,336,337,339,219,220,223,226,228,238,240,250,252,262,264,294,296,297,298,344,345,347,348,349,350,351,354],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_recording.py":[3,4,5,6,7,8,11,12,15,17,38,18,19,20,23,24,25,26,28,29,31,32,34,35],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_reports.py":[3,4,5,6,9,11,16,12,13],"C:\\Users\\au646069\\github\\systole\\systole\\tests\\test_utils.py":[3,4,5,6,7,8,11,13,24,34,43,50,26,27,28,29,30,31,32,14,15,16,17,18,19,20,21,22,36,38,39,40,41,45,46,47,48]}}

build/lib/systole/hrv.py

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import numpy as np
44
import pandas as pd
5-
import matplotlib.pyplot as plt
65
from scipy import interpolate
76
from scipy.signal import welch
87

@@ -173,80 +172,6 @@ def time_domain(x):
173172
return stats
174173

175174

176-
def hrv_psd(x, sfreq=5, method='welch', fbands=None, low=0.003,
177-
high=0.4, show=True, ax=None):
178-
"""Plot PSD of heart rate variability.
179-
180-
Parameters
181-
----------
182-
x : 1d array-like
183-
Length of R-R intervals (default is in miliseconds).
184-
sfreq : int
185-
The sampling frequency.
186-
method : str
187-
The method used to extract freauency power. Default set to `'welch'`.
188-
fbands : None or dict, optional
189-
Dictionary containing the names of the frequency bands of interest
190-
(str), their range (tuples) and their color in the PSD plot. Default is
191-
{'vlf': ['Very low frequency', (0.003, 0.04), 'b'],
192-
'lf': ['Low frequency', (0.04, 0.15), 'g'],
193-
'hf': ['High frequency', (0.15, 0.4), 'r']}
194-
show : boolean
195-
Plot the power spectrum density. Default is `True`.
196-
ax : Matplotlib.Axes instance | None
197-
Where to draw the plot. Default is ´None´ (create a new figure).
198-
199-
Returns
200-
-------
201-
ax | freq, psd : Matplotlib instance | numpy array
202-
If `show=True`, return the PSD plot. If `show=False`, will return the
203-
frequencies and PSD level as arrays.
204-
"""
205-
# Interpolate R-R interval
206-
time = np.cumsum(x)
207-
f = interpolate.interp1d(time, x, kind='cubic')
208-
new_time = np.arange(time[0], time[-1], 1000/sfreq) # Sampling rate = 5 Hz
209-
x = f(new_time)
210-
211-
if method == 'welch':
212-
213-
# Define window length
214-
nperseg = 256 * sfreq
215-
if nperseg > len(x):
216-
nperseg = len(x)
217-
218-
# Compute Power Spectral Density
219-
freq, psd = welch(x=x, fs=sfreq, nperseg=nperseg, nfft=nperseg)
220-
221-
psd = psd/1000000
222-
223-
if fbands is None:
224-
fbands = {'vlf': ['Very low frequency', (0.003, 0.04), 'b'],
225-
'lf': ['Low frequency', (0.04, 0.15), 'g'],
226-
'hf': ['High frequency', (0.15, 0.4), 'r']}
227-
228-
if show is True:
229-
# Plot the PSD
230-
if ax is None:
231-
fig, ax = plt.subplots(figsize=(8, 4))
232-
ax.plot(freq, psd, 'k')
233-
for f in ['vlf', 'lf', 'hf']:
234-
mask = (freq >= fbands[f][1][0]) & (freq <= fbands[f][1][1])
235-
ax.fill_between(freq, psd, where=mask, alpha=0.5,
236-
color=fbands[f][2])
237-
ax.axvline(x=fbands[f][1][0],
238-
linestyle='--',
239-
color='gray')
240-
ax.set_xlim(0.003, 0.4)
241-
ax.set_xlabel('Frequency [Hz]')
242-
ax.set_ylabel('PSD [$s^2$/Hz]')
243-
ax.set_title('Power Spectral Density', fontweight='bold')
244-
245-
return ax
246-
else:
247-
return freq, psd
248-
249-
250175
def frequency_domain(x, sfreq=5, method='welch', fbands=None):
251176
"""Extract the frequency domain features of heart rate variability.
252177

build/lib/systole/plotting.py

Lines changed: 126 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import pandas as pd
66
import matplotlib.pyplot as plt
77
import seaborn as sns
8-
from systole.detection import hrv_subspaces
8+
from systole.detection import hrv_subspaces, oxi_peaks
99
from systole.utils import heart_rate
1010
from scipy.interpolate import interp1d
11+
from scipy.signal import welch
1112

1213

1314
def plot_hr(x, sfreq=75, outliers=None, unit='rr', kind='cubic', ax=None):
@@ -39,25 +40,26 @@ def plot_hr(x, sfreq=75, outliers=None, unit='rr', kind='cubic', ax=None):
3940
"""
4041
if isinstance(x, list):
4142
x = np.asarray(x)
42-
if not isinstance(x, np.ndarray):
43-
x = np.asarray(x.peaks)
44-
45-
# If a RR time serie is provided, transform to peaks vector
46-
if not ((x == 0) | (x == 1)).all():
47-
x = np.round(x).astype(int)
48-
peaks = np.zeros(np.cumsum(x)[-1])
49-
peaks = np.insert(peaks, 0, 1)
50-
peaks[np.cumsum(x)] = 1
51-
sfreq = 1000
52-
else:
53-
peaks = x
43+
if isinstance(x, np.ndarray):
44+
# If a RR time serie is provided, transform to peaks vector
45+
if not ((x == 0) | (x == 1)).all():
46+
x = np.round(x).astype(int)
47+
peaks = np.zeros(np.cumsum(x)[-1])
48+
peaks = np.insert(peaks, 0, 1)
49+
peaks[np.cumsum(x)] = 1
50+
sfreq = 1000
51+
else:
52+
peaks = x
53+
else: # Oximeter instance
54+
peaks = np.asarray(x.peaks)
5455

5556
# Compute the interpolated instantaneous heart rate
5657
hr, times = heart_rate(peaks, sfreq=sfreq, unit=unit, kind=kind)
5758

5859
# New peaks vector
5960
f = interp1d(np.arange(0, len(peaks)/sfreq, 1/sfreq), peaks,
60-
kind='linear', bounds_error=False, fill_value=(0, 0))
61+
kind='linear', bounds_error=False,
62+
fill_value=(np.nan, np.nan))
6163
new_peaks = f(times)
6264

6365
if ax is None:
@@ -83,7 +85,7 @@ def plot_hr(x, sfreq=75, outliers=None, unit='rr', kind='cubic', ax=None):
8385

8486

8587
def plot_events(oximeter, ax=None):
86-
"""Plot events distribution.
88+
"""Plot events occurence across recording.
8789
8890
Parameters
8991
----------
@@ -116,13 +118,15 @@ def plot_events(oximeter, ax=None):
116118
return ax
117119

118120

119-
def plot_oximeter(oximeter, ax=None):
120-
"""Plot recorded PPG signal.
121+
def plot_oximeter(x, sfreq=75, ax=None):
122+
"""Plot PPG signal.
121123
122124
Parameters
123125
----------
124-
oximeter : `systole.recording.Oximeter`
125-
The Oximeter instance used to record the signal.
126+
x : 1d array-like or `systole.recording.Oximeter`
127+
The ppg signal, or the Oximeter instance used to record the signal.
128+
sfreq : int
129+
Signal sampling frequency. Default is 75 Hz.
126130
ax : `Matplotlib.Axes` or None
127131
Where to draw the plot. Default is *None* (create a new figure).
128132
@@ -131,25 +135,39 @@ def plot_oximeter(oximeter, ax=None):
131135
ax : `Matplotlib.Axes`
132136
The figure.
133137
"""
138+
if isinstance(x, (list, np.ndarray)):
139+
times = np.arange(0, len(x)/sfreq, 1/sfreq)
140+
recording = np.asarray(x)
141+
signal, peaks = oxi_peaks(x, new_sfreq=sfreq)
142+
threshold = None
143+
label = 'Offline estimation'
144+
else:
145+
times = np.asarray(x.times)
146+
recording = np.asarray(x.recording)
147+
peaks = np.asarray(x.recording)
148+
threshold = np.asarray(x.threshold)
149+
label = 'Online estimation'
150+
134151
if ax is None:
135152
fig, ax = plt.subplots(figsize=(13, 5))
136153
ax.set_title('Oximeter recording', fontweight='bold')
137-
ax.plot(oximeter.times, oximeter.threshold, linestyle='--', color='gray',
138-
label='Threshold')
139-
ax.fill_between(x=oximeter.times,
140-
y1=oximeter.threshold,
141-
y2=np.asarray(oximeter.recording).min(),
142-
alpha=0.2,
143-
color='gray')
144-
ax.plot(oximeter.times, oximeter.recording, label='Recording',
154+
155+
if threshold is not None:
156+
ax.plot(times, threshold, linestyle='--', color='gray',
157+
label='Threshold')
158+
ax.fill_between(x=times,
159+
y1=threshold,
160+
y2=recording.min(),
161+
alpha=0.2,
162+
color='gray')
163+
ax.plot(times, recording, label='Recording', linewidth=.2,
145164
color='#4c72b0')
146-
ax.fill_between(x=oximeter.times,
147-
y1=oximeter.recording,
148-
y2=np.asarray(oximeter.recording).min(),
165+
ax.fill_between(x=times,
166+
y1=recording,
167+
y2=recording.min(),
149168
color='w')
150-
ax.plot(np.asarray(oximeter.times)[np.where(oximeter.peaks)[0]],
151-
np.asarray(oximeter.recording)[np.where(oximeter.peaks)[0]],
152-
color='#c44e52', linestyle=None, label='Online estimation')
169+
ax.plot(times[np.where(peaks)[0]], recording[np.where(peaks)[0]], 'o',
170+
color='#c44e52', markersize=0.8, label=label)
153171
ax.set_ylabel('PPG level')
154172
ax.set_xlabel('Time (s)')
155173
ax.legend()
@@ -298,6 +316,80 @@ def f2(x): return -c1*x - c2
298316
return ax
299317

300318

319+
def plot_psd(x, sfreq=5, method='welch', fbands=None, low=0.003,
320+
high=0.4, show=True, ax=None):
321+
"""Plot PSD of heart rate variability.
322+
323+
Parameters
324+
----------
325+
x : 1d array-like
326+
Length of R-R intervals (default is in miliseconds).
327+
sfreq : int
328+
The sampling frequency.
329+
method : str
330+
The method used to extract freauency power. Default set to `'welch'`.
331+
fbands : None or dict, optional
332+
Dictionary containing the names of the frequency bands of interest
333+
(str), their range (tuples) and their color in the PSD plot. Default is
334+
{'vlf': ['Very low frequency', (0.003, 0.04), 'b'],
335+
'lf': ['Low frequency', (0.04, 0.15), 'g'],
336+
'hf': ['High frequency', (0.15, 0.4), 'r']}
337+
show : boolean
338+
Plot the power spectrum density. Default is `True`.
339+
ax : Matplotlib.Axes instance | None
340+
Where to draw the plot. Default is ´None´ (create a new figure).
341+
342+
Returns
343+
-------
344+
ax | freq, psd : Matplotlib instance | numpy array
345+
If `show=True`, return the PSD plot. If `show=False`, will return the
346+
frequencies and PSD level as arrays.
347+
"""
348+
# Interpolate R-R interval
349+
time = np.cumsum(x)
350+
f = interp1d(time, x, kind='cubic')
351+
new_time = np.arange(time[0], time[-1], 1000/sfreq) # Sampling rate = 5 Hz
352+
x = f(new_time)
353+
354+
if method == 'welch':
355+
356+
# Define window length
357+
nperseg = 256 * sfreq
358+
if nperseg > len(x):
359+
nperseg = len(x)
360+
361+
# Compute Power Spectral Density
362+
freq, psd = welch(x=x, fs=sfreq, nperseg=nperseg, nfft=nperseg)
363+
364+
psd = psd/1000000
365+
366+
if fbands is None:
367+
fbands = {'vlf': ['Very low frequency', (0.003, 0.04), 'b'],
368+
'lf': ['Low frequency', (0.04, 0.15), 'g'],
369+
'hf': ['High frequency', (0.15, 0.4), 'r']}
370+
371+
if show is True:
372+
# Plot the PSD
373+
if ax is None:
374+
fig, ax = plt.subplots(figsize=(8, 4))
375+
ax.plot(freq, psd, 'k')
376+
for f in ['vlf', 'lf', 'hf']:
377+
mask = (freq >= fbands[f][1][0]) & (freq <= fbands[f][1][1])
378+
ax.fill_between(freq, psd, where=mask, alpha=0.5,
379+
color=fbands[f][2])
380+
ax.axvline(x=fbands[f][1][0],
381+
linestyle='--',
382+
color='gray')
383+
ax.set_xlim(0.003, 0.4)
384+
ax.set_xlabel('Frequency [Hz]')
385+
ax.set_ylabel('PSD [$s^2$/Hz]')
386+
ax.set_title('Power Spectral Density', fontweight='bold')
387+
388+
return ax
389+
else:
390+
return freq, psd
391+
392+
301393
def circular(data, bins=32, density='area', offset=0, mean=False, norm=True,
302394
units='radians', color=None, ax=None):
303395
"""Plot polar histogram.
@@ -445,7 +537,7 @@ def plot_circular(data, y=None, hue=None, **kwargs):
445537
446538
import numpy as np
447539
import pandas as pd
448-
from systole.circular import plot_circular
540+
from systole.plotting import plot_circular
449541
x = np.random.normal(np.pi, 0.5, 100)
450542
y = np.random.uniform(0, np.pi*2, 100)
451543
data = pd.DataFrame(data={'x': x, 'y': y}).melt()

0 commit comments

Comments
 (0)