If you work with fMRI data, you already know the core analysis is multiple regression applied voxel-by-voxel — but specifying the design matrix, convolving events with hemodynamic response functions, and managing multi-run structures can be tedious and error-prone.
The fmrireg package gives you a concise, formula-based
interface for these tasks, inspired by lm() and
glm(). You describe your experimental design and HRF
choices in a formula, and fmrireg handles convolution, baseline
modeling, contrast computation, and model fitting.
This vignette starts with a complete, small fit and then unpacks the objects behind it. You begin with a time-by-voxel matrix and an event table; you finish with one coefficient estimate per condition and voxel.
For a compact example, we create two voxels with different known
responses to faces and scenes. In your own analysis, bold
would be the matrix extracted from preprocessed images or regions of
interest.
quick_events <- data.frame(
onset = seq(10, 80, by = 10),
condition = factor(rep(c("face", "scene"), 4)),
run = 1L
)
quick_frame <- sampling_frame(blocklens = 80, TR = 2)
quick_event_model <- event_model(
onset ~ hrf(condition), block = ~ run,
data = quick_events, sampling_frame = quick_frame
)
quick_design <- as.matrix(design_matrix(quick_event_model))
quick_truth <- matrix(
c(1.0, 0.6, 0.7, 1.1), nrow = ncol(quick_design),
dimnames = list(colnames(quick_design), c("voxel1", "voxel2"))
)
# Set the noise level relative to the regressors' peak height, so the
# signal-to-noise ratio does not depend on the HRF's raw amplitude.
noise_sd <- 0.11 * max(abs(quick_design))
set.seed(2026)
bold <- quick_design %*% quick_truth +
matrix(rnorm(80 * 2, sd = noise_sd), 80, 2)
quick_dataset <- matrix_frame(
bold, TR = 2, run_length = 80, event_table = quick_events
)face_vs_scene <- pair_contrast(
~ condition == "face", ~ condition == "scene",
name = "face_vs_scene"
)
fit <- fmri_lm(
onset ~ hrf(condition, contrasts = face_vs_scene), block = ~ run,
dataset = quick_dataset
)
coefficient_table <- data.frame(
condition = levels(quick_events$condition),
truth_voxel1 = quick_truth[, 1],
estimate_voxel1 = as.numeric(coef(fit)[1, ]),
truth_voxel2 = quick_truth[, 2],
estimate_voxel2 = as.numeric(coef(fit)[2, ])
)
knitr::kable(coefficient_table, digits = 2,
row.names = FALSE,
caption = "Known and recovered condition effects")| condition | truth_voxel1 | estimate_voxel1 | truth_voxel2 | estimate_voxel2 |
|---|---|---|---|---|
| face | 1.0 | 0.95 | 0.7 | 0.76 |
| scene | 0.6 | 0.51 | 1.1 | 1.14 |
quick_contrast_numeric <- data.frame(
voxel = colnames(quick_truth),
estimate = as.numeric(coef(fit, type = "contrasts")$face_vs_scene),
std_error = as.numeric(standard_error(fit, type = "contrasts")$face_vs_scene),
statistic = as.numeric(stats(fit, type = "contrasts")$face_vs_scene),
p_value = as.numeric(p_values(fit, type = "contrasts")$face_vs_scene)
)
quick_contrast <- transform(
quick_contrast_numeric,
p_value = format.pval(p_value, digits = 3, eps = 1e-4)
)
knitr::kable(quick_contrast, digits = 3,
row.names = FALSE,
caption = "Face-minus-scene contrast with uncertainty")| voxel | estimate | std_error | statistic | p_value |
|---|---|---|---|---|
| voxel1 | 0.440 | 0.047 | 9.432 | <1e-04 |
| voxel2 | -0.383 | 0.055 | -6.983 | <1e-04 |
The first table separates the known effect sizes from their estimates. The second table asks a scientific question—face minus scene—and reports its estimate, standard error, test statistic, and p-value for each voxel. Here the first voxel favors faces while the second favors scenes, recovering the pattern used to generate the data. The next sections unpack the dataset, event, baseline, and contrast objects used for larger analyses.
A typical fMRI analysis workflow with fmrireg involves
these main stages:
event_model).baseline_model).contrast).fmri_lm).Before modeling, you need to represent your fMRI data and its
structure. fmrireg uses objects like:
fmri_frame (from the fmridataset package):
an observations-by-features container that carries the data (in memory
or file-backed), its feature space (voxel grid, parcels, or latent
components), the TR and run structure as observation metadata, and the
experimental event table. Build one with matrix_frame(),
neurovec_frame(), nifti_frame(), or
latent_frame().sampling_frame: Defines the temporal structure – the
number of scans per run (blocklens) and the repetition time
(TR).The HRF models the BOLD signal response to a brief neural event.
fmrireg provides various pre-defined HRFs (e.g.,
HRF_SPMG1, HRF_GAUSSIAN) and functions to
create or modify them (gen_hrf, hrf_blocked,
hrf_lagged).
# Plot the SPM canonical HRF
time <- seq(0, 24, by = 0.2)
plot(time, HRF_SPMG1(time), type = 'l', xlab = "Time (s)", ylab = "BOLD Response",
main = "SPM Canonical HRF (HRF_SPMG1)")event_model)This defines the task-related part of your GLM. You specify how
experimental events (onsets, conditions, durations) map to regressors
using a formula syntax. The hrf() or
afni_hrf() functions within the formula link event
variables to specific HRF shapes.
# A simple design table with two stimulus types
design_table <- data.frame(
onset = c(10, 40, 70, 100, 130, 160, 190, 220),
stim_type = factor(rep(c("face", "scene"), 4)),
run_id = rep(1, 8)
)# Model the effect of stim_type, convolved with the SPM canonical HRF
event_mod <- event_model(onset ~ hrf(stim_type),
data = design_table,
block = ~ run_id,
sampling_frame = sframe)
event_modbaseline_model)This models non-task-related components like scanner drift, run intercepts, and other nuisance factors (e.g., motion parameters).
# Example: Cubic B-spline drift model + run intercepts
# (Requires 'sframe' defined earlier)
base_mod <- baseline_model(basis = "bs", degree = 3, sframe = sframe)
print(base_mod)
#> ================================================
#> Baseline Model
#> ================================================
#> Drift Components
#> * Name: baseline_bs_3
#> * Basis type: bs
#> * Degree: 3
#> * Drift columns: 3
#>
#> Additional Components
#> * Constant columns: 1
#> * Nuisance columns: 0
#>
#> Model Summary
#> * Total columns: 4
#>
#> Design Matrix Preview
#> 0.000 0.000 0.000 1.000
#> 0.025 0.000 0.000 1.000
#> 0.049 0.001 0.000 1.000
#> ================================================pair_contrast,
unit_contrast, etc.)Contrasts define specific comparisons between condition estimates.
# Example: Contrast comparing 'face' vs 'scene' levels of 'stim_type'
face_vs_scene <- pair_contrast(~ stim_type == "face", ~ stim_type == "scene", name = "FvS")
print(face_vs_scene)
#> contrast: FvS
#> A: ~stim_type == "face"
#> B: ~stim_type == "scene"vignette("a_05_contrasts", package = "fmrireg")fmri_model,
fmri_lm)You combine the event and baseline models into a full
fmri_model and then fit it to your fmri_frame
using estimation functions like fmri_lm (for standard GLM)
or estimate_betas (for single-trial betas).
# Create three example voxel series over the same one-run design
set.seed(42)
overview_data <- matrix_frame(
matrix(rnorm(120 * 3), 120, 3),
TR = 2, run_length = 120, event_table = design_table
)
# Combine the design with its dataset, then fit it
full_model <- fmri_model(event_mod, base_mod, dataset = overview_data)
overview_fit <- fmri_lm(full_model)
overview_coef_matrix <- coef(overview_fit) # voxels x conditions
overview_coef_table <- data.frame(
condition = levels(design_table$stim_type),
voxel_1 = overview_coef_matrix[1, ],
voxel_2 = overview_coef_matrix[2, ],
voxel_3 = overview_coef_matrix[3, ],
check.names = FALSE
)
knitr::kable(
overview_coef_table, digits = 2, row.names = FALSE,
caption = "Condition coefficients from the object-building route"
)| condition | voxel_1 | voxel_2 | voxel_3 |
|---|---|---|---|
| face | -0.92 | -1.34 | -0.80 |
| scene | 0.51 | 0.86 | 1.09 |
This second fit uses the object-building route rather than the compact formula route shown first. See the linear-model vignette for AR prewhitening, robust row weighting, uncertainty estimates, and model diagnostics.
vignette("a_09_linear_model", package = "fmrireg")fmrireg also supports:
simulate_fmri_matrix).
vignette("a_08_simulation", package = "fmrireg")regressor).For large problems, you can accelerate voxelwise GLM estimation with: - Temporal sketching (CountSketch, Gaussian or SRHT) to reduce timepoints, with standard errors that account for the sketch. - Nyström/landmark spatial extension to solve on a subset of voxels and extend with parcel‑aware kernels.
See vignette("sketched-ar", package = "fmrireg") for
guidance, tradeoffs, and practical defaults.
Combine subject‑level maps or ROI summaries using
fixed/random‑effects meta‑regression: - Inputs: HDF5/NIfTI (beta+SE or
t+df) or ROI CSV. - Random‑effects: Paule–Mandel/DerSimonian–Laird/REML
alias; optional robust Huber. - t‑only combine:
Stouffer/Fisher/Lancaster (supports equal/IVW/custom weights where
applicable). - Exact contrasts: at fit‑time (contrasts=…) or post‑hoc
via return_cov = "tri" + contrast(). -
Multiple comparisons: BH FDR or spatial FDR (block/parcel aware).
See vignette("group_analysis", package = "fmrireg") for
end‑to‑end examples.
This overview provides a map of the fmrireg package. For
detailed explanations and examples, please refer to the specific
vignettes linked above.
vignette("a_10_dataset", package = "fmrireg") — Dataset
managementvignette("a_08_simulation", package = "fmrireg") —
Build controlled examplesvignette("a_09_linear_model", package = "fmrireg") —
Fit and diagnose a GLMvignette("a_05_contrasts", package = "fmrireg") — Test
planned hypotheses