Respiratory Analysis
This notebook demonstrates respiratory rate estimation using multiple methods and sleep apnea event detection from ECG and PPG signals using vitalDSP.
Setup
import numpy as np
import plotly.io as pio
pio.renderers.default = "sphinx_gallery"
from plotly import graph_objects as go
from vitalDSP.notebooks import load_sample_ecg_small, load_sample_ppg, plot_trace
fs = 128
signal_col, date_col = load_sample_ecg_small()
signal_col = np.array(signal_col)
ppg_col, ppg_date = load_sample_ppg()
ppg_col = np.array(ppg_col)
print(f"ECG signal: {len(signal_col)} samples at {fs} Hz")
ECG signal: 82176 samples at 128 Hz
FFT-Based Respiratory Rate Estimation
from vitalDSP.respiratory_analysis.estimate_rr import fft_based_rr, peak_detection_rr, frequency_domain_rr
# FFT method — finds dominant frequency in the respiratory band (0.1-0.5 Hz)
rr_fft = fft_based_rr(signal_col, sampling_rate=fs)
print(f"Respiratory Rate (FFT) : {rr_fft:.2f} breaths/min")
Respiratory Rate (FFT) : 14.30 breaths/min
Peak Detection Respiratory Rate
rr_peaks = peak_detection_rr(signal_col, sampling_rate=fs, min_peak_distance=1.5)
print(f"Respiratory Rate (Peak Detect): {rr_peaks:.2f} breaths/min")
Respiratory Rate (Peak Detect): 37.83 breaths/min
Frequency Domain Respiratory Rate
rr_freq = frequency_domain_rr(signal_col, sampling_rate=fs)
print(f"Respiratory Rate (Freq Domain): {rr_freq:.2f} breaths/min")
WARNING:vitalDSP.respiratory_analysis.estimate_rr.frequency_domain_rr:Low SNR: 1.14
Respiratory Rate (Freq Domain): 15.00 breaths/min
Ensemble Respiratory Rate Estimation
from vitalDSP.respiratory_analysis.respiratory_analysis import RespiratoryAnalysis
ra = RespiratoryAnalysis(signal_col, fs=fs)
# Single method
rr_single = ra.compute_respiratory_rate(method='fft')
print(f"Single Method (FFT): {rr_single:.2f} breaths/min")
# Ensemble: weighted combination of multiple methods
ensemble_result = ra.compute_respiratory_rate_ensemble()
print("\nEnsemble Respiratory Rate Estimation:")
print(f" Rate : {ensemble_result['respiratory_rate']:.2f} breaths/min")
print(f" Confidence : {ensemble_result['confidence']:.3f}")
print(f" Quality : {ensemble_result['quality']}")
WARNING:vitalDSP.respiratory_analysis.estimate_rr.frequency_domain_rr:Low SNR: 1.14
Single Method (FFT): 14.30 breaths/min
Ensemble Respiratory Rate Estimation:
Rate : 21.17 breaths/min
Confidence : 0.047
Quality : medium
Visualizing the Respiratory Band
from vitalDSP.filtering.signal_filtering import SignalFiltering
# Isolate the respiratory frequency band (0.1-0.5 Hz)
sf = SignalFiltering(signal_col)
resp_band = sf.bandpass(0.1, 0.5, fs, order=4)
t = np.arange(len(signal_col)) / fs
fig = go.Figure()
fig.add_trace(go.Scatter(x=t, y=signal_col, mode='lines', name='ECG Signal', opacity=0.4))
fig.add_trace(go.Scatter(x=t, y=resp_band, mode='lines', name='Respiratory Band (0.1-0.5 Hz)'))
fig.update_layout(
title='Respiratory Modulation in ECG Signal',
xaxis_title='Time (s)', yaxis_title='Amplitude'
)
fig.show()
PPG Respiratory Rate
ra_ppg = RespiratoryAnalysis(ppg_col, fs=fs)
rr_ppg = ra_ppg.compute_respiratory_rate(method='fft')
rr_ppg_ensemble = ra_ppg.compute_respiratory_rate_ensemble()
print(f"PPG Respiratory Rate (FFT) : {rr_ppg:.2f} breaths/min")
print(f"PPG Respiratory Rate (Ensemble) : {rr_ppg_ensemble['respiratory_rate']:.2f} breaths/min")
print(f"PPG Ensemble Confidence : {rr_ppg_ensemble['confidence']:.3f}")
WARNING:vitalDSP.respiratory_analysis.estimate_rr.peak_detection_rr:Invalid intervals: [223.2578125 215.0390625 102.9140625]
WARNING:vitalDSP.respiratory_analysis.estimate_rr.frequency_domain_rr:Low SNR: 1.91
PPG Respiratory Rate (FFT) : 7.29 breaths/min
PPG Respiratory Rate (Ensemble) : 9.64 breaths/min
PPG Ensemble Confidence : 0.000
Sleep Apnea Detection
from vitalDSP.respiratory_analysis.sleep_apnea_detection.pause_detection import detect_apnea_pauses
from vitalDSP.respiratory_analysis.sleep_apnea_detection.amplitude_threshold import detect_apnea_amplitude
# Detect apnea pauses (cessation of respiratory activity)
pause_events = detect_apnea_pauses(signal_col, sampling_rate=fs, min_pause_duration=10)
print(f"Apnea pauses detected : {len(pause_events)}")
# Detect apnea by amplitude threshold (signal amplitude drops below threshold)
threshold = np.std(signal_col) * 0.3
amplitude_events = detect_apnea_amplitude(signal_col, sampling_rate=fs, threshold=threshold, min_duration=10)
print(f"Amplitude-based events detected: {len(amplitude_events)}")
# Visualize detected events
fig = go.Figure()
t = np.arange(len(signal_col)) / fs
fig.add_trace(go.Scatter(x=t, y=signal_col, mode='lines', name='Signal', opacity=0.6))
for start, end in pause_events[:5]: # show first 5 events
fig.add_vrect(x0=start/fs, x1=end/fs, fillcolor='red', opacity=0.2, layer='below', line_width=0)
fig.update_layout(
title='Sleep Apnea Pause Detection (red = apnea event)',
xaxis_title='Time (s)', yaxis_title='Amplitude'
)
fig.show()
Apnea pauses detected : 0
Amplitude-based events detected: 0