Station 9 · Lesson 3

Batch processing

Recommended15 minBefore this lesson: A single-subject scriptPrint the card
Starts from: sub-002_task-FaceRecognition_eeg.set (D4) · Nothing yet: the data as downloaded.Download the data (opens in a new tab)
Script to reach this state
MATLAB
% Start from a fresh MATLAB session: the batch script loads every subject itself
clear
[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

The idea in brief

The single-participantThe person whose data were recorded in the study. scriptA MATLAB file of commands that runs processing steps in order. works on D1. Your real project has twenty participants or more, with the same steps and the same settings for each one. Running the script twenty times and changing the file name by hand each time is slow, and it invites mistakes.

Before and after

A loop: three participants sub-002, sub-003 and sub-004 go one at a time into the function process_subject, which applies the pipeline steps; outputs go to derivatives/eeglab, one folder per participant, with batch_summary.csv. A circular arrow returns to the next participant, and a red arrow goes from the function to logs/batch_log.txt on error: log and continue.

Your next questions, each answered in its own section:

Why do I need this step?

The reasons in detail, and the terms you need

The solution is a loop: a list of participant names, with the same steps applied to each name. This is called batch processing.

But a simple loop has a problem: if the seventh participant fails for some reason (a missing file, an odd channelThe signal recorded from one electrode, as shown in EEGLAB., memory), everything stops, and in the morning you find that only six finished. That is why we wrap each participant in try and catch: if it fails, the error is written to a file, and the loop moves on to the next participant.

In this lesson we process only three participants from D4 (about 700 MB to download). The same script works on all eighteen if you download them.

How do I do it in EEGLAB?

10 steps in the menus

First: download three participants from D4

  1. Create a folder named faces_project with a rawdata folder inside it, and make faces_project the MATLAB working folder (cd). Run every script in station 9 from inside this folder. The file organization lesson explains the other folders.
  2. From the ds002718 page on OpenNeuro, download into rawdata the top-level files (dataset_description.json, README and task-FaceRecognition_events.json) and the full sub-002, sub-003 and sub-004 folders. Or, from the system command line, if you have the AWS CLI:
MATLAB
aws s3 sync --no-sign-request s3://openneuro.org/ds002718 rawdata --exclude "sub-*" --include "sub-002/*" --include "sub-003/*" --include "sub-004/*"

Second: try the pipelineThe fixed sequence of processing steps from raw data to results. on one participant in the GUIThe windows and menus used with the mouse instead of typed commands.

  1. Open File then Import data then Using EEGLAB functions and plugins then From BIDS folder structure and choose the rawdata folder. Enable importing events and electrodeThe physical sensor placed on the scalp to pick up the signal. locations from the BIDSA standard for naming and organizing neuroimaging data files. files, and take the event typeThe name or code that identifies what kind of event it is. from the trial_type column. Click Ok. A STUDYAn EEGLAB structure that groups many participants' datasets for joint analysis. is created with the three participants.
  2. Pick one participant from the Datasets menuA list of commands that opens from its name at the top of the window. and apply the steps in the GUI as in stations 3 to 5, changing only three settings: mains powerThe squared amplitude; how strong a signal is at a given frequency. in the UK is 50 Hz, not 60; the eye channels are EEG061 and EEG062; and the heart channels EEG063 and EEG064 are removed.
  3. Type eegh. These lines are what you will put inside the loop.

Third: turn the pipeline into a loop

  1. Copy the single-subject script into a new file batch_d4.m. Move the processing steps into a function at the end of the file named process_subject, which takes the participant file and the settings, saves the results, and returns a summary.
  2. At the top of the file: the list of participants, the settings, and a for loop that calls the function inside try.
  3. In catch: write the participant name and the error message to logs/batch_log.txt, and record the status failed in the summary table.
  4. Run the file. ICAA method that separates the recording into independent sources, some brain and some artifact. on 72 channels (70 EEG and 2 EOG) and fifty minutes of recording takes time for each participant, so let it run. If you have many participants, see the HPC lesson.
  5. When it finishes, open batch_summary.csv. Each row is one participant: status, bad channels, removed components, and remaining epochs.

How do I write it as code?

The same steps in 118 lines of MATLAB
MATLAB
%% D4 batch pipeline: same steps for every subject, errors logged
eeglab nogui

%% Settings
projectRoot = pwd;
bidsRoot    = fullfile(projectRoot, 'rawdata');
derivRoot   = fullfile(projectRoot, 'derivatives', 'eeglab');
logRoot     = fullfile(projectRoot, 'logs');
subjects    = {'sub-002', 'sub-003', 'sub-004'};

cfg.lineFreq    = 50;                          % UK recording: 50 Hz mains
cfg.eogLabels   = {'EEG061', 'EEG062'};        % HEOG, VEOG
cfg.dropLabels  = {'EEG063', 'EEG064'};        % ECG channels
cfg.epochEvents = {'famous_new', 'unfamiliar_new', 'scrambled_new'};
cfg.epochWindow = [-1 2];
cfg.thresholdUv = 100;

if ~exist(logRoot, 'dir'), mkdir(logRoot); end
logFile = fullfile(logRoot, 'batch_log.txt');

%% Import BIDS data once: events from trial_type, locations from electrodes.tsv
[STUDY, ALLEEG] = pop_importbids(bidsRoot, 'eventtype', 'trial_type', ...
    'bidsevent', 'on', 'bidschanloc', 'on', 'subjects', subjects, ...
    'outputdir', derivRoot, 'studyName', 'faces');
clear STUDY ALLEEG   % each subject is reloaded from disk inside the loop

%% Loop over subjects
nSub    = numel(subjects);
status  = repmat({'not run'}, nSub, 1);
nBad    = nan(nSub, 1);
nIcRem  = nan(nSub, 1);
nEpochs = nan(nSub, 1);

for s = 1:nSub
    subj = subjects{s};
    fprintf('\n===== %s (%d of %d) =====\n', subj, s, nSub);
    try
        inFile = fullfile(derivRoot, subj, 'eeg', sprintf('%s_task-FaceRecognition_eeg.set', subj));
        res = process_subject(inFile, cfg);
        status{s}  = 'ok';
        nBad(s)    = res.nBadChans;
        nIcRem(s)  = res.nIcRemoved;
        nEpochs(s) = res.nEpochs;
    catch err
        status{s} = 'failed';
        fid = fopen(logFile, 'a');
        fprintf(fid, '%s  %s  %s\n', datestr(now, 'yyyy-mm-dd HH:MM'), subj, err.message);
        fclose(fid);
        fprintf(2, 'FAILED %s: %s\n', subj, err.message);
    end
end

%% Summary table, one row per subject
summary = table(subjects(:), status, nBad, nIcRem, nEpochs, ...
    'VariableNames', {'subject', 'status', 'bad_channels', 'ics_removed', 'epochs_kept'});
disp(summary);
writetable(summary, fullfile(derivRoot, 'batch_summary.csv'));

%% ------------------------------------------------------------------
function res = process_subject(inFile, cfg)
% Single-subject pipeline from stations 3 to 5, with project settings in cfg

[inDir, baseName] = fileparts(inFile);
baseName = strrep(baseName, '_eeg', '');
EEG = pop_loadset('filename', [baseName '_eeg.set'], 'filepath', inDir);

% Remove ECG channels, keep EOG for ICA
EEG = pop_select(EEG, 'rmchannel', cfg.dropLabels);
fullLocs = EEG.chanlocs;

% High-pass: passband edge at 0.1 Hz (removes slow drift)
EEG = pop_eegfiltnew(EEG, 'locutoff', 0.1);
% Notch: remove a narrow band around the line frequency (revfilt = 1)
EEG = pop_eegfiltnew(EEG, 'locutoff', cfg.lineFreq - 2, 'hicutoff', cfg.lineFreq + 2, 'revfilt', 1);

% Detection with clean_rawdata channel criteria only:
% no drift removal, no ASR, no window rejection, EOG channels ignored
EEG_test = pop_clean_rawdata(EEG, 'FlatlineCriterion', 5, 'ChannelCriterion', 0.8, ...
    'LineNoiseCriterion', 4, 'Highpass', 'off', 'BurstCriterion', 'off', ...
    'WindowCriterion', 'off', 'BurstRejection', 'off', 'Distance', 'Euclidian', ...
    'channels_ignore', cfg.eogLabels);
badChans = setdiff({EEG.chanlocs.labels}, {EEG_test.chanlocs.labels}, 'stable');
fprintf('Bad channels: %s\n', strjoin(badChans, ', '));
if ~isempty(badChans)
    EEG = pop_select(EEG, 'rmchannel', badChans);
end
if ~isempty(badChans)
    EEG = pop_interp(EEG, fullLocs, 'spherical');
end
clear EEG_test

% Average reference, EOG channels excluded
eogIdx = find(ismember({EEG.chanlocs.labels}, cfg.eogLabels));
EEG = pop_reref(EEG, [], 'exclude', eogIdx);

% ICA copy: same data, high-pass at 1 Hz
EEG_main = EEG;
EEG_ica = pop_eegfiltnew(EEG_main, 'locutoff', 1);
EEG_ica = pop_runica(EEG_ica, 'icatype', 'runica', 'extended', 1);

% Copy the decomposition back to the main dataset
EEG_main.icaweights  = EEG_ica.icaweights;
EEG_main.icasphere   = EEG_ica.icasphere;
EEG_main.icachansind = EEG_ica.icachansind;
EEG_main.icawinv     = [];   % recomputed by eeg_checkset
EEG_main.icaact      = [];
EEG = eeg_checkset(EEG_main);
clear EEG_main EEG_ica

% Flag components above 90% Muscle or Eye, then remove them
EEG = pop_iclabel(EEG, 'default');
thresh = [NaN NaN; 0.9 1; 0.9 1; NaN NaN; NaN NaN; NaN NaN; NaN NaN];
EEG = pop_icflag(EEG, thresh);
artifactComps = find(EEG.reject.gcompreject);
if ~isempty(artifactComps)
    EEG = pop_subcomp(EEG, artifactComps, 0);
end

% Low-pass at 30 Hz, postponed until after ICA cleaning
EEG = pop_eegfiltnew(EEG, 'hicutoff', 30);
EEG.setname = [baseName ' clean'];
EEG = pop_saveset(EEG, 'filename', [baseName '_desc-clean_eeg.set'], 'filepath', inDir);

% Epochs, baseline, rejection of epochs beyond the threshold
EEG = pop_epoch(EEG, cfg.epochEvents, cfg.epochWindow, 'epochinfo', 'yes');
EEG = pop_rmbase(EEG, [-200 0]);
EEG = pop_eegthresh(EEG, 1, 1:EEG.nbchan, -cfg.thresholdUv, cfg.thresholdUv, EEG.xmin, EEG.xmax, 0, 0);
if any(EEG.reject.rejthresh)
    EEG = pop_rejepoch(EEG, EEG.reject.rejthresh, 0);
end
EEG.setname = [baseName ' epochs'];
EEG = pop_saveset(EEG, 'filename', [baseName '_desc-epochs_eeg.set'], 'filepath', inDir);

res.nBadChans  = numel(badChans);
res.nIcRemoved = numel(artifactComps);
res.nEpochs    = EEG.trials;
end

Which value should I choose?

Suitable values for your analysis goal
SettingValue in this lessonWhy
Line frequency50 HzD4 was recorded in Cambridge, UK, and PowerLineFrequency in the eeg.json file is 50. Your data from Saudi Arabia are 60. This setting is the clearest difference between D1 and D4.
Eye and heart channelsEEG061 and EEG062 for the eyes, EEG063 and EEG064 for the heartFrom the channels.tsv file in D4. Heart channels are removed; eye channels stay to help ICA and do not enter the reference.
Events for epochingfamous_new, unfamiliar_new and scrambled_newThe first presentation of each image, 150 of each type in sub-002. Repetitions have other names (_second_early and _second_late).
Importpop_importbids once before the loopReads events from events.tsv and electrode locations from electrodes.tsv, as in the official EEGLAB tutorial on the same data.
Where settings liveThe cfg variable at the topEverything that differs between projects is in one place, and the function itself does not change.
What happens on errorA line in batch_log.txt, status failed, then the next participantIn the morning you know who failed and why, and you rerun that participant alone.

How do I check it worked?

4 quick checks
  • The table printed at the end has three rows, with status ok in each row.
  • logs/batch_log.txt does not exist or is empty. If there is a line, it holds the participant name and the reason.
  • In each participant’s folder inside derivatives/eeglab there are two new files: desc-clean and desc-epochs.
  • The number of remaining epochs for each participant is close to 450 (150 of each type), minus the rejected ones and any epoch that crosses a boundary event (5 in sub-002), because pop_epoch drops those. If it is much lower for one participant, review it.

What mistakes do people make?

5 mistakes and how to avoid them
  • A loop without try and catch. One error in one participant stops all processing, and the night’s hours are lost.
  • A silent catch. Catching an error without logging it is worse than stopping: you think every participant finished. Always log the name and the message, and look at the summary table.
  • Settings written inside the function. If you write 60 inside process_subject and forget it, you process UK data with a Saudi filter. Every setting goes in cfg.
  • Forgetting the differences between datasets. The eye channels in D1 are named EOG1 and EOG2, and in D4 EEG061 and EEG062. Copying the script without checking these names makes ICA and the reference treat eye channels as scalp channels.
  • Trusting the loop without checking. A summary saying ok does not mean the cleaning is good. Open one or two files in the GUI, and look at the number of removed components and remaining epochs for each participant. The odd value in the table is the participant you review first.

How do I write it in my paper?

A Methods template: edit the data, settings and versions to match what you ran

A template: edit the data, settings and versions to match what you ran.

Data from [n] participants (ds002718; EEG-BIDS plugin 10.5, EEGLAB 2026.1) were high-pass filtered (passband edge 0.1 Hz; firfilt plugin 2.8), and line noise was attenuated with a band-stop filter (passband edges 48 and 52 Hz). Bad channels were identified with clean_rawdata (clean_rawdata plugin 2.11; flatline 5 s, channel correlation 0.8) and interpolated, then data were average-referenced, EOG excluded. Independent component analysis (extended Infomax; Lee et al., 1999) was computed on a 1 Hz high-passed copy, and components labelled Eye or Muscle with probability ≥ 0.9 (ICLabel 1.7; Pion-Tonachini et al., 2019) were removed. After low-pass filtering (passband edge 30 Hz), epochs from −1 to 2 s were baseline-corrected (−200 to 0 ms), and epochs beyond ±100 µV were rejected.

Where can I read more?

3 sources, official docs first

Written for EEGLAB 2026.1 · 2026-10-28

Need help?

Ask, share feedback, or report an error