ECG Morphology: PQRST Segmentation and Interval Analysis
Source:vignettes/ecg-morphology.Rmd
ecg-morphology.RmdIntroduction
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:
- Generating synthetic ECG with known PQRST morphology
- R-peak detection as the entry point for morphology analysis
- PQRST waveform delineation (P-wave, QRS complex, T-wave)
- Computing clinical ECG intervals
- QT interval analysis and Bazett correction
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:
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.
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.
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)))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.