mrpheus provides a full PSG preprocessing pipeline
through preprocess_psg(). This vignette walks through the
standard workflow for a Sleep-EDF recording, then shows how to use the
individual filter functions independently when you need finer
control.
Note — All code blocks use
eval = FALSEbecause they require a local EDF file. For a fully-executed walkthrough see the companion article: Preprocessing: Live Walkthrough
Data
The examples use SC4001E0-PSG.edf from the Sleep-EDF Cassette study (Kemp et al., 2000), a 22-hour ambulatory PSG of a healthy young adult. Freely available from PhysioNet under CC BY 1.0 (free account required).
# Download via MNE Python (handles authentication automatically)
import mne
files = mne.datasets.sleep_physionet.age.fetch_data(subjects=[0], recording=[1])
print(files[0]) # path to SC4001E0-PSG.edfStandard pipeline
The typical workflow is four steps:
library(mrpheus)
# 1. Read EDF
rec <- read_edf("SC4001E0-PSG.edf")
# 2. Epoch and classify channels
psg <- prepare_psg(rec)
psg$channel_mapThe Sleep-EDF file uses linked-ear channel names
(EEG Fpz-Cz, EEG Pz-Oz). These are already
matched by prepare_psg()’s default EEG pattern, but if your
recording uses notation like C3-A2 you can pass a rename
map at the preprocessing step:
# 3. Preprocess: rename, remove DC, notch, bandpass
psg_clean <- preprocess_psg(
psg,
channel_rename = c(
"C3-A2" = "C3", "C4-A1" = "C4",
"O1-A2" = "O1", "O2-A1" = "O2"
)
)preprocess_psg() does the following in order, on the
full continuous signal before re-epoching (this avoids
filter discontinuities at epoch boundaries):
| Step | Default behaviour |
|---|---|
| Channel renaming | Optional; updates channel_map and all internal
references |
| DC removal | Subtracts per-channel mean |
| Powerline detection | Compares PSD power at 50 and 60 Hz; picks the dominant one |
| Notch filter | 2nd-order Butterworth bandstop at powerline frequency + harmonics |
| Bandpass filter | EEG 0.3–35 Hz · EOG 0.3–15 Hz · EMG 10–99 Hz · ECG 0.5–40 Hz |
# 4. Automatic AASM sleep staging on the cleaned PSG
stages <- stage_epochs(psg_clean)
head(stages)Customising the pipeline
All parameters have sensible defaults but can be overridden:
psg_clean <- preprocess_psg(
psg,
powerline_freq = 50L, # skip auto-detection
notch_harmonics = TRUE, # also notch 100, 150 Hz, …
notch_bw_hz = 3, # wider notch bandwidth
eeg_bandpass = c(0.5, 40), # broader EEG range
eog_bandpass = c(0.3, 15),
emg_bandpass = c(10, 99),
ecg_bandpass = c(0.5, 40),
dc = TRUE,
verbose = TRUE
)To skip channel-type-specific filtering entirely, set a bandpass to the full range for that type:
# Keep raw EMG (no bandpass)
psg_clean <- preprocess_psg(psg, emg_bandpass = c(0, Inf))Using the signal-level functions independently
Each step in preprocess_psg() delegates to an exported
function that works on a plain numeric vector. These are useful
when:
- You want to filter a single channel without building a full PSG object.
- You are building your own pipeline outside the
mrpheus_psgstructure. - You need to apply filtering to signals loaded from another source
(e.g. CSV, MATLAB
.mat, or a custom reader).
DC removal
eeg <- rec$signals[["EEG Fpz-Cz"]]$signal
eeg <- remove_dc(eeg)Powerline detection
sr <- rec$channels$sample_rate[rec$channels$label == "EEG Fpz-Cz"]
freq <- detect_powerline(eeg, sr)
#> ℹ Detected powerline frequency: 50 HzNotch filtering
# Remove 50 Hz and harmonics (100, 150 Hz, …)
eeg_notched <- notch_filter(eeg, sr = sr, freq = 50)
# Remove 60 Hz only, no harmonics
eeg_notched <- notch_filter(eeg, sr = sr, freq = 60, harmonics = FALSE)
# Wider notch bandwidth (default is 2 Hz)
eeg_notched <- notch_filter(eeg, sr = sr, freq = 50, bandwidth_hz = 4)Bandpass filtering
# Standard EEG passband
eeg_bp <- bandpass_filter(eeg, sr = sr, low_hz = 0.3, high_hz = 35)
# High-pass only (baseline drift removal) — set high_hz beyond Nyquist
eeg_hp <- bandpass_filter(eeg, sr = sr, low_hz = 0.1, high_hz = Inf)
# Low-pass only — set low_hz to 0
eeg_lp <- bandpass_filter(eeg, sr = sr, low_hz = 0, high_hz = 35)A note on filter design
The notch and bandpass filters both use zero-phase IIR
(Butterworth) designs applied via
gsignal::filtfilt():
- Notch: 2nd-order Butterworth bandstop (≈9 coefficients). Safe on long recordings — no memory issues.
- Bandpass: 4th-order Butterworth. The
high_hzcutoff is automatically clamped to 99 % of the Nyquist frequency to prevent instability.
Zero-phase (filtfilt) filtering introduces no phase
distortion but doubles the effective filter order. If you prefer a
causal (single-pass) filter for any reason, use
gsignal::filter() directly with the same Butterworth
coefficients.
The staging pipeline (stage_epochs()) applies its own
separate MNE-matched FIR bandpass internally —
preprocess_psg() filtering and staging filtering are
independent.
EOG artefact correction
Ocular artefacts (blinks, saccades, slow drift) contaminate EEG
channels through volume conduction from the eyes. mrpheus provides two
correction methods, both operating on the continuous signal and
returning a new mrpheus_psg with cleaned epochs.
Regression-based correction
correct_eog_regression() regresses each EEG channel on
the EOG reference channels and replaces the EEG with the residuals. This
removes both transient blinks and slow ocular drift without requiring
blink event detection.
psg_eog <- correct_eog_regression(psg_clean)This is the recommended method when:
- You have one or two EOG channels and want a fast, deterministic result.
- The recording has relatively few EEG channels (fewer than 4).
- Slow drift contamination is a concern alongside blinks.
You can restrict which channels are used:
psg_eog <- correct_eog_regression(
psg_clean,
eog_channels = c("EOG LOC", "EOG ROC"),
eeg_channels = c("EEG C3", "EEG C4", "EEG O1", "EEG O2")
)ICA-based correction
correct_eog_ica() decomposes the EEG into independent
components via FastICA, identifies those whose time courses correlate
with the EOG channels above a threshold, and subtracts their
contribution.
psg_eog <- correct_eog_ica(psg_clean)Key parameters:
psg_eog <- correct_eog_ica(
psg_clean,
n_components = 6L, # default: min(6, n_eeg_channels)
threshold = 0.35 # default: Pearson |r| above which a component is flagged
)ICA requires at least 2 EEG channels. For recordings with fewer than
3–4 EEG channels, regression is more reliable. The
threshold controls sensitivity: lower values flag more
components (more aggressive removal, risk of removing genuine signal);
higher values are more conservative.
Which method to choose?
| Regression | ICA | |
|---|---|---|
| Minimum EEG channels | 1 | 2 (3+ recommended) |
| Removes slow drift | Yes | Partial |
| Removes blink transients | Yes | Yes |
| Deterministic | Yes | No (seed-dependent) |
| Computation | Fast | Slower |
For a standard overnight PSG with 4+ EEG channels, ICA generally produces cleaner results. For a 2-channel ambulatory recording, use regression.
Full pipeline
Putting it all together:
rec <- read_edf("SC4001E0-PSG.edf")
psg <- prepare_psg(rec)
psg <- preprocess_psg(psg)
psg <- correct_eog_regression(psg) # or correct_eog_ica()
stages <- stage_epochs(psg)