Skip to content

Design matrices & GLM

A DesignMatrix is one row per timepoint, one column per regressor, plus the metadata a GLM needs: the sampling frequency, which columns are already HRF-convolved, and which are nuisance. Build one from a BIDS events .tsv, a confounds table, a numpy array, or a dict. An events file is detected by its onset and duration columns: each trial_type becomes a boxcar named <type>_c0 and is convolved with the Glover HRF for you. Pass hrf_model=None to keep raw boxcars and call convolve yourself.

Nuisance regressors go in through append(..., axis=1, as_confounds=True), which marks them so convolve skips them and vif ignores them. Add low-frequency drift with add_poly (polynomials) or add_dct_basis (cosines). Pick one, not both.

Goal Use Notes
Events → convolved regressors DesignMatrix(events_tsv, run_length=, TR=) hrf_model='glover' by default; None for boxcars
Convolve later convolve kernel='glover' (or another nilearn HRF model name) or your own kernel array; skips confounds
Add nuisance columns append(..., axis=1, as_confounds=True) axis=0 stacks runs and keeps confounds separate per run
Drift add_poly / add_dct_basis Generated names carry the reserved .nl_ prefix
Check the design vif, corr, plot vif excludes confounds by default
Drop redundant columns clean Removes columns correlated above thresh (default 0.95)
Fit a first-level model BrainData.fit(model='glm', X=design) model='ridge' for the penalized fit; see Prediction
Contrasts compute_contrasts("A - B", inference=) Effect map by default; inference=True returns a ContrastResult
Group test concatenatettest Returns {'mean', 't', 'z', 'p'} of BrainData
Correct and threshold fdr, holm_bonf, threshold See Statistics & inference

First level

from nltools.data import BrainData, DesignMatrix

bold = BrainData("sub-01_bold.nii.gz")
events = DesignMatrix("sub-01_events.tsv", run_length=bold.shape[0], TR=2.0)
confounds = DesignMatrix("sub-01_confounds.tsv", run_length="infer", TR=2.0)

design = events.append(confounds, axis=1, as_confounds=True).add_poly(2)
design.vif()                 # one VIF per non-confound regressor

bold.fit(X=design)
effect = bold.compute_contrasts("face_c0 - house_c0")
con = bold.compute_contrasts("face_c0 - house_c0", inference=True)

fit keeps the fitted model on the object, and the model remembers its column names, so compute_contrasts can name columns directly. A contrast is a string of column names with optional coefficients ("2*A - B - C"), a numeric weight vector, or a {name: contrast} dict to evaluate several at once. The default returns the effect map — the input a group analysis consumes. inference=True returns a ContrastResult carrying effect, variance, standard_error, statistic, z_score, p_value, and degrees_of_freedom together, so you can threshold con.statistic now and reuse con.effect at the group level. Contrast p-values are one-sided, following the nilearn/SPM convention that a contrast tests "A > B"; flip the contrast for the other direction. This is the one place in nltools where the default is not two-tailed.

For a multi-regressor group model, stack the per-subject effect maps and fit a second-level DesignMatrix with one row per subject, then contrast its coefficients:

group = concatenate(effects)                                   # (n_subjects, n_voxels)
second_level = DesignMatrix({"intercept": np.ones(len(group)), "age": ages})
group.fit(model="glm", X=second_level)
result = group.compute_contrasts("age", inference=True)

This estimates variance across effect maps; it does not propagate first-level effect variance. ttest() remains the concise intercept-only version of the same test.

Group level

from nltools import concatenate
from nltools.algorithms import fdr, threshold

group = concatenate(betas)                                # (n_subjects, n_voxels)
result = group.ttest()                                    # mean, t, z, p
group_z = threshold(result["z"], result["p"], thr=0.001)  # uncorrected
fdr_thr = fdr(result["p"].data, q=0.05)                   # -1 if nothing survives

Design warnings

One warning describes your design, not a bug in nltools. Read it instead of filtering it.

  • RankDeficientDesignWarning. A column is an exact linear combination of others (a duplicated regressor, an intercept added twice, a condition that never occurs in this run). The fit uses a pseudo-inverse, so contrasts involving that column are not interpretable. Fix the design; clean() handles the common case.

It subclasses UserWarning, so you can silence it individually. Do that only once you know which regressors triggered it.

Next: Statistics & inference, or the GLM workflow tutorial for the whole pipeline end to end.