Confound Selection

When you fit a first-level GLM, the design matrix should explain task-driven BOLD changes — and only those. Everything else (head motion, heartbeat, breathing, scanner drift, CSF pulsation) needs to be modelled out as a nuisance regressor, or it will leak into your contrasts and inflate false positives. This page walks through what fMRIPrep gives you, which subset to pick, and how to wire it into nilearn or FSL FEAT.

Note

Confound selection is a modelling decision, not a preprocessing step. Once fMRIPrep finishes (see 2.3 fmriPrep with Docker), the confounds TSV is fixed; what changes is which columns you put into the design matrix.


1. Why confounds matter

The raw BOLD time series mixes neural signal with a long list of artefacts that share its spectral and spatial properties:

SourceTypical frequencyWhy it hurts
Head motionbroadband, spikyShifts voxel position → intensity changes correlated with stimulus blocks if motion is task-locked
Cardiac pulsation~1.0 HzAliases into the BOLD band at typical TRs (2 s)
Respiration~0.3 HzModulates B0 field, causes global signal swings
Low-frequency drift< 0.01 HzScanner heating, slow physiology — looks like a slow task effect
CSF / WM signalmixedNon-neural fluctuations that still correlate across voxels

The pathological case: a participant moves more during one condition than another. Motion creates BOLD-like intensity changes; without nuisance regression those changes load directly onto your contrast and look like a real activation. Parkes et al. (2018) and Power et al. (2014) document exactly this scenario in detail.

Warning

Motion that correlates with the task is the single most dangerous confound in fMRI. No amount of clever statistics rescues a study where patients moved more on hard trials than easy ones. Inspect your framewise_displacement traces per condition before you trust any contrast.


2. What’s in *_desc-confounds_timeseries.tsv

fMRIPrep (≥ 24.x) writes a wide TSV next to every preprocessed BOLD file:

$FMRIPREP_DIR/sub-01/func/sub-01_task-rest_desc-confounds_timeseries.tsv

It contains dozens of columns. Here are the families you actually need to know:

Rigid-body motion (6 + expansions)

  • trans_x, trans_y, trans_z — translations in mm
  • rot_x, rot_y, rot_z — rotations in radians
  • *_derivative1 — temporal derivative (first row is NaN)
  • *_power2 — squared term
  • *_derivative1_power2 — squared derivative

The full 24-parameter Friston expansion = 6 originals + 6 derivatives + 6 squares + 6 squared derivatives.

Summary motion metrics

  • framewise_displacement (FD) — Power’s scalar summary, mm
  • dvars, std_dvars — root-mean-square BOLD change between successive volumes; std_dvars is standardised

CompCor components

  • a_comp_cor_00..NNanatomical CompCor: PCA of voxels inside an eroded WM + CSF mask. Captures physiological noise without dipping into grey matter.
  • t_comp_cor_00..NNtemporal CompCor: PCA of the top-variance voxels (mask-free, more aggressive).

Each component column carries metadata in the sidecar JSON — cumulative variance explained, mask used, etc.

Tissue signals

  • csf — mean signal inside the CSF mask
  • white_matter — mean signal inside the WM mask
  • global_signal — mean signal across the whole brain mask (the GSR regressor)

High-pass basis

  • cosine_00..NN — discrete cosine basis. Including these in your design matrix is equivalent to a high-pass filter at fMRIPrep’s default cutoff (128 s).

Scrubbing / outliers (one-hot)

  • non_steady_state_outlier_XX — flags the first few volumes before T1 equilibrium
  • motion_outlier_XX — one regressor per flagged volume; modelling these effectively censors that volume

Tip

Open the sidecar JSON (*_desc-confounds_timeseries.json) once — every column has a Method, Sources, and (for CompCor) variance-explained entry. It’s the ground truth for what each regressor is.


3. Pre-built denoising strategies

Pick one strategy and stick with it. Mixing components ad-hoc (a few aCompCor here, GSR there, sometimes scrubbing) destroys the comparability of your results across subjects.

StrategyRegressorsCountWhen to useReference
6HMP6 motion params6Quick exploration; very weak — task fMRI with low motion onlyFriston 1996
24HMPFriston-24 expansion24Task fMRI baseline; captures spin-history effectsFriston 1996; Satterthwaite 2013
aCompCor5 WM + 5 CSF PCs (typical)~10Physiological denoising without GSR debateBehzadi 2007; Muschelli 2014
24HMP + aCompCormotion + physio~34Workhorse for task fMRI; conservative on global signalParkes 2018
24HMP + aCompCor + 1GSRmotion + physio + global~35Workhorse for rest/connectivity; best motion suppression in Parkes 2018Parkes 2018; Ciric 2017
Scrubbing+ one-hot outliers (FD > thr)+ N spikesAdd on top when motion is heavy; FD > 0.5 mm for task, > 0.2 mm for restPower 2014; Satterthwaite 2013

Note

Default for this course: 24HMP + aCompCor + scrubbing (FD > 0.5 mm) for task fMRI. GSR is included only when you justify it (resting-state, FC). The choice should be pre-registered in your analysis plan, not picked after peeking at the contrasts.

The original benchmark papers — Ciric et al. 2017 (105 pipelines) and Parkes et al. 2018 (192 pipelines) — converge on the same finding: 24HMP + aCompCor + GSR + scrubbing consistently sits near the Pareto front of (motion suppression, signal preservation, degrees of freedom).

Per-subject motion screening

Before you settle on a strategy and run the GLM, sweep across subjects to see who is even eligible. The numbers you want per subject are mean FD, max FD, and the proportion of volumes above a spike threshold — all derivable from the same confounds TSV:

import os
from pathlib import Path
import pandas as pd
 
bids_dir     = Path(os.environ["BIDS_DIR"])
fmriprep_dir = Path(os.environ.get("FMRIPREP_DIR", bids_dir / "derivatives" / "fmriprep"))
task         = "flanker"  # adjust to your task label
 
records = []
for tsv in fmriprep_dir.rglob(f"*task-{task}*_desc-confounds_timeseries.tsv"):
    sub = tsv.name.split("_")[0]              # e.g. "sub-01"
    df = pd.read_csv(tsv, sep="\t")
    fd = df["framewise_displacement"].astype(float)  # first row is NaN
    records.append({
        "subject":      sub,
        "mean_FD":      fd.mean(),
        "max_FD":       fd.max(),
        "pct_FD_gt_05": (fd > 0.5).mean() * 100,
    })
 
summary = pd.DataFrame(records).sort_values("mean_FD")
print(summary)
 
# common exclusion rule: mean FD > 0.2 mm OR > 20% volumes with FD > 0.5 mm
excluded = summary.query("mean_FD > 0.20 or pct_FD_gt_05 > 20")["subject"].tolist()
print("Excluding:", excluded)

Warning

Exclusion thresholds (e.g. mean_FD > 0.20, pct_FD_gt_05 > 20) must be pre-registered as part of the analysis plan — not chosen after looking at the summary table. Picking thresholds that happen to drop the noisy-but-inconvenient subjects is a textbook garden of forking paths.


4. Loading confounds in nilearn

nilearn.interfaces.fmriprep.load_confounds reads the TSV, picks the right columns for a named strategy, and returns a clean DataFrame plus an optional sample_mask for scrubbing. This is the recommended path — it stays in sync with fMRIPrep’s column names across versions.

import os
from pathlib import Path
from nilearn.interfaces.fmriprep import load_confounds
from nilearn.glm.first_level import FirstLevelModel
 
bids_dir = Path(os.environ["BIDS_DIR"])
fmriprep_dir = Path(os.environ["FMRIPREP_DIR"])
 
sub = "sub-01"
task = "stroop"
fmri_file = fmriprep_dir / sub / "func" / (
    f"{sub}_task-{task}_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz"
)
 
confounds, sample_mask = load_confounds(
    str(fmri_file),
    strategy=("motion", "wm_csf", "high_pass", "scrub"),
    motion="full",            # 24HMP (Friston)
    wm_csf="basic",           # csf + white_matter means
    scrub=5,                  # also censor 5 volumes around each spike
    fd_threshold=0.5,
    std_dvars_threshold=1.5,
)
 
print(confounds.shape)        # (n_volumes, ~28)
print(confounds.columns.tolist()[:10])
print(f"Volumes kept: {len(sample_mask)} / {confounds.shape[0]}")

Drop the resulting DataFrame straight into the first-level fit:

events = bids_dir / sub / "func" / f"{sub}_task-{task}_events.tsv"
 
glm = FirstLevelModel(
    t_r=2.0,
    hrf_model="spm",
    drift_model=None,         # cosines come from confounds already
    high_pass=None,
    smoothing_fwhm=5.0,
)
glm = glm.fit(
    str(fmri_file),
    events=events,
    confounds=confounds,
    sample_masks=sample_mask, # optional: drops scrubbed volumes from the fit
)

Tip

Setting drift_model=None and high_pass=None is intentional when you include cosine_* from the confounds: you don’t want nilearn to add a second set of cosine drift regressors on top of fMRIPrep’s. Double high-passing eats degrees of freedom and shrinks effects you care about.

Inspecting what was selected

import matplotlib.pyplot as plt
 
fig, ax = plt.subplots(figsize=(10, 4))
confounds.filter(like="trans_").plot(ax=ax)
ax.set_xlabel("Volume")
ax.set_ylabel("Translation (mm)")
plt.tight_layout()

A 30-second sanity check that beats reading 28 column names.

Strategy presets

load_confounds also exposes named presets via load_confounds_strategy:

from nilearn.interfaces.fmriprep import load_confounds_strategy
 
confounds, sample_mask = load_confounds_strategy(
    str(fmri_file),
    denoise_strategy="scrubbing",   # or "simple", "compcor", "ica_aroma"
)

For most coursework, the explicit load_confounds(...) call above is more transparent and easier to defend in a methods section.


5. Adding confounds in FSL FEAT

FSL doesn’t read the BIDS TSV directly. You convert it into a plain whitespace-separated text file with one regressor per column, then point FEAT at it.

import os
from pathlib import Path
import pandas as pd
 
fmriprep_dir = Path(os.environ["FMRIPREP_DIR"])
sub, task = "sub-01", "stroop"
 
tsv = fmriprep_dir / sub / "func" / f"{sub}_task-{task}_desc-confounds_timeseries.tsv"
df = pd.read_csv(tsv, sep="\t")
 
# 24HMP + aCompCor (first 5) + csf + white_matter
motion_cols = [
    *[f"{p}{ax}" for p in ("trans_", "rot_") for ax in ("x", "y", "z")],
]
expanded = []
for c in motion_cols:
    expanded += [c, f"{c}_derivative1", f"{c}_power2", f"{c}_derivative1_power2"]
 
acompcor = [f"a_comp_cor_{i:02d}" for i in range(5)]
cols = expanded + acompcor + ["csf", "white_matter"]
 
out = df[cols].fillna(0)        # first-row derivative NaNs
out.to_csv(
    fmriprep_dir / sub / "func" / f"{sub}_task-{task}_confounds.txt",
    sep=" ", header=False, index=False,
)

Then in the FEAT GUI:

  1. Stats tab → Motion correction
  2. Confound EVs → tick on
  3. Browse to the *_confounds.txt file you just wrote

FEAT appends each column as an additional EV with no convolution. Make sure your *_confounds.txt has the same number of rows as your preprocessed BOLD volumes — FEAT errors out silently otherwise.

Warning

FSL’s GUI shows the confounds as unconvolved EVs with no contrast. That’s correct — they’re nuisance regressors, not effects of interest. Don’t set a contrast on them.

For more on the FSF design and second level, see 4.1 First-level Analysis with FSL and 2.5 fMRIPrep and FitLins (which lets you express the same model as a BIDS Stats Model and skip the GUI entirely).


6. Trade-offs

Every regressor you add buys some artefact suppression and costs you degrees of freedom plus a bit of real signal. The two failure modes:

  • Too aggressive (e.g. 50+ regressors, ICA-AROMA + 24HMP + aCompCor + GSR + scrubbing): you remove motion-locked signal and slow cognitive signal. Block designs at long durations are especially vulnerable. Effect sizes shrink, sometimes to nothing.
  • Too minimal (6HMP only on a high-motion sample): motion-driven false positives, especially near tissue boundaries and large vessels.

Tip

Decide your strategy from the literature for your task class before you run the GLM. If you find yourself adding regressors because a contrast “doesn’t look right”, you’re tuning your nuisance model to the data — that’s a garden of forking paths and won’t replicate.

A defensible analysis plan reads: “We modelled 24 motion parameters (Friston expansion), 5 anatomical CompCor components, CSF and WM mean signals, and censored volumes with FD > 0.5 mm. The strategy was fixed before analysis based on Parkes et al. (2018).“


7. Common pitfalls

Warning

NaNs in derivative columns. The first row of every *_derivative1 column is NaN. nilearn’s load_confounds handles this; pandas + statsmodels do not. Always .fillna(0) on the first row before regressing.

Warning

Mixing aCompCor and GSR uncritically. aCompCor already absorbs much of the variance that GSR would remove. Including both is defensible (Parkes 2018 endorses it for rest-state) but it shouldn’t be reflexive — write down why you added GSR.

Warning

Using framewise_displacement as a continuous regressor. FD is a non-linear summary of the six motion parameters — it’s collinear with them, not orthogonal. Use it for scrubbing decisions (which volumes to censor) or as a QC metric per subject, not as an EV alongside the motion params.

Warning

High-pass mismatch. If you include cosine_* from the confounds, set drift_model=None and high_pass=None in nilearn (or untick high-pass in FEAT). If you don’t include cosines, let the GLM tool add its own drift model — but not both.

Warning

Motion-outlier columns sum to scrubbing. Each motion_outlier_XX is a one-hot column that effectively removes one volume from the fit. If you also pass a sample_mask to censor the same volumes, you’re double-counting. Pick one mechanism.

Warning

Inconsistent strategy across subjects. If subject 03’s FD trace is bad and you scrub aggressively only for them, your group-level test is no longer apples-to-apples. Either apply the same threshold to everyone, or drop subjects who exceed a pre-set motion criterion (e.g. mean FD > 0.3 mm).


8. Further reading

  • Parkes, L. et al. (2018). An evaluation of the efficacy, reliability, and sensitivity of motion correction strategies for resting-state functional MRI. NeuroImage 171, 415–436. — The benchmark across 192 pipelines.
  • Ciric, R. et al. (2017). Benchmarking of participant-level confound regression strategies for the control of motion artifact in studies of functional connectivity. NeuroImage 154, 174–187.
  • Satterthwaite, T. D. et al. (2013). An improved framework for confound regression and filtering for control of motion artifact in the preprocessing of resting-state functional connectivity data. NeuroImage 64, 240–256. doi:10.1016/j.neuroimage.2012.08.052 — denoising framework that underpins the scrubbing + expansion approach used here.
  • Power, J. D. et al. (2014). Methods to detect, characterize, and remove motion artifact in resting state fMRI. NeuroImage 84, 320–341. — Where scrubbing comes from.
  • Behzadi, Y., Restom, K., Liau, J., & Liu, T. T. (2007). A component based noise correction method (CompCor) for BOLD and perfusion based fMRI. NeuroImage 37(1), 90–101. doi:10.1016/j.neuroimage.2007.04.042
  • Muschelli, J., Nebel, M. B., Caffo, B. S., Barber, A. D., Pekar, J. J., & Mostofsky, S. H. (2014). Reduction of motion-related artifacts in resting state fMRI using aCompCor. NeuroImage 96, 22–35. doi:10.1016/j.neuroimage.2014.03.028 — refinement of Behzadi’s aCompCor for resting-state denoising.
  • Friston, K. J., Williams, S., Howard, R., Frackowiak, R. S. J., & Turner, R. (1996). Movement-related effects in fMRI time-series. Magnetic Resonance in Medicine 35(3), 346–355. doi:10.1002/mrm.1910350312 — The original 24-parameter expansion.
  • fMRIPrep docs: Outputs → Confounds — column-by-column reference, kept in sync with releases.
  • nilearn docs: nilearn.interfaces.fmriprep.load_confounds — option matrix for strategy, motion, wm_csf, scrub.

See also