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:
| Source | Typical frequency | Why it hurts |
|---|---|---|
| Head motion | broadband, spiky | Shifts voxel position → intensity changes correlated with stimulus blocks if motion is task-locked |
| Cardiac pulsation | ~1.0 Hz | Aliases into the BOLD band at typical TRs (2 s) |
| Respiration | ~0.3 Hz | Modulates B0 field, causes global signal swings |
| Low-frequency drift | < 0.01 Hz | Scanner heating, slow physiology — looks like a slow task effect |
| CSF / WM signal | mixed | Non-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_displacementtraces 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 mmrot_x,rot_y,rot_z— rotations in radians*_derivative1— temporal derivative (first row isNaN)*_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, mmdvars,std_dvars— root-mean-square BOLD change between successive volumes;std_dvarsis standardised
CompCor components
a_comp_cor_00..NN— anatomical CompCor: PCA of voxels inside an eroded WM + CSF mask. Captures physiological noise without dipping into grey matter.t_comp_cor_00..NN— temporal 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 maskwhite_matter— mean signal inside the WM maskglobal_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 equilibriummotion_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 aMethod,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.
| Strategy | Regressors | Count | When to use | Reference |
|---|---|---|---|---|
6HMP | 6 motion params | 6 | Quick exploration; very weak — task fMRI with low motion only | Friston 1996 |
24HMP | Friston-24 expansion | 24 | Task fMRI baseline; captures spin-history effects | Friston 1996; Satterthwaite 2013 |
aCompCor | 5 WM + 5 CSF PCs (typical) | ~10 | Physiological denoising without GSR debate | Behzadi 2007; Muschelli 2014 |
24HMP + aCompCor | motion + physio | ~34 | Workhorse for task fMRI; conservative on global signal | Parkes 2018 |
24HMP + aCompCor + 1GSR | motion + physio + global | ~35 | Workhorse for rest/connectivity; best motion suppression in Parkes 2018 | Parkes 2018; Ciric 2017 |
Scrubbing | + one-hot outliers (FD > thr) | + N spikes | Add on top when motion is heavy; FD > 0.5 mm for task, > 0.2 mm for rest | Power 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=Noneandhigh_pass=Noneis intentional when you includecosine_*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:
- Stats tab → Motion correction
- Confound EVs → tick on
- Browse to the
*_confounds.txtfile 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
*_derivative1column isNaN. nilearn’sload_confoundshandles 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_displacementas 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, setdrift_model=Noneandhigh_pass=Nonein 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_XXis a one-hot column that effectively removes one volume from the fit. If you also pass asample_maskto 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 forstrategy,motion,wm_csf,scrub.
See also
- 2.3 fmriPrep with Docker — where the confounds TSV is produced
- 2.4 Quality Control with MRIQC — pre-fMRIPrep motion QC
- 2.5 fMRIPrep and FitLins — declaring the same nuisance model in a BIDS Stats Model
- 5.1 NiLearn fMRI Basic Analysis — plugging confounds into
FirstLevelModel - 4.1 First-level Analysis with FSL — adding confound EVs in FEAT
- 3.2 General Linear Model — why nuisance regression works at all