ECG Morphology: PQRST Segmentation and Interval Analysis

Introduction

Beyond heart rate and HRV analysis, the morphology of individual ECG waveforms carries important clinical and research information. PhysioECG provides functions for PQRST waveform delineation and computation of standard clinical ECG intervals (PR, QT, QTc, QRS duration).

This vignette demonstrates:

  1. Generating synthetic ECG with known PQRST morphology
  2. R-peak detection as the entry point for morphology analysis
  3. PQRST waveform delineation (P-wave, QRS complex, T-wave)
  4. Computing clinical ECG intervals
  5. QT interval analysis and Bazett correction

Setup

library(PhysioECG)
library(PhysioCore)

Simulating ECG with PQRST Morphology

The make_ecg_pqrst() generator creates synthetic ECG signals with physiologically realistic P, Q, R, S, and T wave components. It also returns a data.frame of known fiducial points for validation.

result <- make_ecg_pqrst(
  n_time = 10000,   # 20 seconds at 500 Hz
  n_channels = 1,
  sr = 500,
  heart_rate = 72,
  noise_sd = 0.02
)

pe <- result$pe
fiducials <- result$fiducials
head(fiducials)

The fiducials data.frame includes columns for each known wave location (r_peak, p_peak, q_point, s_point, t_peak) and QRS boundaries (qrs_onset, qrs_offset), all in sample indices.

R-Peak Detection

Morphology analysis begins with R-peak detection. The Pan-Tompkins algorithm reliably identifies R-peaks even in the presence of moderate noise.

peaks <- ecgDetectRpeaks(pe)
head(peaks)

Compare detected peaks against known fiducial points:

# Check detection accuracy
n_detected <- nrow(peaks)
n_expected <- nrow(fiducials)
cat(sprintf("Detected %d peaks, expected %d\n", n_detected, n_expected))

PQRST Waveform Delineation

The ecgDelineate() function identifies the full PQRST complex for each detected R-peak:

  • QRS onset and QRS offset (J-point): detected using gradient-based search from the R-peak
  • P-wave peak: local maximum in the 300–80 ms window before the R-peak
  • T-wave peak: local maximum in the 80–500 ms window after the R-peak
  • T-wave end: estimated using the tangent-intercept method from the T-wave downslope
delin <- ecgDelineate(pe, peaks)
head(delin)

The result contains one row per beat with columns: channel, beat, r_peak, qrs_onset, qrs_offset, qrs_duration_ms, p_peak, t_peak, and t_end.

Understanding Delineation Output

# QRS duration distribution
summary(delin$qrs_duration_ms)

# P-wave detection rate
p_detected <- sum(!is.na(delin$p_peak))
cat(sprintf("P-wave detected in %d/%d beats (%.0f%%)\n",
            p_detected, nrow(delin), 100 * p_detected / nrow(delin)))

# T-wave detection rate
t_detected <- sum(!is.na(delin$t_peak))
cat(sprintf("T-wave detected in %d/%d beats (%.0f%%)\n",
            t_detected, nrow(delin), 100 * t_detected / nrow(delin)))

Validating Against Known Fiducials

When using simulated data with known fiducial points, you can assess delineation accuracy:

# Match detected beats to known fiducials by closest R-peak
matched <- merge(
  delin,
  fiducials,
  by.x = "beat",
  by.y = "beat",
  suffixes = c("_detected", "_true")
)

# QRS onset accuracy (in samples)
onset_error <- matched$qrs_onset - matched$qrs_onset
cat(sprintf("QRS onset error: mean = %.1f samples, SD = %.1f samples\n",
            mean(onset_error, na.rm = TRUE),
            sd(onset_error, na.rm = TRUE)))

Computing Clinical ECG Intervals

The ecgIntervals() function calculates standard clinical ECG intervals from the delineation output:

  • PR interval: P-wave peak to QRS onset (ms)
  • QT interval: QRS onset to T-wave end (ms)
  • QTc: Corrected QT using Bazett’s formula (QT / sqrt(RR in seconds))
  • QRS duration: QRS onset to QRS offset (ms)
  • RR interval: R-peak to next R-peak (ms)
intervals <- ecgIntervals(delin, sr = samplingRate(pe))
head(intervals)

QRS Duration Analysis

Normal QRS duration is 80–120 ms. Prolonged QRS may indicate bundle branch block or ventricular conduction abnormalities.

# Summary statistics
summary(intervals$qrs_ms)

# Identify beats with prolonged QRS (> 120 ms)
prolonged <- intervals[intervals$qrs_ms > 120, ]
cat(sprintf("Beats with prolonged QRS: %d/%d\n",
            nrow(prolonged), nrow(intervals)))

PR Interval Analysis

Normal PR interval is 120–200 ms. Shortened PR may indicate pre-excitation (WPW syndrome); prolonged PR suggests first-degree AV block.

# Filter beats with detected P-waves
pr_valid <- intervals[!is.na(intervals$pr_ms), ]
summary(pr_valid$pr_ms)

# Check for PR prolongation
pr_prolonged <- pr_valid[pr_valid$pr_ms > 200, ]
cat(sprintf("Beats with prolonged PR: %d/%d\n",
            nrow(pr_prolonged), nrow(pr_valid)))

QT Interval and Bazett Correction

The QT interval represents ventricular depolarization and repolarization. Because QT varies with heart rate, the corrected QT (QTc) using Bazett’s formula is preferred for clinical assessment. Normal QTc is below 440 ms for males and 460 ms for females.

# Filter beats with valid QT measurements
qt_valid <- intervals[!is.na(intervals$qt_ms) & !is.na(intervals$qtc_ms), ]
summary(qt_valid$qt_ms)
summary(qt_valid$qtc_ms)

# Check for QTc prolongation (> 460 ms)
qtc_prolonged <- qt_valid[qt_valid$qtc_ms > 460, ]
cat(sprintf("Beats with prolonged QTc: %d/%d\n",
            nrow(qtc_prolonged), nrow(qt_valid)))

Beat-to-Beat Variability

Examine how intervals change across beats:

# QT variability index
if (nrow(qt_valid) > 2) {
  qt_sd <- sd(qt_valid$qt_ms, na.rm = TRUE)
  rr_sd <- sd(qt_valid$rr_ms, na.rm = TRUE)
  cat(sprintf("QT SD: %.1f ms, RR SD: %.1f ms\n", qt_sd, rr_sd))
}

Complete Morphology Analysis Pipeline

Here is a full pipeline from raw ECG to morphology analysis:

# 1. Simulate or load ECG data
result <- make_ecg_pqrst(
  n_time = 60000,  # 2 minutes at 500 Hz
  sr = 500,
  heart_rate = 70,
  noise_sd = 0.02
)
pe <- result$pe

# 2. Optional: correct baseline wander
pe <- ecgBaselineCorrect(pe, method = "highpass", cutoff = 0.5,
                         output_assay = "clean")

# 3. Detect R-peaks
peaks <- ecgDetectRpeaks(pe, assay_name = "clean")

# 4. Delineate PQRST waveforms
delin <- ecgDelineate(pe, peaks, assay_name = "clean")

# 5. Compute clinical intervals
intervals <- ecgIntervals(delin, sr = samplingRate(pe))

# 6. Summary report
cat("=== ECG Morphology Summary ===\n")
cat(sprintf("Total beats analyzed: %d\n", nrow(intervals)))
cat(sprintf("Mean QRS duration: %.1f ms (SD: %.1f)\n",
            mean(intervals$qrs_ms, na.rm = TRUE),
            sd(intervals$qrs_ms, na.rm = TRUE)))
cat(sprintf("Mean PR interval: %.1f ms (SD: %.1f)\n",
            mean(intervals$pr_ms, na.rm = TRUE),
            sd(intervals$pr_ms, na.rm = TRUE)))
cat(sprintf("Mean QTc: %.1f ms (SD: %.1f)\n",
            mean(intervals$qtc_ms, na.rm = TRUE),
            sd(intervals$qtc_ms, na.rm = TRUE)))
cat(sprintf("Mean RR interval: %.1f ms (SD: %.1f)\n",
            mean(intervals$rr_ms, na.rm = TRUE),
            sd(intervals$rr_ms, na.rm = TRUE)))

Handling Multi-Channel ECG

Both delineation and interval functions handle multi-channel ECG data. Results are computed independently per channel.

# Create multi-channel ECG
result_mc <- make_ecg_pqrst(n_time = 10000, n_channels = 3, sr = 500)
pe_mc <- result_mc$pe

peaks_mc <- ecgDetectRpeaks(pe_mc)
delin_mc <- ecgDelineate(pe_mc, peaks_mc)
intervals_mc <- ecgIntervals(delin_mc, sr = samplingRate(pe_mc))

# Per-channel summary
for (ch in unique(intervals_mc$channel)) {
  ch_data <- intervals_mc[intervals_mc$channel == ch, ]
  cat(sprintf("Channel %d: %d beats, mean QTc = %.1f ms\n",
              ch, nrow(ch_data),
              mean(ch_data$qtc_ms, na.rm = TRUE)))
}

References

  • Goldberger, A.L., et al. (2000). “PhysioBank, PhysioToolkit, and PhysioNet: Components of a new research resource for complex physiologic signals.” Circulation, 101(23), e215–e220.
  • Pan, J. & Tompkins, W.J. (1985). “A real-time QRS detection algorithm.” IEEE Transactions on Biomedical Engineering, 32(3), 230–236.
  • Clifford, G.D., Azuaje, F. & McSharry, P.E. (2006). Advanced Methods and Tools for ECG Data Analysis. Artech House.
  • Bazett, H.C. (1920). “An analysis of the time-relations of electrocardiograms.” Heart, 7, 353–370.

Session Info

sessionInfo()