PhysioExperiment is a Bioconductor-compatible R
package for analyzing multi-modal physiological signal data. It provides
a unified data model built on top of SummarizedExperiment
that can store EEG, EMG, ECG, IMU, MoCap, and other time-series signals
alongside rich channel and event metadata.
Key features include:
PhysioExperiment class wraps signal data, channel metadata,
event markers, and sampling rate into a single object.Install PhysioExperiment from GitHub:
# Install from GitHub
if (!requireNamespace("remotes", quietly = TRUE))
install.packages("remotes")
remotes::install_github("matsui-lab/PhysioExperiment")Since PhysioExperiment depends on Bioconductor packages
(SummarizedExperiment, S4Vectors,
HDF5Array), you may also need to install the Bioconductor
infrastructure:
The constructor takes an assay matrix (rows = time points, columns = channels), channel metadata, and a sampling rate:
library(PhysioExperiment)
# Simulate 4 seconds of 4-channel EEG at 250 Hz
set.seed(42)
n_time <- 1000
n_channels <- 4
sr <- 250
eeg_data <- matrix(rnorm(n_time * n_channels), nrow = n_time, ncol = n_channels)
colnames(eeg_data) <- c("Fz", "Cz", "Pz", "Oz")
pe <- PhysioExperiment(
assays = list(raw = eeg_data),
colData = S4Vectors::DataFrame(
label = c("Fz", "Cz", "Pz", "Oz"),
type = rep("EEG", 4),
unit = rep("uV", 4)
),
samplingRate = sr
)
peYou can also supply rowData (per-time-point metadata)
and a metadata list for experiment-level information such
as reference electrode or recording date.
PhysioExperiment ships with a small sample EEG dataset. Load it with
readCSV():
# Locate the bundled sample file
csv_path <- system.file("extdata", "sample_eeg.csv", package = "PhysioExperiment")
# Read the wide-format CSV (time column + channel columns)
pe <- readCSV(csv_path, time_col = "time", sampling_rate = 250)
peOther I/O functions include readEDF(),
readBrainVision(), readGDF(),
readHDF5(), readBIDS(), and
readMATLAB().
Events (triggers, stimuli, responses) are stored as
PhysioEvents objects:
Printing a PhysioExperiment gives a concise
overview:
Signal data is stored as assays. Each processing step typically adds a new assay so you can always go back to the original data:
PhysioExperiment supports [i, j] subsetting where
i selects time points and j selects
channels:
# First 500 time points, channels 1-2
pe_sub <- pe[1:500, 1:2]
dim(pe_sub)
# Extract a time window in seconds
pe_window <- extractWindow(pe, tmin = 0.5, tmax = 1.5)
dim(pe_window)
# Pick channels by name
pe_frontal <- pickChannels(pe, c("Fz", "Cz"))
channelNames(pe_frontal)
# Drop channels
pe_no_oz <- dropChannels(pe, "Oz")
channelNames(pe_no_oz)butterworthFilter() applies a zero-phase IIR filter (via
signal::filtfilt) and stores the result in a new assay:
# Bandpass filter: keep 1-40 Hz (common for EEG)
pe <- butterworthFilter(pe, low = 1, high = 40, type = "pass")
# Lowpass filter at 30 Hz
pe <- butterworthFilter(pe, high = 30, type = "low", output_assay = "lowpass")
# Highpass filter at 0.5 Hz (remove DC drift)
pe <- butterworthFilter(pe, low = 0.5, type = "high", output_assay = "highpass")
# Check the new assays
SummarizedExperiment::assayNames(pe)Remove power-line interference at 50 Hz (or 60 Hz) and its harmonics:
For linear-phase filtering, use firFilter():
A simple smoothing filter:
Remove linear trends or the mean from each channel:
Re-referencing changes the reference electrode for EEG data. Three modes are supported:
# Average reference (common for high-density EEG)
pe_avg <- rereference(pe, ref_type = "average")
isAverageReferenced(pe_avg) # TRUE
# Single-channel reference (e.g., Cz)
pe_cz <- rereference(pe, ref_type = "channel", ref_channels = "Cz")
getCurrentReference(pe_cz) # "Cz"
# Linked-mastoids reference (average of two channels)
# pe_linked <- rereference(pe, ref_type = "channels",
# ref_channels = c("M1", "M2"))Epoching segments continuous data into time-locked trials around events.
# Ensure events are present
pe <- setEvents(pe, PhysioEvents(
onset = c(0.5, 1.0, 1.5),
type = "stimulus",
value = c("target", "distractor", "target")
))
# Epoch: 200 ms before to 800 ms after each stimulus
pe_epochs <- epochData(pe, tmin = -0.2, tmax = 0.8, event_type = "stimulus")
pe_epochs
# The epoched data is now 4D: time x channel x epoch x sample
dim(SummarizedExperiment::assay(pe_epochs, "epoched"))Compute the event-related potential (ERP) by averaging across epochs:
# Compute spectrogram for channel 1
spec <- spectrogram(pe, channel = 1, window_size = 128, overlap = 0.75)
# The result is a list with power, frequencies, and times
names(spec)
dim(spec$power) # frequency x time
range(spec$frequencies)
# Plot the spectrogram
plotSpectrogram(spec, freq_range = c(1, 50))Morlet wavelet decomposition gives better frequency resolution at low frequencies and better time resolution at high frequencies:
Extract power in standard EEG frequency bands:
# Default bands: delta, theta, alpha, beta, gamma
bp <- bandPower(pe)
bp
# Relative band power (proportions summing to 1)
bp_rel <- bandPower(pe, relative = TRUE)
bp_rel
# Custom frequency bands
custom_bands <- list(
low_alpha = c(8, 10),
high_alpha = c(10, 13),
low_beta = c(13, 20),
high_beta = c(20, 30)
)
bp_custom <- bandPower(pe, bands = custom_bands)
bp_customExtract instantaneous amplitude (envelope) and phase:
PhysioExperiment provides several ggplot2-based plotting
functions.
Scalp topography requires electrode positions. Use
applyMontage() to assign standard 10-20 positions:
# Assign electrode positions from the 10-20 system
pe <- applyMontage(pe, "10-20")
# Plot topographic map at a specific time point
plotTopomap(pe, time = 0.5)
# Plot with custom values (e.g., band power or t-statistics)
plotTopomap(pe, values = c(1.2, 0.5, -0.8, -1.5))
# Series of topomaps across time
plots <- plotTopomapSeries(pe, times = c(0.1, 0.2, 0.3, 0.4, 0.5))Test whether the ERP is significantly different from zero (one-sample) or between two conditions (two-sample) at every time point and channel:
# One-sample t-test (all epochs against zero)
res_t <- tTestEpochs(pe_epochs)
names(res_t)
dim(res_t$t_values) # time x channel
dim(res_t$p_values) # time x channel
# Two-sample t-test between condition groups
res_t2 <- tTestEpochs(pe_epochs, condition1 = 1:5, condition2 = 6:10)
# Paired t-test
res_tp <- tTestEpochs(pe_epochs, condition1 = 1:5, condition2 = 6:10,
paired = TRUE)One-way ANOVA across three or more conditions:
Cluster permutation testing controls for multiple comparisons by identifying contiguous clusters of significant effects and computing cluster-level p-values through permutation:
# Compare two conditions with 1000 permutations
res_clust <- clusterPermutationTest(
pe_epochs,
condition1 = 1:5,
condition2 = 6:10,
n_permutations = 1000,
cluster_threshold = 0.05,
seed = 123
)
# Significant clusters
res_clust$cluster_p
# Logical mask of significant time-channel locations
sum(res_clust$cluster_mask)Apply correction to any p-value matrix:
Given a vector of p-values over time, identify contiguous intervals that reach significance:
PhysioExperiment uses DuckDB for efficient storage and querying of large datasets.
PhysioExperiment includes a browser-based GUI built with React, providing interactive signal exploration without writing R code.
# Launch the GUI in your default browser
launchGUI()
# Start on a custom port
launchGUI(port = 3000)
# Start the API server in the background (non-blocking)
server <- startAPIServer()
# ... do other work ...
server$kill()
# Check GUI dependencies
checkGUIDependencies()The GUI supports data import, interactive visualization, preprocessing pipelines, time-frequency analysis, statistical testing, and a visual workflow builder.