DocsResults Validation
BCILattice Documentation

Results Validation

The statistics tab on the Results page: compare any factor or combination of factors in a study, with the test the design calls for, its assumptions, an effect size, and a corrected pairwise family.

v1.0BCINexus Platform · 2026-08-27[email protected]

Why a second tab

The Results page can tell you that one pipeline scored highest. It cannot tell you the difference is real. “Highest mean” is not a finding — with twelve subjects and three pipelines, a two-point accuracy gap is what you would expect from noise alone about as often as not, and a table of best scores gives a reader no way to know which they are looking at.

Results Validation closes that gap without leaving the app. Pick a metric, pick the axis to compare across — session, experiment, pipeline, workflow, subject, run, or any combination — and it runs the test the design actually calls for, checks that test’s assumptions, reports an effect size with an interval, corrects the pairwise family, and writes the whole thing out in a form you can paste into a methods section.

Results vs Results Validation

TabAnswersTypical use
ResultsWhat did this study measure?Best accuracy per experiment, confusion matrix, ROC, PR, per-experiment comparison table.
Results ValidationDoes the difference survive a test?Is pipeline A really better than B? Did session 2 differ from session 1? Does the winner depend on the workflow?

Nothing runs until you press Run

Switching to this tab, changing a factor, or coming back to the page never starts an analysis. Selecting options rebuilds only a one-line preview (how many levels, how many units, whether the design is paired). The tests themselves — bootstrap intervals included — run on a background thread when you press Run validation, so the window stays responsive and you never re-run a result you have already read.

What it reads

Every score your study has already stored. Each completed training run keeps a score per unit per pipeline per metric, and the tab turns all of them into one long table where every row carries every axis it belongs to:

value    metric    experiment       run     workflow   pipeline   subject   session
0.7318   accuracy  Motor Imagery    Run 1   EEGNet     EEGNet     sub-01    Session 1
0.6741   accuracy  Motor Imagery    Run 1   CSP+LDA    CSP+LDA    sub-01    Session 1
0.7204   accuracy  Motor Imagery    Run 1   EEGNet     EEGNet     sub-01    Session 2
   ⋮

Any column can become the grouping; any other column can become the observation unit. That is the whole idea — one table, and the design is whichever slice of it you ask for.

A unit id is not always a subject

What a training job is depends on the cohort mode the graph declared. Under “Per subject, per session” a job is one recording and its id looks like sub-01 / Session 2; under “Combined subjects, per session” a job is a session across the whole cohort; under the pooled mode there is one job for everything. The tab reads the mode each run recorded and splits the id accordingly, so a session-wise comparison groups sessions and not strings that happen to look like them. Runs trained before the mode was recorded are inferred from the id shape, conservatively.

Setting Up a Comparison

Six controls in the left panel. The preview line under them updates as you change any of them and tells you what you are about to run before you run it.

1. Metric

Lists only what this study actually measured, most-measured first. Metric names are canonicalised before they are grouped, so F1 Score from an ML Suite metric block and f1_macro from a typed backend are one metric rather than two half-empty ones. If a canonical metric turns out to pool more than one stored name, the result says so in its caveats — they are treated as one measurement, and if they are not one, that is yours to decide.

Direction is handled for you. Loss, log loss, MSE, RMSE and MAE are lower-is-better, so the written interpretation names the right winner without you inverting anything.

2. Compare by

Tick one factor, or several. Several ticked means their combination becomes the grouping: Session × Pipeline over two sessions and three pipelines gives six levels (Session 1 · EEGNet, Session 1 · CSP+LDA, and so on).

FactorOne level perAnswers
ExperimentExperiment in the studyDid the redesign in v2 actually help?
SessionRecording sessionIs performance stable across days, or is there a session effect?
SubjectParticipantHow much does performance vary between people?
PipelinePipeline that produced the scoreWhich model actually wins?
WorkflowWorkflow graph the run executedDoes the preprocessing route change the outcome?
RunTraining runIs run-to-run variation larger than the effect being claimed?

A factor that takes only one value across the stored results is greyed out with its level count shown — there is nothing to compare it against, and that is visible before you click rather than as a refusal after. Factors that cut the data identically are labelled as such: in an ML Suite run a job is one workflow on one subject, so Pipeline and Workflow are usually the same partition under two names, and the picker says so instead of letting you build a two-factor model on a distinction that is not there.

3. Observation unit

This is the control that decides whether your n is real, and it is the one most worth understanding.

Averaging is a claim

Fourteen subjects × two sessions × three pipelines, grouped by pipeline, gives 28 numbers per pipeline. They are not 28 independent observations of that pipeline — each subject contributes several, and testing them as if they were independent inflates n by exactly the factor that matters. Every comparison therefore names an observation unit, averages the scores inside each unit first, and reports how many stored values became how many observations (“144 stored values → 36 observations”) in its caveats.
UnitOne observation perWhen it is right
SubjectParticipantThe usual choice. A cross-subject claim is about people, so a person is one observation.
SessionSessionSession-level designs, where the recording day is the sampling unit.
Subject × SessionRecordingEach recording is a separate measurement — more power, but only if sessions are genuinely independent.
RunTraining runComparing configurations where a run, not a person, is what was sampled.
PipelinePipelineComparing subjects or sessions with pipeline as the repeated measure.
Each measurementStored scoreOnly when rows are genuinely independent — one score per recording, no repeats. Never the default.

Units that are already the grouping are greyed out: a factor cannot be both, because then every unit would sit in exactly one group, nothing could be paired, and every group would hold a single value.

4. Levels

Every level of the current grouping, with a checkbox. Unticking one drops it from the comparison — useful for excluding a pilot session or a pipeline that never finished training. Exclusions are recorded and reported: a test over three of five pipelines is a claim about those three, and the caveat says which two were dropped, because excluding the levels that disagreed is exactly how a result stops being one.

5. Design

Auto detects it, and says why. The design is repeated measures when the same units were measured under every level — which is what makes pairing meaningful — and independent groups otherwise.

DesignChosen whenWhy it matters
Repeated measures (paired)The same units appear under every level, and at least 60% of units are complete.Between-subject variance in BCI performance dwarfs most effects of interest. Pairing keeps it out of the error term, which is where nearly all the power comes from.
Independent groupsDifferent units per level, or too few complete units to pair without discarding most of the data.Every value is a separate observation. Correct when the cohorts genuinely differ.

Units missing a level are excluded from the paired test and named in the caveats — so an n of 9 out of 12 is never unexplained. Asking for pairing on a design that cannot support it is refused by name rather than quietly run as something else.

6. Test, post-hoc, correction, α

Test defaults to Recommended, which resolves to a named test with a stated reason printed next to the result — “three independent groups, every one normal by Shapiro-Wilk and with homogeneous variances by Levene’s test”. Every alternative valid for the design stays selectable, and whatever you select is what runs.

A recommendation, never a substitution

A page that quietly ran Kruskal-Wallis because the data looked skewed would be reporting something other than what you believe you asked for. The recommendation is separate from the run: it names a test and its reason, you accept or override it, and the result records which of those happened.

Post-hoc is Recommended (Tukey HSD after a one-way ANOVA on independent groups, pairwise contrasts otherwise), or an explicit choice, or none. Correction is Benjamini-Hochberg FDR by default, with Holm and Bonferroni for confirmatory work and “no correction” for a genuinely pre-registered single contrast. α defaults to 0.05.

The Test Catalogue

Which tests are offered depends on the design and the number of levels, because those are what make a test applicable at all.

Omnibus tests

TestDesignLevelsAssumesEffect size
One-way ANOVA (Fisher)Independent2+Normality, equal variancesη², plus ω²
Welch’s ANOVAIndependent2+Normality onlyη²
Kruskal-Wallis HIndependent2+Neitherε²
Repeated-measures ANOVAPaired3+Normality, sphericitypartial η²
Friedman testPaired3+NeitherKendall’s W

Welch’s ANOVA is not the classic one

SciPy has no Welch ANOVA, and answering a request for one with f_oneway returns the equal-variance test that Welch’s exists to replace. BCILattice implements Welch (1951) properly — each group weighted by n/s² with corrected denominator degrees of freedom — and reports those corrected df, so F(2, 25.6) tells a reader which test ran. On unequal-variance, unequal-n groups the two disagree by enough to move a result across α.

Repeated-measures ANOVA assumes sphericity and no Greenhouse-Geisser correction is applied, so a p between 0.01 and 0.05 is flagged as provisional and you are pointed at the Friedman test reported alongside it. That warning appears in the caveats every time, not only when it is convenient.

Two-level contrasts

TestDesignAssumesEffect size
Welch’s t-testIndependentNormalityHedges’ g
Student’s t-testIndependentNormality, equal variancesHedges’ g
Mann-Whitney UIndependentNeitherrank-biserial r
Paired t-testPairedNormality of differencesCohen’s dz
Wilcoxon signed-rankPairedNeitherCohen’s dz

Welch’s is the default over Student’s wherever a t-test is used: it costs almost nothing when variances are equal and is correct when they are not. Below twelve observations in the smaller sample, Recommended takes the rank-based test instead, because the normality a t-test assumes cannot be checked from that little data anyway.

The effect size follows the test rather than the other way round. Quoting Hedges’ g — which standardises by a pooled SD — next to a Mann-Whitney p describes two different models at once, so the rank test reports rank-biserial r instead.

Post-hoc families

With three or more levels, a significant omnibus says the levels differ but not which ones.

FamilyWhat it does
Tukey HSDEvery pair with the family-wise error rate controlled exactly for the all-pairs family — the right post-hoc after a one-way ANOVA on independent groups. No further correction is applied on top.
Pairwise contrastsEvery pair as its own contrast — paired by unit identity when the design is repeated — then corrected across the family. Both the raw and the corrected p are shown, because a table showing only corrected values hides how much the correction cost.

Correcting matters more than which correction you choose: eight levels give 28 pairs, and at α = 0.05 that family produces a false positive more than three quarters of the time uncorrected. Eleven levels (55 pairs) is the most this will print; past 60 pairs the family is refused rather than shown — see refusals.

If the omnibus did not reach α, the pairwise table still appears but is flagged as exploratory: reporting a significant pair under a non-significant omnibus is precisely the multiple-comparison problem the omnibus exists to control.

Two-factor models

Tick exactly two factors and a Type II two-way ANOVA is fitted alongside the main test, with both main effects and the interaction. That is the question a pair of one-way tests cannot answer: whether the effect of one factor depends on the other — whether the pipeline that wins on session 1 also wins on session 2.

A significant interaction is called out explicitly, because it means the main effects should not be reported on their own. Type II sums of squares are used because they are correct for the unbalanced designs that training results usually are. The model is fitted on the averaged table, not on the raw rows, so the same protection against pseudo-replication applies here as to the test above it.

The two-way model is descriptive when the design is paired

It treats every cell as independent — it cannot see that the same subjects appear under several levels. Read its interaction term as a description of the pattern and take the repeated-measures test above it as the test. The result says this in its caveats whenever it applies.

Assumption checks

CheckTestReported as
Normality, per levelShapiro-WilkW, p, and a verdict per level
Equal variancesLevene, median-centred (Brown-Forsythe)One line under the table

Levene is median-centred rather than mean-centred because BCI score distributions are routinely skewed, and the mean-centred form flags skew alone as unequal variance — which would push every such comparison onto Welch for a reason that is not heteroscedasticity.

“Cannot be checked” is not “normal”

A level with fewer than three values cannot be tested for normality at all. That is reported as unassessable rather than as a pass, and the recommendation will not cite normality as a reason when it could not be established. Where SciPy itself warns that a sample is close enough to constant that its calculation is unreliable, that warning is carried into the row rather than printed to a console nobody reads.

Reading the Result

Verdict, effect, design, levels

Four tiles across the top:

1

Verdict

Different or Not distinguished, with the p-value. Never “the same” — see the caveat below.
2

Effect size

The number, its name, and a magnitude word (negligible / small / medium / large) from the conventional bands for that family.
3

Design

Paired or Independent, and how many units the test actually used.
4

Levels

How many levels were compared, and how many stored values went in.

Not significant is not equivalence

A p above α means this study cannot distinguish the levels — not that they perform the same. The written reading says so in those words every time, because “no significant difference” is read as “no difference” by most readers and the two are very different claims. If you need to claim equivalence, you need an equivalence test and a pre-specified margin, which is a different design.

The APA line and the reading

Under the tiles: a plain-language reading of what the test found, then the statistic formatted the way it would be written in a paper, then why this test and this design were chosen.

F(2, 22.0) = 168.74, p < .001, partial η² = 0.939

Repeated-measures ANOVA over 3 levels of pipeline, on 12 subjects: the levels
differ by more than sampling noise (p < .001 < α = 0.05). The effect is large
(partial η² = 0.939). "EEGNet" has the best mean accuracy (0.7311) and
"CSP+LDA" the worst (0.6755), a spread of 0.0556.

The APA line is there because the number a reader needs is not the one a table is good at showing, and assembling F(2, 22.0) = 168.74, p < .001 by hand from six cells is where transcription errors come from.

The distribution plot

A box plot per level, ordered to match the descriptives table underneath it. When the design is paired, each unit is drawn as a faint line across every level, behind the boxes.

Why the paired lines are there

A box plot alone hides the thing a repeated-measures result rests on: whether the units moved together. “Every subject improved a little” and “two subjects improved a lot” are the same pair of boxes and very different findings. The connecting lines are what distinguish them.

The figure renders off the UI thread and exports to PNG, SVG or PDF through the standard export button.

Cross-checks

Every result runs its sibling test too — the rank-based counterpart of a parametric test, or vice versa — and reports it as a labelled cross-check with whether it reaches the same conclusion.

Cross-checks are robustness, not candidates

Taking whichever of them crosses α is exactly the practice a pre-chosen test exists to prevent, and the panel says so on screen. Their value is the opposite: a conclusion that survives both the parametric and the rank form of the same question is one a reviewer will not ask about. When they disagree, the honest report is that the conclusion rests on an assumption rather than on the data — and the tab says that too.

What this result does not say

The last section of every result, and the one worth reading first. It is generated from what actually happened, not from a template. It can include:

  • How many stored values were averaged into how many observations, and why testing them unaveraged would inflate n.
  • Which units were excluded from a paired test for not having every level.
  • Which levels you excluded by hand.
  • That the canonical metric pooled more than one stored metric name.
  • That the omnibus was not significant, so the pairwise table is exploratory.
  • That the repeated-measures ANOVA assumes sphericity with no correction applied.
  • That Levene rejected equal variances while Fisher’s ANOVA was the test run.
  • That the design cannot reach α at all — see below.

When the sample cannot reach α at any effect size

Rank tests have a floor. Five paired observations cannot produce a two-sided Wilcoxon p below 0.0625; three-versus-three cannot produce a Mann-Whitney p below 0.1. If that floor sits above your α, no effect however large can be significant, and reporting “p = 0.0625, not significant” without saying so describes the sample size as if it were the effect. The tab computes the floor and tells you to report the difference and its interval instead.

Effect Sizes

Every test returns one, because “p = 0.03” answers whether a difference is distinguishable from zero and never how big it is. A 0.4-point accuracy gain across 200 subjects is both highly significant and useless.

MeasureReported withSmallMediumLarge
η², partial η², ε², Kendall’s WANOVA family, Kruskal-Wallis, Friedman0.010.060.14
Hedges’ g, Cohen’s dzt-tests, paired contrasts0.20.50.8
rank-biserial rMann-Whitney U0.10.30.5

Two-level contrasts also carry a percentile-bootstrap confidence interval on the effect size itself. Where too few resamples are usable to make that interval meaningful, it is reported as absent with the reason rather than as a number describing the resampling.

A perfectly consistent difference is undefined, not zero

If every pair moves by exactly the same amount, that is the largest effect a paired design can show — and standardising it divides by a spread of zero. It is reported as undefined with an explanation, never as 0.0 (which reads as “no effect” for the strongest possible one) and never as infinity. The mean difference and its interval are exact in that case, and those are the numbers to report.

What It Refuses to Do

A NaN in a results table reads as “not significant” to every reader who has seen one, so the tab refuses by name instead of producing one. Each refusal says what is missing and what to change.

SituationWhat happens
The grouping factor takes one valueRefused — a comparison needs at least two levels. The Run button is disabled and the preview explains why.
A factor is both the grouping and the observation unitUnavailable in the picker: every unit would sit in one group, so nothing could be paired and every group would hold one value.
Fewer than two observations in a groupRefused — an ANOVA has no within-group variance to work with.
Every group has zero spreadRefused — F divides by zero. Report the group means directly.
Pairing requested on an incomplete designRefused, naming how many units qualify out of how many.
A two-way table with an empty cellThe interaction cannot be estimated. Named cells are listed; an additive model is fitted instead and flagged.
More than 60 pairwise comparisonsRefused with the arithmetic: 40 levels give 780 tests and a Bonferroni threshold of 6.4 × 10⁻⁵. The omnibus still runs and still answers the question that many levels can support.
statsmodels is not installedA named refusal with the install command, for the models that need it — never a traceback and never a silent fall back to a different method.

Exporting for a Paper

Two buttons under the reading.

ButtonProduces
Copy reportThe whole validation as plain text on your clipboard: setup, descriptives, assumptions, the primary test and its cross-checks, the post-hoc table, the two-way model, the reading, and every caveat. Deliberately includes what a screenshot of the table would leave out — which test was chosen and why, and what was averaged into what — because those are the parts a reviewer asks about.
Export CSVOne flat table with every number: setup rows, descriptives, normality, variance, the primary test, cross-checks, post-hoc rows, two-way terms, the per-unit observations that went in, and the caveats. One file rather than four, so the group ns and the corrected p can never drift apart.

The distribution plot exports separately through the figure export button beneath it.

Or take the whole study out

That CSV is one validation. To export everything the study measured — every stored score, every run, and every configuration value that produced them — use Results Export, which is free on every plan. The same page covers the Notebook tab, where you can run the analysis this tab does not offer against those tables directly, using this module.

Worked Examples

Which pipeline actually wins?

Metric            Accuracy
Compare by        Pipeline
Observation unit  Subject
Design            Auto  →  repeated (the same 12 subjects ran all 3 pipelines)
Test              Recommended  →  Repeated-measures ANOVA
Post-hoc          Recommended  →  pairwise contrasts, FDR corrected

The paired design is the point here. Twelve subjects, three pipelines, and the between-subject spread is larger than the pipeline effect — an unpaired test would very likely miss a real difference that pairing finds comfortably.

Is performance stable across sessions?

Metric            Accuracy
Compare by        Session
Observation unit  Subject
Design            Auto  →  repeated
Test              Recommended  →  Paired t-test or Wilcoxon by cohort size

A non-significant result here is the common and useful one — but read it as “this study cannot distinguish the sessions”, not as “the sessions are equivalent”.

Does the winning pipeline depend on the session?

Metric            Accuracy
Compare by        Session  +  Pipeline        (6 crossed levels)
Observation unit  Subject
Design            Auto  →  repeated
→ Repeated-measures ANOVA over the 6 levels, plus a two-way model with
  Session, Pipeline, and the Session × Pipeline interaction.

The interaction term is the answer. A non-significant interaction means the pipeline ranking holds across sessions; a significant one means it does not, and the main effects should not be quoted on their own.

Did the v2 experiment improve on v1?

Metric            Accuracy
Compare by        Experiment
Observation unit  Subject × Session
Design            Auto  →  repeated if both experiments ran the same recordings

Does the preprocessing workflow matter?

Metric            Accuracy
Compare by        Workflow
Observation unit  Subject
→ If the picker says Workflow is the same split as Pipeline, the two are one
  partition under two names in this study and comparing them is comparing
  pipelines. Group by Pipeline instead.

Troubleshooting

SymptomCause and fix
“Nothing validated yet” and the metric list is emptyNo completed run has stored per-result metrics yet. Train a model in ML Suite → Execute; the scores appear here once a run finishes.
Every factor is greyed outThe study has one experiment, one pipeline and one session, so nothing varies. Add a second arm — another pipeline, another session, another experiment — and re-run training.
Session shows only “Unspecified session”The runs were trained under a cohort mode that does not separate sessions, so no session identity was recorded per score. Set the data source’s Runs mode to one that splits sessions and re-train.
Subject shows only “All subjects”The runs were pooled — one job across the whole cohort — so there is one score, not one per participant. That is a valid design; it just has no subject axis to compare or pair on.
Design says independent when you expected pairedToo few units were measured under every level. The reason line says how many qualified; usually one arm did not run on the whole cohort.
Pairwise comparisons refusedMore than 60 pairs. Untick levels, or group by a coarser factor. The omnibus result above is unaffected.
Effect size shows —Either the spread is zero (a perfectly consistent difference, which is undefined rather than zero) or too few bootstrap resamples were usable. The note next to it says which.
A test you want is not in the listThe list follows the design and the level count. Two levels do not offer ANOVA; three do not offer a t-test. Change the design dropdown, or the grouping.
The two-factor model is missingIt appears only when exactly two factors are ticked, and only when every combination of them holds at least one result.

Related

Analysis Suite covers the statistics available on the signal itself, before training. Using the ML Suite covers how the runs that this tab reads are produced, including the Runs mode that decides what a unit is.
Results Validation v1.0 · BCINexus Platform · 2026-08-27