Pipeline Recipes
Build channel selection, dimensionality reduction, cross-validation, LOSO and domain adaptation from blocks — starting from nothing but Filtered recordings.
Before you start
Every recipe on this page starts from the same place and adds blocks. They are written for the case where you have nothing but the recordings — no finished Analysis stage to inherit — so they work identically on every plan.
The starting point
Drop a Select Files block and set:
| Setting | Set to | Why |
|---|---|---|
| modality | EEG / fNIRS / EMG | Whichever you recorded |
| data_source | Filtered | The band-passed recording — the one pre-analysis stage, available on every plan |
| session | All Sessions | Or one session, if you are deliberately scoping |
| selected_subjects | (pick in the dialog) | More than one subject is required for anything cross-subject |
That block now emits X (the signal table) and Y (labels), plus Channels for a recording-stage source. Everything below hangs off those two ports.
Why start from Filtered rather than a finished stage
MLFlow, Workflow, or both
Any of these recipes can be built entirely in the Workflow canvas — that is the simplest thing to do, and for a one-off analysis it is the right answer. Build in MLFlow instead when you want the same chain reused across several workflows or studies. The combined section below shows the split.
Channel selection
Keeps the channels that actually separate your classes and drops the rest. On high-density montages this is usually the single biggest win: fewer, better channels means fewer parameters, less overfitting, and a model that trains in seconds.
Wiring it
[Select Files] [Channel Selection]
data_source: Filtered
X ──────────────────────────▶ Data
Y ──────────────────────────▶ Labels
Selected Data ────▶ (next stage)
Selected Channels ─▶ (optional: Data Graph)
Scores ────────────▶ (optional: Data Graph)| From | To |
|---|---|
| Select Files · X | Channel Selection · Data |
| Select Files · Y | Channel Selection · Labels |
| Channel Selection · Selected Data | the next block in the chain |
| Setting | Set to | Why |
|---|---|---|
| method_name | t-Value Method | A solid default for two-class contrasts |
| percentile | 5 | Keeps the top 5% of channels by score. Raise it if you are dropping too much |
| rest_label | 0 | The integer label of your rest / baseline class |
| ignore_classes | (blank) | Comma-separated labels to leave out of the ranking |
See what it did
Selected Channels and Scores into a Data Graph. It is a pass-through tap, so it costs you nothing in the graph and shows exactly which channels survived and why.Choosing a method
Twenty methods are available. In practice:
| Use | When |
|---|---|
| t-Value Method | Two classes, roughly normal — the default, and hard to beat |
| ANOVA F-value | More than two classes |
| Common Spatial Pattern (CSP) | Motor imagery. The classic choice for left/right hand EEG |
| Regularized CSP / Sparse CSP | CSP on few trials or noisy data, where plain CSP overfits |
| Mutual Information Feature Selection | You suspect a non-linear relationship |
| Wilcoxon Signed-Rank Test | Non-normal distributions, paired design |
| PSD-based Selection | The discriminating signal is a band-power difference |
Dimensionality reduction
Projects many correlated channels onto a handful of components. Useful after channel selection, when what remains is still wider than your trial count.
Wiring PCA
PCA is a class block, so it has a selected_method — set it to fit_transform and its ports become X, y and one output.
[Channel Selection] [PCA] selected_method: fit_transform
Selected Data ────────────────────▶ X n_components: 20
(labels from Select Files) ───────▶ y
output ─────▶ (next stage)| Setting | Set to | Why |
|---|---|---|
| selected_method | fit_transform | Fits and projects in one step, which is what a pipeline stage needs |
| n_components | 10–30 | Start well below your trial count. 20 is a reasonable first guess |
| whiten | false | Turn on only if the downstream model assumes unit variance |
FastICA, KernelPCA and TruncatedSVD wire identically. Use FastICA when you want statistically independent sources rather than maximum-variance ones, KernelPCA when the structure is non-linear.
Where it belongs
Fit it inside the folds, not before them
Feature selection
Where dimensionality reduction builds new combined components, feature selection keeps a subset of the original columns — which stays interpretable, because every surviving feature is still a real channel or band.
| Setting | Set to | Why |
|---|---|---|
| SelectKBest · k | 20–50 | Number of features to keep |
| SelectKBest · score_func | f_classif | ANOVA F between each feature and the label |
| SelectPercentile | percentile: 10 | Use instead of SelectKBest when feature count varies between subjects |
| VarianceThreshold | threshold: 0.0 | Cheap first pass — drops dead / constant channels |
| RFE | n_features_to_select | Recursive elimination. Strong but slow: it refits the model repeatedly |
Class blocks need a selected_method
VarianceThreshold and RFE are class blocks, like PCA — set selected_method to fit_transform and their ports become X / y → output. SelectKBest and SelectPercentile are plain function blocks and need no method chosen.The same fold-leakage warning applies: selection is fitting, so inside the folds it goes.
Cross-validation
Trains nothing itself. It splits, hands each fold to the estimator you wire in, and reports per-fold and aggregate metrics with a confidence interval.
Wiring it
[Channel Selection] [Cross-Validation]
Selected Data ──────────────────▶ Dataset cv_strategy: stratified_k_fold
[Select Files] n_splits: 5
Y ──────────────────────────────▶ Labels
[a model block]
model_obj ──────────────────────▶ Model
Metrics ──────────▶ [Output Block]
Confusion Matrix ─▶ [Data Graph]
Fold Scores ──────▶ [Data Graph]| From | To |
|---|---|
| Channel Selection · Selected Data | Cross-Validation · Dataset |
| Select Files · Y | Cross-Validation · Labels |
| your model block · model_obj | Cross-Validation · Model |
| Cross-Validation · Metrics | Output Block |
| Cross-Validation · Confusion Matrix | Data Graph (optional) |
Picking a strategy
| Setting | Set to | Why |
|---|---|---|
| stratified_k_fold | (default) | One subject, balanced classes. The everyday choice |
| k_fold | — | Regression, or where stratification is meaningless |
| group_k_fold | — | Multiple subjects, but you only need folds not to split a subject |
| stratified_group_k_fold | — | Both: grouped AND class-balanced. Classification only |
| leave_one_group_out | — | LOSO / leave-one-session-out — see below |
| time_series | — | Ordered data where a fold must not train on the future |
| repeated_stratified_k_fold | n_repeats: 10 | Small datasets, where a single 5-fold estimate is noisy |
LOSO (cross-subject)
Leave-One-Subject-Out holds out an entire subject per fold. It is the honest way to answer “will this work on someone the model has never seen” — and the number it returns is almost always lower than a within-subject score, which is the point.
The pooling step nobody expects
A normal run sees one subject at a time
Set the Input Block's subject_scope to all. That collapses every selected subject into a single run, which is what gives the CV block real subject groups to hold out.
Wiring it
| Setting | Set to | Why |
|---|---|---|
| Input Block · subject_scope | all | Pools every selected subject into one run — the step that makes LOSO possible |
| Cross-Validation · cv_strategy | leave_one_group_out | One fold per group |
| Cross-Validation · group_by | subject | Groups are subjects. Use session for leave-one-session-out |
| Select Files · selected_subjects | 3 or more | Two subjects gives you two folds — technically valid, statistically meaningless |
You do not need to wire the Groups port. Leave it empty and the block derives per-sample subject and session ids from the run's own provenance, selected by group_by. Wire it only when you have group ids of your own that the provenance does not know about.
group_by: auto is smarter than it looks
auto the block prefers whichever of subject or session actually has more than one distinct value — so a single-subject multi-session recording automatically becomes cross-session rather than failing.The leakage guard
strict_leakage_check is on by default, and it will stop a run rather than let it report a number you would have to retract.
It fires when:
- you use a non-grouped strategy on data spanning more than one subject or session — trials from the same person land in both train and test, and the score measures memorisation;
- group ids were present but had to be dropped, e.g. a block changed the row count and the groups no longer align with
X.
The fix is almost always to switch to group_k_fold or leave_one_group_out, not to turn the check off. Turn it off only when you have a specific reason and know what the resulting number does and does not mean.
Domain adaptation
Cross-subject and cross-session models fail mostly because each recording sits in a slightly different feature space — different impedances, cap placement, day. Domain adaptation pulls those spaces together before the model sees them.
Euclidean Alignment
Start here. It is cheap, needs no target labels, and reliably lifts cross-subject scores — the standard first thing to try for transfer EEG.
[Channel Selection] [Euclidean Alignment]
Selected Data ────────────────────▶ Data reference_mode: mean
▶ Reference (optional)
Aligned Data ────▶ [Cross-Validation] Dataset| From | To |
|---|---|
| Channel Selection · Selected Data | Euclidean Alignment · Data |
| Euclidean Alignment · Aligned Data | Cross-Validation · Dataset |
CORAL / MMD / TCA
These take an explicit source and target, so they suit the setup where you train on a group of subjects and adapt to one held-out person. Use Data Splitter or two Select Files blocks to produce the two streams.
| Block | Inputs → Outputs | Use when |
|---|---|---|
| CORAL Adapter | Source Data, Target Data → Aligned Source, Alignment Matrix | Fast, linear second-order alignment. The usual first choice after EA |
| MMD Aligner | Source Data, Target Data → Aligned Data, MMD Score | Distribution-level alignment; MMD Score tells you how far apart they were |
| Transfer Component Analysis | Source Data, Target Data → Source Projected, Target Projected | Learns a shared subspace. Stronger, slower, needs tuning of n_components and mu |
| Feature Alignment | Source Data, Target Data → Aligned Data | Simple z-score / moment matching. Good baseline to beat |
| Session Transfer | Source Features, Target Features, Source Labels → Transferred Features, Sample Weights | Same subject, different day |
Subject Adaptation
Different shape from the rest: it takes a trained model and a small amount of labelled data from the new person, and returns a model tuned to them. This is the calibration-session workflow — a few minutes of data from a new user instead of a full training session.
| From | To |
|---|---|
| BCILattice Trainer · Trained Model | Subject Adaptation · Base Model |
| (new subject) Select Files · X | Subject Adaptation · Adaptation Data |
| (new subject) Select Files · Y | Subject Adaptation · Adaptation Labels |
| Subject Adaptation · Adapted Model | Output Block |
| Setting | Set to | Why |
|---|---|---|
| method | fine-tune | Or a lighter scheme if you have very few shots |
| n_shots | 20 | Labelled trials from the new subject. This is your calibration budget |
| lr | 0.0001 | Low on purpose — you are nudging a trained model, not retraining it |
The combined pipeline
Everything above, in one graph: filtered recordings in, an honest cross-subject number out.
[Select Files] data_source: Filtered, 8 subjects
X ──┐
Y ──┼──────────────────────────────────┐
│ │
▼ │
[Channel Selection] t-Value, top 5% │
Selected Data │
│ │
▼ │
[Euclidean Alignment] reference: mean │
Aligned Data │
│ │
▼ ▼
[Cross-Validation] Dataset Labels
cv_strategy: leave_one_group_out
group_by: subject
strict_leakage_check: on
│ ▲
│ └──── Model ──── [a model block]
├── Metrics ─────────────▶ [Output Block]
├── Confusion Matrix ────▶ [Data Graph]
└── Fold Scores ─────────▶ [Data Graph]
+ Input Block subject_scope: all ← the pooling step LOSO needsSplitting it across MLFlow + Workflow
Once that chain works, the reusable half is everything between the data and the validation. Move it into an MLFlow pipeline:
MLFlow — "EEG Preprocess" Workflow ┌──────────────────────────┐ ┌────────────────────────────────┐ │ [Input Block] Signal ─┐ │ │ [Select Files] │ │ [Input Block] Labels ─┼─▶│ │ X ──▶ Signal ┌─────────────┐ │ │ [Channel Selection] │ ═══▶ │ Y ──▶ Labels │EEG Preprocess│ │ │ [Euclidean Align.] │ │ │ Clean ─┼─┼─▶ [Cross-Validation] │ [Output Block] Clean │ │ └─────────────┘ │ └──────────────────────────┘ └────────────────────────────────┘
Two Input Blocks and one Output Block become two input ports and one output port on the compiled block, named from each block's port_name. Point a second Select Files at fNIRS instead and the same compiled block runs on that. See the port contract.
Sanity checklist
Before you believe a number, check these:
- Is the split grouped? Multi-subject data with a plain k-fold measures memorisation. The leakage guard catches this — do not switch it off to make the error go away.
- Did anything fit outside the folds? A reduction or feature selection fitted upstream of Cross-Validation has seen the test rows.
- Is the score above chance? The CV report carries theoretical and empirical chance levels. Compare against the empirical one — with imbalanced classes it is higher than 1/n.
- Is the confidence interval narrow enough to mean anything? A 5-fold run on 40 trials produces a wide one. Use
repeated_stratified_k_fold. - Did you pool for LOSO? Without
subject_scope: all, a cross-subject strategy has one subject to work with. - Two models to compare? Use Statistical Comparison — McNemar or a paired test — rather than eyeballing two accuracies.
Block-by-block detail: Custom Blocks and Advanced Blocks. How the canvases fit together: Using the ML Suite.