Independent Component Analysis (ICA) is a blind source separation technique commonly used to identify and remove artifacts from electrophysiological recordings. Artifacts such as eye blinks, eye movements, muscle activity, and heartbeat contamination can be isolated as independent components and removed without discarding entire channels or epochs.
PhysioPreprocess provides two ICA workflows:
icaDecompose() /
icaRemove()): A self-contained FastICA implementation with
no additional dependencies.runICA() /
removeICAComponents()): A wrapper around the
fastICA package for users who prefer that interface.This vignette walks through both approaches.
We simulate a multi-channel EEG recording containing a mixture of neural sources and artifact sources.
set.seed(123)
n_time <- 2560
n_channels <- 16
sr <- 256
# Simulate 10 seconds of data with injected artifacts
raw_data <- matrix(rnorm(n_time * n_channels), nrow = n_time, ncol = n_channels)
# Inject a simulated eye-blink artifact into frontal channels
blink <- dnorm(seq(-3, 3, length.out = 100), mean = 0, sd = 1) * 50
blink_onset <- 500
raw_data[blink_onset:(blink_onset + 99), 1:2] <-
raw_data[blink_onset:(blink_onset + 99), 1:2] + blink
pe <- PhysioExperiment(
assays = list(raw = raw_data),
colData = S4Vectors::DataFrame(
label = paste0("Ch", seq_len(n_channels)),
type = rep("EEG", n_channels)
),
samplingRate = sr
)
peThe built-in ICA implementation uses the FastICA algorithm with PCA-based whitening. It requires no additional packages.
icaDecompose() separates the multi-channel signal into
statistically independent components. Each component represents a source
signal that is maximally non-Gaussian.
The result is a list containing:
components: The independent component time courses
(time x components).mixing: The mixing matrix mapping components to
channels.unmixing: The unmixing matrix mapping channels to
components.object: The updated PhysioExperiment with
ICA components stored in the "ica_components" assay.Identifying which components represent artifacts is typically done through visual inspection of component time courses and topographies. Components corresponding to eye blinks tend to show large, brief deflections concentrated over frontal electrodes.
# Examine the mixing matrix to see each component's spatial pattern
# Columns of the mixing matrix show the contribution of each component
# to each channel
mixing <- ica_result$mixing
print(round(mixing[, 1:4], 3))
# A component heavily weighted on frontal channels (Ch1, Ch2) with
# characteristic blink morphology is likely an eye-blink artifactOnce artifact components have been identified,
icaRemove() zeros out those components and reconstructs the
signal from the remaining components.
If the fastICA package is installed, you can use the
runICA() and removeICAComponents() wrapper
functions. These provide a simpler interface that delegates computation
to the fastICA package.
The result is a list with:
S: Source matrix (time x components).A: Mixing matrix (channels x components).W: Unmixing matrix (components x channels).Before running ICA, it is often helpful to identify and handle bad channels. Bad channels can degrade ICA decomposition quality because they introduce noise that may spread across multiple components.
# Detect channels with abnormally high or low variance
bad_zscore <- detectBadChannels(pe, method = "zscore", threshold = 3)
# Detect channels with low correlation to neighbors
bad_corr <- detectBadChannels(pe, method = "correlation", threshold = 0.4)
# Detect flatline channels
bad_flat <- detectBadChannels(pe, method = "flatline", threshold = 0.01)
# Combine results
bad_all <- unique(c(bad_zscore, bad_corr, bad_flat))
print(bad_all)A typical preprocessing pipeline for EEG data incorporating ICA artifact removal follows this order:
# Step 1: Highpass filter at 1 Hz
pe_hp <- butterworthFilter(pe, low = 1, type = "high")
# Step 2: Notch filter at 50 Hz
pe_notch <- notchFilter(pe_hp, freq = 50)
# Step 3: Detect and interpolate bad channels
bad <- detectBadChannels(pe_notch, method = "zscore")
if (length(bad) > 0) {
pe_notch <- interpolateBadChannels(pe_notch, bad_channels = bad)
}
# Step 4: Run ICA
ica <- icaDecompose(pe_notch, method = "fastica")
pe_ica <- ica$object
# Step 5: Remove artifact components (after visual inspection)
pe_clean <- icaRemove(pe_ica, components = c(1))
# Step 6: Re-reference to average
pe_ref <- rereference(pe_clean, ref_type = "average")The number of ICA components to extract affects decomposition quality:
A common practice is to use the same number of components as
channels, or to reduce dimensionality first via PCA (which
icaDecompose() does internally during the whitening
step).
This vignette demonstrated two ICA workflows available in PhysioPreprocess:
icaDecompose() and
icaRemove(), requiring no additional packages.runICA() and
removeICAComponents(), wrapping the fastICA
package.Both workflows follow the same general pattern: decompose the signal, identify artifact components, and reconstruct the cleaned signal with those components removed.
For general preprocessing operations (filtering, resampling,
re-referencing, detrending), see
vignette("preprocessing-guide", package = "PhysioPreprocess").