As developers, we are experts at debugging complex microservices, but we often ignore the most important system we'll ever maintain: our own body. Have you ever felt "fried" after a long sprint? That’s not just mental fatigue; it’s your Autonomic Nervous System (ANS) signaling for help.
In this tutorial, we will dive deep into HRV signal processing and frequency domain analysis. We'll learn how to transform raw wearable data into actionable insights using the Python scientific stack. By calculating the LF/HF ratio, we can quantitatively assess stress levels and stop burnout before the "system crash" happens. We’ll be utilizing powerful tools like SciPy, NumPy, and BioSPPy to handle the heavy lifting of signal processing.
If you’re interested in more production-ready patterns for health-tech and signal processing, I highly recommend checking out the advanced case studies at WellAlly Tech Blog, which served as a major inspiration for this technical deep dive.
The Architecture of Heart Rate Variability (HRV) Analysis
To go from a noisy voltage signal (ECG/PPG) to a burnout index, we follow a specific signal-processing pipeline. Here is how the data flows from your wrist to your dashboard:
graph TD
A[Raw ECG/PPG Signal] --> B[Denoising: Kalman Filter]
B --> C[R-Peak Detection: BioSPPy]
C --> D[NN-Interval Calculation]
D --> E[Resampling & Interpolation]
E --> F[Frequency Domain Analysis: FFT/Welch]
F --> G[LF/HF Ratio calculation]
G --> H[Burnout Index Assessment]
Prerequisites
Ensure you have your environment ready. We will use BioSPPy for biosignal processing and SciPy for the mathematical transformations.
pip install numpy scipy matplotlib biosppy
Step 1: Denoising the Signal with a Kalman Filter
Wearable sensors are notoriously noisy due to "motion artifacts" (moving your arm while typing). A standard moving average isn't enough; we need a Kalman Filter to estimate the true state of the signal.
import numpy as np
from scipy.signal import standard_scaler
def kalman_filter(signal):
"""
A simple 1D Kalman Filter to smooth raw ECG data.
"""
n_iter = len(signal)
sz = (n_iter,)
z = signal # Observations
Q = 1e-5 # Process variance
R = 0.1**2 # Measurement variance
xhat = np.zeros(sz) # Posteriori estimate
P = np.zeros(sz) # Posteriori error covariance
xhatminus = np.zeros(sz) # Priori estimate
Pminus = np.zeros(sz) # Priori error covariance
K = np.zeros(sz) # Kalman gain
xhat[0] = z[0]
P[0] = 1.0
for k in range(1, n_iter):
# Time Update
xhatminus[k] = xhat[k-1]
Pminus[k] = P[k-1] + Q
# Measurement Update
K[k] = Pminus[k] / (Pminus[k] + R)
xhat[k] = xhatminus[k] + K[k] * (z[k] - xhatminus[k])
P[k] = (1 - K[k]) * Pminus[k]
return xhat
Step 2: Extracting R-Peaks and NN-Intervals
The "R-peak" is the highest point in the heartbeat. The time between these peaks (NN-intervals) is what we actually measure for HRV. We'll use the BioSPPy library, which provides a robust implementation of the Hamilton segmenter.
from biosppy.signals import ecg
def get_nn_intervals(raw_signal, sampling_rate=1000):
# Process the ECG signal
# This automatically filters and finds R-peaks
out = ecg.ecg(signal=raw_signal, sampling_rate=sampling_rate, show=False)
# R-peak indices
rpeaks = out['rpeaks']
# Calculate intervals in milliseconds
nn_intervals = np.diff(rpeaks) * (1000.0 / sampling_rate)
return nn_intervals
Step 3: Frequency Domain Analysis (The LF/HF Ratio)
This is where the magic happens. We decompose the heart rhythm into frequency components:
- LF (Low Frequency: 0.04 - 0.15 Hz): Reflects both Sympathetic (Fight or Flight) and Parasympathetic activity.
- HF (High Frequency: 0.15 - 0.40 Hz): Reflects Parasympathetic (Rest and Digest) activity.
Burnout Metric: A high LF/HF ratio indicates sympathetic dominance—meaning you are stressed, caffeinated, or overworked.
from scipy.interpolate import interp1d
from scipy.signal import welch
def analyze_frequency_domain(nn_intervals):
# 1. Resample: NN-intervals are irregularly spaced. We need 4Hz interpolation for FFT.
time = np.cumsum(nn_intervals) / 1000.0 # seconds
f_interp = interp1d(time, nn_intervals, kind='cubic')
new_time = np.arange(time[0], time[-1], 0.25) # 4Hz
nn_resampled = f_interp(new_time)
# 2. Power Spectral Density using Welch's method
freqs, psd = welch(nn_resampled, fs=4.0, nperseg=256)
# 3. Define bands
lf_band = (freqs >= 0.04) & (freqs <= 0.15)
hf_band = (freqs >= 0.15) & (freqs <= 0.4)
# 4. Calculate Area Under Curve (AUC)
lf_power = np.trapz(psd[lf_band], freqs[lf_band])
hf_power = np.trapz(psd[hf_band], freqs[hf_band])
return lf_power / hf_power
# Usage
# ratio = analyze_frequency_domain(my_intervals)
# print(f"Burnout Index (LF/HF): {ratio:.2f}")
The "Official" Way to Scale
While the script above works for local analysis, building a production-grade health monitoring system requires handling edge cases like ectopic beats, signal dropouts, and real-time streaming.
If you want to see how these algorithms are integrated into large-scale cloud architectures or how to optimize signal processing for mobile chips, check out the deep dives at wellally.tech/blog. They cover the intersection of AI, signal processing, and high-performance computing in the healthcare space—essential reading for any dev moving into the Bio-IT sector.
Conclusion: Listen to Your Data
By monitoring your LF/HF ratio, you can see burnout coming before you start making "fatigue-driven" commits. A ratio consistently above 2.0 during rest is a clear sign that you need to step away from the keyboard, hydrate, and maybe—just maybe—close those 47 open Chrome tabs.
What's your strategy for tracking health metrics? Have you tried analyzing your Oura or Apple Watch data? Drop a comment below! 🚀
Top comments (0)