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
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.edfThe 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 |
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:
Each step in preprocess_psg() delegates to an exported
function that works on a plain numeric vector. These are useful
when:
mrpheus_psg structure..mat, or a custom reader).# 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)# 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)The notch and bandpass filters both use zero-phase IIR
(Butterworth) designs applied via
gsignal::filtfilt():
high_hz cutoff 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.
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.
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.
This is the recommended method when:
You can restrict which channels are used:
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.
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.
| 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.
Kemp B, Zwinderman A, Tuk B, Kamphuisen H, Oberyé J (2000). Analysis of a sleep-dependent neuronal feedback loop: the slow-wave microcontinuity of the EEG. IEEE Transactions on Biomedical Engineering, 47(9), 1185–1194.