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:
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.
Morphology analysis begins with R-peak detection. The Pan-Tompkins algorithm reliably identifies R-peaks even in the presence of moderate noise.
Compare detected peaks against known fiducial points:
The ecgDelineate() function identifies the full PQRST
complex for each detected R-peak:
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.
# 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)))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)))The ecgIntervals() function calculates standard clinical
ECG intervals from the delineation output:
Normal QRS duration is 80–120 ms. Prolonged QRS may indicate bundle branch block or ventricular conduction abnormalities.
Normal PR interval is 120–200 ms. Shortened PR may indicate pre-excitation (WPW syndrome); prolonged PR suggests first-degree AV block.
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)))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)))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)))
}