Part VII: Health, Biosignals, and Wearable AI
Chapter 30: PPG and Wearable Cardiovascular Sensing

SpO2, heart rate, and HRV

"Give me one green LED and I will count your pulse. Give me a red one too, and I will tell you how much oxygen rides on it. Ask me for the variability between beats and I will quietly remind you that I timed a squeeze at your wrist, not a spark in your heart."

A Two-Wavelength AI Agent

Why this section matters

Three of the most-quoted numbers on any wearable, blood-oxygen saturation, heart rate, and heart-rate variability, are all derived from the same optical pulse wave introduced in Section 30.1. None of them is measured directly. Each is an estimate produced by a chain of physics and signal processing that the marketing label hides. This section opens that chain. It shows why oxygen saturation needs exactly two colors of light and an empirically fitted curve, why heart rate is a peak-counting or spectral problem with a motion asterisk, and why the "HRV" a watch reports is a cousin of the clinical quantity rather than the quantity itself. Getting these three right, and knowing their honest error bars, is the difference between a wellness gadget and a signal a clinician will act on.

This section assumes the pulse-waveform physics and the AC/DC decomposition of Section 30.1, and treats each reported value as an estimate \(\hat{s}\) of a hidden physiological state, the measurement framing from Chapter 2. The bandpass filtering that isolates the pulse comes from Chapter 6, the spectral view of heart rate and HRV bands from Chapter 7, and the gold-standard beat timing we compare against from the ECG chapter, Chapter 29. Motion artifact and quality gating are their own topic in Section 30.6; here we assume a clean-enough pulse and focus on the three estimators themselves.

SpO2: why oxygen saturation needs two colors

Pulse oximetry rests on one fact: oxygenated hemoglobin (\(\text{HbO}_2\)) and deoxygenated hemoglobin (Hb) absorb light differently, and the difference flips across wavelengths. In the red band (around \(660\,\text{nm}\)) deoxygenated blood absorbs more; in the near-infrared band (around \(940\,\text{nm}\)) oxygenated blood absorbs more. Shine both, measure how much each is attenuated, and the ratio encodes the oxygen fraction. The trick that makes it work on a moving finger or wrist is to look only at the pulsatile (AC) part of each signal, which comes from arterial blood that swells with each beat, and normalize it by the steady (DC) part, which absorbs tissue, bone, and venous blood. That normalization cancels skin thickness, pigmentation offset, and LED brightness, leaving a quantity that depends on arterial hemoglobin alone. The core statistic is the ratio of ratios:

$$R = \frac{\text{AC}_{660} / \text{DC}_{660}}{\text{AC}_{940} / \text{DC}_{940}}.$$

Beer-Lambert absorption predicts that \(R\) should map monotonically to the functional saturation \(\text{SpO}_2\), but the tissue is a scattering, layered mess that Beer-Lambert only approximates. So no device ships a first-principles formula. Instead each manufacturer fits an empirical curve, calibrated against arterial blood-gas co-oximetry (\(\text{SaO}_2\)) in a controlled desaturation study on human volunteers. A common textbook linearization is \(\text{SpO}_2 \approx 110 - 25\,R\), but the real curve is a polynomial or lookup table locked at the factory. The consequence for an AI engineer: \(\text{SpO}_2\) is a calibrated regression, not a computation, and its accuracy is only as good as the population the calibration study sampled. That single fact is the root of the skin-tone bias that gets its own treatment in Section 30.7, because darker skin adds an absorption offset the DC normalization does not fully remove, and volunteer studies historically under-sampled it.

The AC/DC ratio is the whole idea

Everything clever about pulse oximetry lives in dividing the pulsatile signal by the baseline. The AC component isolates arterial blood (the only compartment that pulses); dividing by DC cancels every static optical nuisance, from melanin to LED aging. Two wavelengths turn one such normalized ratio into a ratio-of-ratios that, by design, no longer depends on path length or perfusion, only on the oxygen fraction. This is why a reflectance sensor on the wrist and a transmission clip on a fingertip can share the same calibration idea despite wildly different geometry.

Heart rate: counting a beat you never touched

Heart rate from PPG is the frequency of the pulse wave, and there are two honest ways to get it. The time-domain route detects each systolic upstroke (a foot-to-peak or a derivative maximum), converts consecutive peak-to-peak gaps into an instantaneous rate \(60 / \Delta t\), and reports a windowed median. The frequency-domain route takes a short window, computes its spectrum, and reads heart rate off the dominant peak in the plausible band (roughly \(0.5\) to \(3.5\,\text{Hz}\), i.e. \(30\) to \(210\) beats per minute). Time-domain gives you every beat and is what HRV needs; frequency-domain is far more robust when the waveform is noisy, because a periodic pulse concentrates energy at one frequency even when individual peaks are hard to localize. Production trackers blend both, plus an accelerometer channel to subtract cadence harmonics (a runner's arm swing at \(2.7\,\text{Hz}\) masquerades as a \(162\,\text{bpm}\) heart rate), a fusion trick we revisit in Section 30.6.

HRV: the honest asterisk

Heart-rate variability is the fluctuation in the gaps between beats, and it is a genuine window onto autonomic tone: parasympathetic ("rest and digest") activity speeds up the beat-to-beat wobble, sympathetic ("fight or flight") activity smooths it out. The clinical vocabulary is small and worth memorizing. In the time domain, SDNN is the standard deviation of the inter-beat intervals over a window, and RMSSD is the root mean square of successive differences, defined for \(N\) intervals \(x_i\) as

$$\text{RMSSD} = \sqrt{\frac{1}{N-1}\sum_{i=1}^{N-1}\left(x_{i+1}-x_i\right)^2}.$$

RMSSD is the workhorse for wearables because it emphasizes fast, beat-to-beat changes (a proxy for vagal tone) and needs only a short window, which is why watches sample it overnight. In the frequency domain, the interval series is resampled and its power split into a low-frequency band (LF, \(0.04\) to \(0.15\,\text{Hz}\)) and a high-frequency band (HF, \(0.15\) to \(0.4\,\text{Hz}\)); the LF/HF ratio is a much-abused summary of autonomic balance, and the band definitions come straight from the spectral machinery of Chapter 7.

PRV is not HRV

The gold standard for HRV is the R-R interval between electrical R-peaks in the ECG (Chapter 29). PPG times a mechanical pulse at the wrist, one pulse-transit-time later, so what you actually measure is pulse-rate variability (PRV). PRV tracks HRV closely at rest but the pulse-arrival time itself jitters with respiration and blood-pressure swings, inflating short-term variability metrics. During motion or arrhythmia the two diverge sharply. Report PPG-derived variability as PRV, validate it against simultaneous ECG before making any autonomic claim, and never present a wrist RMSSD as if it were a clinical Holter reading.

The code below computes RMSSD and SDNN from a list of detected pulse-peak times. It is deliberately short: the science is in the peak detection and the artifact rejection upstream, not the arithmetic.

import numpy as np

def prv_metrics(peak_times_s, min_ibi=0.33, max_ibi=2.0):
    """Time-domain PRV from PPG pulse-peak timestamps (seconds)."""
    ibi = np.diff(np.asarray(peak_times_s)) * 1000.0        # inter-beat intervals, ms
    # Reject physiologically implausible intervals (ectopics, missed/extra peaks)
    ibi = ibi[(ibi > min_ibi * 1000) & (ibi < max_ibi * 1000)]
    sdnn  = float(np.std(ibi, ddof=1))                       # overall variability
    rmssd = float(np.sqrt(np.mean(np.diff(ibi) ** 2)))       # beat-to-beat (vagal)
    hr_bpm = 60000.0 / np.mean(ibi)
    return {"hr_bpm": round(hr_bpm, 1),
            "sdnn_ms": round(sdnn, 1),
            "rmssd_ms": round(rmssd, 1)}

# Synthetic 60 bpm pulse with realistic beat-to-beat jitter
rng = np.random.default_rng(0)
ibis = 1.0 + rng.normal(0, 0.03, 120)          # ~1 s intervals, 30 ms sigma
peaks = np.cumsum(ibis)
print(prv_metrics(peaks))
Time-domain PRV from PPG peak timestamps: the interval-rejection step (dropping implausible gaps) matters more than the RMSSD formula, because one missed peak doubles an interval and corrupts every downstream metric.

Right tool: NeuroKit2 for the full pipeline

The snippet above assumes you already have clean peak times. Getting them (bandpass, systolic-peak detection, artifact correction, then the full battery of time- and frequency-domain HRV indices) is roughly 120 to 150 lines of careful signal processing done by hand. NeuroKit2 collapses it to three:

import neurokit2 as nk
signals, info = nk.ppg_process(ppg, sampling_rate=64)   # filter + peak detect
hrv = nk.hrv(info["PPG_Peaks"], sampling_rate=64)        # SDNN, RMSSD, LF/HF, ...

That is about a 40-to-1 line reduction, and the library handles the parts that silently break naive code: derivative-based peak detection, Kubios-style artifact correction of ectopic intervals, and Lomb-Scargle spectral estimation on the unevenly sampled interval series.

A ring that scores your recovery overnight

A sleep-and-recovery ring vendor computes a nightly "readiness" score dominated by overnight RMSSD. The engineering reality: the device samples PPG at a modest rate to save battery, so it does not trust every beat. It gates on signal quality (skin contact, motion from the onboard accelerometer), keeps only stable stretches during deep sleep when motion is minimal and PRV best approximates HRV, and computes RMSSD only over those windows. The score a user sees is therefore an average of the cleanest few hours, not the whole night. When the vendor validated against a chest-strap ECG, resting RMSSD agreed within a few milliseconds, but they learned to suppress any variability metric during the restless first hour, where PRV inflation would have made stressed users look falsely relaxed. The lesson generalizes: a wearable biomarker is only as trustworthy as the quality gate in front of it.

Reporting the estimate with its error bar

All three quantities are estimates with device-specific error, and a wearable that hides that is a wearable a clinician will not use. Regulators codify it: a cleared \(\text{SpO}_2\) sensor states an accuracy root-mean-square error (\(A_{rms}\), typically \(2\) to \(3\%\) over the \(70\) to \(100\%\) range), heart rate is usually quoted to within a few beats per minute of a reference at rest, and HRV is rarely cleared at all as a diagnostic. The disciplined move is to attach an uncertainty and a quality flag to every reported value rather than a bare number, using the calibration and conformal-prediction methods of Chapter 18. A value the device is not confident in should be withheld or widened, not printed with false precision, which is exactly the gating logic Section 30.6 makes concrete.

Exercise: build and stress-test a PRV estimator

Take a public wrist-PPG dataset with a simultaneous ECG channel (for example the PPG-DaLiA set). (1) Detect PPG systolic peaks and compute RMSSD over five-minute rest windows. (2) Compute the same from the ECG R-peaks and plot PPG-PRV RMSSD against ECG-HRV RMSSD; quantify the bias. (3) Now inject synthetic missed peaks (drop 2% of detections) and measure how much RMSSD inflates, confirming why the interval-rejection step in the code above is not optional. (4) Repeat the comparison on the dataset's cycling segment and report how the PRV-versus-HRV agreement degrades with motion.

Self-check

  1. Why does SpO2 require two wavelengths and a factory-fit calibration curve rather than a single closed-form Beer-Lambert equation?
  2. You must pick one estimator for heart rate on a jogging user and one for computing HRV at rest. Which route (time-domain peaks vs spectral peak) fits each, and why?
  3. A colleague labels a wrist-tracker's overnight RMSSD as "HRV." What single word would you correct, and what physical effect makes the two quantities differ?

What's Next

In Section 30.4, we push the same pulse wave further and ask whether it can estimate blood pressure without a cuff, turning pulse-transit time, waveform morphology, and the very PRV jitter we just flagged as noise into the signal that a cuffless, continuous blood-pressure model learns from.