# Configure Plotly to render inline figures via CDN so they display
# in the rendered Sphinx output (RTD).  Safe to ignore when running
# locally in Jupyter.
import plotly.io as pio
pio.renderers.default = 'notebook_connected'

Import and load sample ECG & PPG

import vital_sqi
from vital_sqi.data.signal_io import ECG_reader,PPG_reader
import os
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import matplotlib.pyplot as plt

file_name = "example.edf"
ecg_data = ECG_reader(os.path.join("test_data",file_name),'edf')
file_name = "ppg_smartcare.csv"
ppg_data = PPG_reader(os.path.join("test_data",file_name),
                    signal_idx=6,
                    timestamp_idx= 1)
Matplotlib is building the font cache; this may take a moment.

Explain the signal object structure

The signal object contains the following attributes:

  1. signals: a dataframe stores the raw signals mark by a timestamp column

  2. sampling_rate: the signal sampling_rate

  3. wave_type: either ‘ECG’ or ‘PPG’

  4. infor: additional information retrieve from the csv (other columns) or edf file

  5. sqis: a dataframe stores the scores of all sqis computation.

  6. rules: the list of decision rule with the threshold to reject invalid signal

  7. ruleset: the set of ruleset will be applied to determine valid/invalid signal

print("List of ECG object attributes: ",list(ecg_data.__dict__.keys()))
print("List of PPG object attributes: ",list(ppg_data.__dict__.keys()))
List of ECG object attributes:  ['wave_type', 'signals', 'sampling_rate', 'start_datetime', 'info', 'sqis', 'rules', 'ruleset']
List of PPG object attributes:  ['wave_type', 'signals', 'sampling_rate', 'start_datetime', 'info', 'sqis', 'rules', 'ruleset']
ecg_signals =  ecg_data.signals
ecg_sampling_rate = int(ecg_data.sampling_rate)
print(ecg_signals.head())

ppg_signals =  ppg_data.signals
ppg_sampling_rate = int(ppg_data.sampling_rate)
print(ppg_signals.head())
                     timestamps             0            1
0 2019-01-22 01:04:13.000000000 -13675.252155 -7651.251911
1 2019-01-22 01:04:13.003906250  -1351.679835   191.522423
2 2019-01-22 01:04:13.007812500  12283.614405  9414.911635
3 2019-01-22 01:04:13.011718750  10867.175189  7609.916136
4 2019-01-22 01:04:13.015625000  -2296.891218 -2713.004685
                  COUNTER  PLETH
0 1970-01-01 00:00:00.009  34019
1 1970-01-01 00:00:00.010  33322
2 1970-01-01 00:00:00.011  32664
3 1970-01-01 00:00:00.012  32003
4 1970-01-01 00:00:00.013  31360

Explore the ECG signal with different channels

ppg_signals
COUNTER PLETH
0 1970-01-01 00:00:00.009 34019
1 1970-01-01 00:00:00.010 33322
2 1970-01-01 00:00:00.011 32664
3 1970-01-01 00:00:00.012 32003
4 1970-01-01 00:00:00.013 31360
... ... ...
61268 1970-01-01 00:01:01.450 12298
61269 1970-01-01 00:01:01.451 11386
61270 1970-01-01 00:01:01.452 10529
61271 1970-01-01 00:01:01.453 9729
61272 1970-01-01 00:01:01.454 9020

61273 rows × 2 columns

fig = go.Figure()
fig.add_trace(go.Scatter(x= ecg_signals.iloc[:,0],
                         y= ecg_signals.iloc[:,1],
                         name='channel 1'))
fig.add_trace(go.Scatter(x= ecg_signals.iloc[:,0],
                         y= ecg_signals.iloc[:,2],
                         name='channel 2'))
fig.show()

fig = go.Figure()
fig.add_trace(go.Scatter(x= ppg_signals.iloc[:,0],
                         y= ppg_signals.iloc[:,1],
                         name='ppg signal'))
fig.show()

Example of splitting the whole data into subsegment using time domain for ECG.

The whole channel length will be splitted into each n-second segment

Notes:

  1. Segment is split by time (every n-second) (type=0) or by beat (every n-beat) (type=1)

  2. The process is executed in only 1 channel at a time

  3. The split segment also have the option of overlapping

from vital_sqi.preprocess.segment_split import split_segment
#======================================================
#ECG signal
#======================================================
channel_1 = ecg_signals.iloc[:,[0,1]]
channel_2 = ecg_signals.iloc[:,[0,2]]
n = 10
segments_channel_1, segment_channel_1_milestones = split_segment(channel_1,
                                                   sampling_rate= ecg_sampling_rate,
                                                   split_type=0,
                                                   duration=n)
segments_channel_2, segment_channel_2_milestones = split_segment(channel_2,
                                                   sampling_rate= ecg_sampling_rate,
                                                   split_type=0,
                                                   duration=n)

# ecg_sample_idx = 0 
ecg_sample_idx = np.random.randint(len(segments_channel_1))
fig = go.Figure()
fig.add_trace(go.Scatter(x= segments_channel_1[ecg_sample_idx].iloc[:,0],
                         y= segments_channel_1[ecg_sample_idx].iloc[:,1],
                         name='channel 1'))
fig.add_trace(go.Scatter(x= segments_channel_2[ecg_sample_idx].iloc[:,0],
                         y= segments_channel_2[ecg_sample_idx].iloc[:,1],
                         name='channel 2'))
fig.show()

#======================================================
#PPG signal
#======================================================
ppg_sig = ppg_signals.iloc[:,:2]
n = 10
segments_ppg, segment_ppg_milestones = split_segment(ppg_sig,
                                                   sampling_rate= ppg_sampling_rate,
                                                   split_type=0,
                                                   duration=n)

# ppg_sample_idx = 55 
ppg_sample_idx = np.random.randint(len(segments_ppg))
fig = go.Figure()
fig.add_trace(go.Scatter(x= segments_ppg[ppg_sample_idx].iloc[:,0],
                         y= segments_ppg[ppg_sample_idx].iloc[:,2],
                         name='ppg signal'))
fig.show()

Data Preprocessing

We will manipulate the following features:

  1. bandpass filtering

  2. smoothing

  3. tapering

# -----------------------------------------------------
# Apply band pass filter on ECG
# -----------------------------------------------------
from vital_sqi.common.band_filter import BandpassFilter
# Create instances
butter_bandpass = BandpassFilter("butter", fs=ecg_sampling_rate)
cheby_bandpass = BandpassFilter("cheby1", fs=ecg_sampling_rate)
ellip_bandpass = BandpassFilter("ellip", fs=ecg_sampling_rate)

s1_ecg = segments_channel_1[ecg_sample_idx].iloc[:,1]
times_ecg = segments_channel_1[ecg_sample_idx].iloc[:,0]
# Apply
b1_ecg = butter_bandpass.signal_highpass_filter(s1_ecg, cutoff=1, order=5)
b2_ecg = butter_bandpass.signal_highpass_filter(s1_ecg, cutoff=0.8, order=5)
b3_ecg = butter_bandpass.signal_highpass_filter(s1_ecg, cutoff=0.6, order=5)
c1_ecg = cheby_bandpass.signal_highpass_filter(s1_ecg, cutoff=1, order=5)
e1_ecg = ellip_bandpass.signal_highpass_filter(s1_ecg, cutoff=1, order=5)

fig = go.Figure()
# Add traces
fig.add_trace(go.Scatter(x=times_ecg, y=s1_ecg, name='original'))
fig.add_trace(go.Scatter(x=times_ecg, y=b1_ecg, name='f=Butter, cutoff 1Hz'))
fig.add_trace(go.Scatter(x=times_ecg, y=b2_ecg, name='f=Butter, cutoff 0.8Hz'))
fig.add_trace(go.Scatter(x=times_ecg, y=b3_ecg, name='f=Butter, cutoff 0.6Hz'))
fig.add_trace(go.Scatter(x=times_ecg, y=c1_ecg, name='f=Butter, cutoff 0.6Hz'))
fig.add_trace(go.Scatter(x=times_ecg, y=e1_ecg, name='f=Butter, cutoff 0.6Hz'))

fig.show()

# -----------------------------------------------------
# Apply band pass filter on PPG
# -----------------------------------------------------
# Create instances
butter_bandpass = BandpassFilter("butter", fs=ppg_sampling_rate)
cheby_bandpass = BandpassFilter("cheby1", fs=ppg_sampling_rate)
ellip_bandpass = BandpassFilter("ellip", fs=ppg_sampling_rate)

s_ppg = segments_ppg[ppg_sample_idx].iloc[:,2]
times_ppg = segments_ppg[ppg_sample_idx].iloc[:,0]
# Apply
b1_ppg = butter_bandpass.signal_highpass_filter(s_ppg, cutoff=1, order=5)
b2_ppg = butter_bandpass.signal_highpass_filter(s_ppg, cutoff=0.8, order=5)
b3_ppg = butter_bandpass.signal_highpass_filter(s_ppg, cutoff=0.6, order=5)
c1_ppg = cheby_bandpass.signal_highpass_filter(s_ppg, cutoff=1, order=5)
e1_ppg = ellip_bandpass.signal_highpass_filter(s_ppg, cutoff=1, order=5)

fig = go.Figure()
# Add traces
fig.add_trace(go.Scatter(x=times_ppg, y=s_ppg, name='original'))
fig.add_trace(go.Scatter(x=times_ppg, y=b1_ppg, name='f=Butter, cutoff 1Hz'))
fig.add_trace(go.Scatter(x=times_ppg, y=b2_ppg, name='f=Butter, cutoff 0.8Hz'))
fig.add_trace(go.Scatter(x=times_ppg, y=b3_ppg, name='f=Butter, cutoff 0.6Hz'))
fig.add_trace(go.Scatter(x=times_ppg, y=c1_ppg, name='f=Butter, cutoff 0.6Hz'))
fig.add_trace(go.Scatter(x=times_ppg, y=e1_ppg, name='f=Butter, cutoff 0.6Hz'))

fig.show()
# --------------------------------------------------
# Apply Smooth Signal and Tapering on ECG
# --------------------------------------------------
from vital_sqi.preprocess.preprocess_signal import smooth_signal,taper_signal
# Apply
smoothed_s1_ecg = smooth_signal(s1_ecg,window_len=5, window='flat')
tapered_smoothed_s1_ecg = taper_signal(smoothed_s1_ecg,shift_min_to_zero=False)

fig = go.Figure()
# Add traces
fig.add_trace(go.Scatter(x=times_ecg, y=s1_ecg, name='original'))
fig.add_trace(go.Scatter(x=times_ecg, y=smoothed_s1_ecg, name='smoothed signal'))
fig.add_trace(go.Scatter(x=times_ecg, y=tapered_smoothed_s1_ecg, name='tapered smoothed signal'))

fig.show()


# --------------------------------------------------
# Apply Smooth Signal and Tapering on PPG
# --------------------------------------------------
smoothed_s_ppg = smooth_signal(s_ppg,window_len=5, window='flat')
tapered_smoothed_s_ppg = taper_signal(b2_ppg, shift_min_to_zero=False)

fig = go.Figure()
# Add traces
fig.add_trace(go.Scatter(x=times_ppg, y=s_ppg, name='original'))
fig.add_trace(go.Scatter(x=times_ppg, y=smoothed_s_ppg, name='smoothed signal'))
fig.add_trace(go.Scatter(x=times_ppg, y=tapered_smoothed_s_ppg, name='tapered smoothed signal'))

fig.show()

Example of trimming the first and the last n-minute data.

The before and after trimming 5 minutes segment (300 seconds)

from vital_sqi.preprocess.removal_utilities import trim_signal

trimmed_ecg = trim_signal(ecg_signals.iloc[:,:2], 
                          ecg_sampling_rate, 
                          duration_left=300, 
                          duration_right=300)
fig = go.Figure()
fig.add_trace(go.Scatter(x= ecg_signals.iloc[:,0],
                         y= ecg_signals.iloc[:,1],
                         name='channel 1'))
fig.add_trace(go.Scatter(x= trimmed_ecg.iloc[:,0],
                         y= trimmed_ecg.iloc[:,1],
                         name='trimmed channel 1'))
fig.show()