DocsResults Export & Notebook
BCILattice Documentation

Results Export & Notebook

Every table behind the Results page as CSV or Excel — and a notebook inside the app to run your own Python against them: custom charts, custom validation, any reshaping you need.

v1.0BCINexus Platform · 2026-09-05[email protected]

Your results, your analysis

However many charts a tool draws, it will not draw the one your reviewer asks for. The Results page answers what did this study measure, and Results Validation answers does the difference survive a test. This page is about the third question, the one no fixed feature can answer in advance: the analysis we did not build.

There are two answers to it, and you can use either or both. Take the whole result set out as tables and work on it in whatever you already use. Or stay in the app and write the analysis against the results directly, with everything already loaded.

Export and notebook

What it isUse it when
Export resultsNine tables — experiments, runs, every stored score, metric summaries, every configuration value — as CSV files, a zip, or one Excel workbook.You want the numbers in R, SPSS, a spreadsheet, a paper, or a colleague’s inbox.
NotebookA third tab on the Results page. Cells of Python, run one at a time, output underneath.You want a chart or a test the app does not offer, without leaving it or re-exporting after every change.

What each plan gets

Export is free on every plan, including Free. These are your measurements; a tool that recorded them and then charged you to read them elsewhere would not deserve them.

Running a notebook inside the app is part of Researcher and above. It is the same boundary the ML Suite’s Custom Code block already uses: a Free plan gets the whole catalogue, and what is paid for is the escape hatch out of it.

ActionFreeResearcher and up
Export every table (CSV / zip / Excel)YesYes
Starter notebook shipped with the exportYesYes
Write, edit and save notebooks in the appYesYes
Open the four templatesYesYes
Export a notebook as .ipynbYesYes
Import an .ipynbYesYes
Run cells inside the appYes

The escape hatch stays open

On a Free plan you can still write the whole notebook here, export it as .ipynb and run it in your own Jupyter or in Colab — the templates and the exported CSVs are built for exactly that. What Researcher buys is not the capability, it is not having to leave.

Exporting Results

Where to start it

Two places, one dialog — they cannot drift apart:

1

Results page → Export results…

Under the experiment list, on the left. Available even when the study has no results yet — “this study has not trained anything” and “give me what it has” are different questions.
2

Import / Export page → Take Out → Export Results

Beside Export Study. The study package moves a study; this moves the numbers. A reviewer asking for your results does not want a .bciproj they cannot open.

The dialog lists all nine tables with a checkbox each, the three formats, and a switch for the starter notebook. The export itself runs on a background thread — a study with thousands of scores spends real time flattening every configuration into the parameters table, and the window stays usable while it does.

The nine tables

TableOne row perHolds
studykey / valueStudy name and id, app version, export time, how many experiments, runs and scores, and which metrics were recorded.
experimentsexperimentIdentity, status, version, which experiment it was branched from, datasets, subjects, sessions, modalities, run count — and one column per metric that experiment actually summarised.
runstraining runRun label and number, timestamp, cohort mode, workflow, the pipelines it scored, how many units and scores, duration, seed, and each run’s own run-level metrics.
observationsstored scoreThe tidy long table. One row per number the study measured, labelled with every axis it belongs to. This is the table statistics are run on.
metricsexperiment × metricBest, mean, SD, n, min, max, and which direction is better.
parametersconfiguration valueEvery setting the study holds, flattened — preprocessing, filtering, activation maps, GLM, ML training, analysis, report, plus each run’s own hyperparameters.
modelstrained modelWhat was produced, where it points, its size and content hash.
reproducibilityexperimentConfig and dataset hashes, seeds, and the software versions the run was produced under.
versionsversion-history entryThe study’s own version log.

Nothing is invented

There is no fixed metric list anywhere in the export. A metric your study never computed has no column and no row; a custom scorer this codebase has never heard of gets both. That is the same rule the Results dashboard follows, for the same reason — a zero-filled column for a metric nobody measured reads as a measurement of zero.

How they join

Every row of every experiment-level table carries experiment_id as its first column. Open the folder in anything — pandas, R, Excel — and the tables rejoin without the app:

import pandas as pd

obs    = pd.read_csv("observations.csv")
params = pd.read_csv("parameters.csv")
runs   = pd.read_csv("runs.csv")

# every score, next to the settings that produced it
settings = params[params.scope == "experiment"].pivot_table(
    index="experiment_id", columns="parameter", values="value", aggfunc="first")
obs.merge(settings, on="experiment_id", how="left")

The observations table is the one most analyses start from:

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

These are the same rows the Results Validation tab tests, produced by the same reader. An export whose numbers disagree with the screen they came from would be worse than no export, and the only way to guarantee they agree is for there to be one source.

The parameters table

The table that makes the rest of the export usable as evidence. A set of scores whose settings are written down nowhere cannot be reproduced, and “we used the defaults” is not a record of what the defaults were.

experiment_id  experiment      scope       run     section        parameter        value    type
exp-1          Motor Imagery   experiment          preprocessing  bandpass.low     8.0      float
exp-1          Motor Imagery   experiment          preprocessing  bandpass.high    30.0     float
exp-1          Motor Imagery   experiment          preprocessing  channels         C3; Cz; C4  list
exp-1          Motor Imagery   experiment          ml_training    folds            5        int
exp-1          Motor Imagery   run         Run 1   hyperparameters lr              0.001    float
exp-1          Motor Imagery   run         Run 1   hyperparameters epochs          50       int

Nested settings are flattened to a dotted path (bandpass.low). A list of plain values stays one parameter — C3; Cz; C4 — because splitting it into three rows turns one setting into three that no longer read as one.

Formats

FormatWhat you getNotes
CSV folderOne <table>.csv per table, plus a README.txt that explains each file.The default. Written with a UTF-8 byte-order mark so Excel on Windows reads non-ASCII subject ids correctly instead of mangling them.
CSV zipThe same files in one archive, with the starter notebook and README inside.For emailing or attaching to a submission.
Excel workbookOne .xlsx, one sheet per table, header row frozen.Needs the optional openpyxl package. Without it the option is disabled and says so; every table is still available as CSV.

The starter notebook

Every export can write an analysis.ipynb beside the tables. It is not a format description — it is a file that already runs. Open it in Jupyter or drop the folder into Colab and it loads the CSVs, summarises each metric by pipeline, draws a bar chart with error bars, runs a paired t-test across subjects, and pivots the parameters table.

jupyter lab analysis.ipynb

It uses nothing but pandas, matplotlib and scipy, so it opens anywhere. It is the answer to “can I do my own analysis on this?” that does not require reading a schema first.

The Notebook

A third tab on the Results page, beside Results and Results Validation. Notebooks on the left, cells in the middle. It is a notebook in the ordinary sense — cells of Python, run one at a time, output underneath, and state carried from one cell to the next.

What makes it worth having inside the app rather than beside it is two things.

What is already loaded

The study is not something you load; it is already there. Every name below exists in every cell from the first line you type:

NameWhat it is
resultsThe tidy table — one row per stored score, with every axis it belongs to. The same rows Results Validation tests.
dfThe same table under the name most people type first. It is the same object as results, not a copy, so the two can never drift apart.
experiments_df, runs_df, metrics_df, parameters_df, models_dfThe other export tables, as DataFrames.
sheetsAll nine, keyed by name.
experimentsThe raw experiment records, for anything the tables flattened away.
np, pd, pltnumpy, pandas, matplotlib.
sns, scipy, stats, sklearn, torchseaborn, scipy, scipy.stats, scikit-learn and PyTorch — imported the first time a cell touches them, so a notebook that never uses torch never pays for it.
validationThe app’s own statistics layer — the exact module the Results Validation tab runs on.
describe()Mean / SD / n per metric and pipeline, the usual first look.
save_csv(), save_figure(), workdir()Write a table or a figure into this notebook’s folder, and say where that is.

Nothing is loaded twice

np, pd and plt in a cell are the same module objects the app already has in memory — the notebook shares them rather than importing its own copies. Starting a notebook adds a few megabytes, and that is your study’s own tables, not libraries.

That validation module is the second reason this belongs inside the app. A “custom” analysis written against a re-implementation of the study’s statistics can quietly disagree with the tab beside it. Written against the same module, it cannot.

The Data button

You do not have to remember any of the table above. Data on the toolbar opens a reference onto your own study and answers the three questions that otherwise cost the first ten minutes:

QuestionWhat the dialog shows
What names can I type?Every name a cell starts with, grouped — the study’s tables, the statistics module, the libraries, the helpers — with a line each. Plus all nine tables under sheets[…].
What is in this table?Every column: its name, its type, a real value out of your study, and what the column actually means.
How big is it?Rows and columns, for this study, now.

Example values are read from your data rather than invented — “what does this actually contain” is the question being asked, and a made-up "sub-01" answers a different one. Copy name puts the selected name on the clipboard ready to paste into a cell (a table comes across as sheets["parameters"], the form that works).

It works before you have run anything

It is a “what can I type” button, so needing a running notebook first would defeat it. Open it on a study you have just loaded and it reads the tables on a background thread to fill in the shapes. It is also not plan-gated — reading what your own results contain is not a paid action.

Running cells

ControlDoes
Run on a cell, or Ctrl+EnterRuns that one cell.
Run allRuns every code cell top to bottom, and stops at the first failure.
InterruptStops the running cell. A Python loop stops promptly; a single long C call — a big torch matmul — finishes first, the same as Jupyter.
RestartForgets every variable and starts fresh. Outputs are cleared with it.
Clear outputsEmpties every cell’s output, keeping the code and the variables.
Add cellA code cell or a text cell. Text cells are Markdown — double-click to edit, Done to render.
DataWhat is in your data — every name, every column, and what each one means. See The Data button.
Save, or Ctrl+SWrites the notebook into the study.

Run all stops where it failed

Continuing past an exception would run every later cell against a state the notebook never reached — the outputs below the failure would be wrong rather than absent, which is harder to notice and worse to act on.

Cells run on a background thread, never the window’s. The app stays responsive while a cell works, and Interrupt is always reachable.

Outputs

Each output lands under its cell in the form that suits it:

The cell producedYou get
print(...)Text, in a monospace block. Standard error is shown in red.
A DataFrame or Series on the last lineA real table. Large frames show the first 200 rows and say how many there were.
Any other value on the last lineIts repr, the same as a notebook anywhere.
A matplotlib figureA chart on the app’s own canvas — see below.
An exceptionA traceback trimmed to your cell, with the kernel’s own frames removed. The session survives it; the next cell still has your variables.

Figures and the right-click menu

A chart you write is a chart the app drew. Figures come back as live matplotlib objects and go onto the same canvas every other chart in BCILattice uses, which means yours gets the same right-click menu without asking for it:

  • Save image — PNG at 300 or 600 dpi, SVG, or PDF
  • Copy to clipboard
  • Zoom and pan, in data space, with reset
  • Transpose, grid, legend and X-label rotation

That is why the notebook runs in the app rather than in a separate process: a picture of a chart cannot be re-exported at 600 dpi or saved as vector, and a figure is the thing most likely to end up in a paper.

You can produce several

Every figure a cell creates is collected, in order, and each gets its own canvas. You do not need plt.show() — it is accepted and ignored.

Reading and writing files

Each notebook has a folder of its own. workdir() returns the path, save_csv(frame, "name.csv") and save_figure(fig, "name.png", dpi=300) write into it and print where they landed, and a relative open("out.csv", "w") resolves there too.

save_csv(results, "scores.csv")
save_figure(fig, "accuracy_by_pipeline.png", dpi=300)
print(workdir())

Reading is unrestricted — point pandas at any file on your machine. Writing outside the notebook’s folder is refused, with a message that says where the folder is. It is a guardrail against a stray path, not a security boundary, and it is documented as one in the code.

Saving, and what is stored

Notebooks are saved inside the study, not as loose files, so they travel with it — export the study, share it with your team, sync it, and the notebooks go along.

Code is stored; outputs are not

A saved notebook keeps its cells and drops its outputs. Two reasons: a study document is not a place for embedded images, and a chart persisted next to results that have since been retrained is a stale claim presented as a current one. Press Run to see it against the data as it stands. Exporting to .ipynb can include outputs — a file you hand to someone should show what it showed.

Templates

New notebook offers a blank one and four templates. Each is a whole small notebook that runs unchanged against any study — none of them hard-codes a metric or a pipeline name, because a template that raises on its first cell teaches you the feature is broken.

TemplateCovers
Explore the resultsWhat each column of results means, what the study actually measured, and the first cuts of it.
Custom graphA bar chart with error bars, the same data as per-subject lines, and a seaborn box-and-strip plot — all on the app canvas, then saved at print resolution.
Custom result validationThe app’s own validate() driven from a cell, a paired comparison written by hand with an effect size and CI, and a pass-threshold criterion no omnibus test answers.
Reshape and re-exportLong to wide, joining scores to the settings that produced them, a publication-ready mean ± SD table, and writing it all back out as CSV.

Worked Examples

results and df are the same table — use whichever name you prefer; these examples use results.

A chart the app does not draw

Average within an observation unit before plotting — fourteen subjects × three pipelines is 42 numbers but not 42 independent ones — and show the spread, because a bar with no error bar is a claim with no evidence.

metric = results["metric"].value_counts().index[0]
subset = results[results["metric"] == metric]

per_subject = subset.groupby(["pipeline", "subject"])["value"].mean().reset_index()
summary = per_subject.groupby("pipeline")["value"].agg(["mean", "std", "count"])

fig, ax = plt.subplots(figsize=(7, 4.2))
ax.bar(summary.index, summary["mean"], yerr=summary["std"].fillna(0),
       capsize=5, color="#14b8a6", edgecolor="none")
ax.set_ylabel(metric)
ax.set_title(f"{metric} by pipeline (mean ± SD over {summary['count'].iloc[0]} subjects)")
ax.spines[["top", "right"]].set_visible(False)
fig.tight_layout()
fig

A validation of your own

The Validation tab’s full analysis, driven from a cell — design selection, assumption checks, omnibus test, cross-check, corrected pairwise family and an APA line:

observations = validation.collect_observations(experiments)
metric = validation.available_metrics(observations)[0]

result = validation.validate(
    observations,
    metric=metric,
    group_factors=["pipeline"],    # what is being compared
    unit_factors=["subject"],      # what an independent observation is
    design="auto",                 # paired when the design supports it
    alpha=0.05,
)
print(validation.report_text(result))

Or the test written out by hand, when you want one the tab does not offer:

from math import sqrt

wide = (results[results["metric"] == metric]
        .groupby(["subject", "pipeline"])["value"].mean()
        .unstack("pipeline").dropna())

a, b = wide.columns[:2]
diff = wide[a] - wide[b]
n = len(diff)
t, p = stats.ttest_rel(wide[a], wide[b])
half = stats.t.ppf(0.975, n - 1) * diff.std(ddof=1) / sqrt(n)

print(f"{a} vs {b}")
print(f"  mean difference {diff.mean():+.4f}  95% CI [{diff.mean() - half:+.4f}, {diff.mean() + half:+.4f}]")
print(f"  t({n - 1}) = {t:.2f}, p = {validation.format_p(p)}, n = {n}")

Reshape and re-export

# one row per subject, one column per pipeline
wide = (results[results["metric"] == metric]
        .groupby(["subject", "pipeline"])["value"].mean()
        .unstack("pipeline").round(4))

# a publication table
summary = (results[results["metric"] == metric]
           .groupby(["pipeline", "subject"])["value"].mean().reset_index()
           .groupby("pipeline")["value"].agg(["mean", "std", "count"]))
table = summary.apply(
    lambda r: f"{r['mean']:.3f} ± {r['std']:.3f} (n={int(r['count'])})", axis=1)

save_csv(wide.reset_index(), f"{metric}_wide.csv")

Jupyter, Colab and .ipynb

The notebook format here is the Jupyter format — .ipynb v4, nothing invented. A notebook written in BCILattice opens in Jupyter, Colab, VS Code, nbviewer and GitHub; a notebook written in any of those opens here.

DirectionHowWhat happens
OutNotebook file → Export as .ipynb…You are asked whether to include the outputs shown on screen. Include them to hand someone a notebook that already shows its results; leave them out for a clean file that runs from scratch.
InNotebook file → Import a .ipynb…The cells come in. Stored outputs are dropped — they were produced by a run this app never saw, against data it cannot check, and showing them under the cells would present someone else’s results as your study’s.

Every results export also ships an analysis.ipynb for the same reason: nothing about this feature should make your work harder to take somewhere else.

Safety and Limits

A notebook runs real Python with the same access as the rest of the app. Four things follow from that, and each is handled rather than assumed away:

ConcernWhat the app does
Nothing runs on its ownNot on page load, not when a study opens, not on a tab switch, and not because a notebook arrived inside an imported study. A cell runs when you press Run and at no other time.
A notebook from someone elseNotebooks record the machine that wrote them. One that arrived with an imported study shows a banner saying so, and the first Run asks you to confirm you have read the code.
A cell that will not stopCells never run on the window’s thread, so the app stays usable. Interrupt raises inside the running cell.
An accidental writeWrites are confined to the notebook’s own folder. Reads are not restricted.

Read imported notebooks before running them

The origin banner is a prompt to look, not a scanner. A notebook that came with a study from outside your lab should be read the same way you would read any script somebody sent you.

Other limits worth knowing:

  • One cell runs at a time; Run all queues the rest.
  • A cell’s printed output is capped at 200,000 characters, and a displayed table at the first 200 rows and 60 columns — a runaway loop is a normal mistake, and turning it into hundreds of megabytes of window is not a normal consequence.
  • Restarting or switching study discards every variable. Notebook code is never lost by either.

Troubleshooting

SymptomCause and fix
The Run buttons are disabled and a banner mentions ResearcherRunning in the app is a Researcher feature. Everything else on the tab still works — write the notebook, export it as .ipynb, and run it in your own Jupyter.
results is emptyThe study has no stored scores yet. Train a run, or check the Results tab — if it shows “no results yet”, there is nothing for the notebook to read either.
The Excel option is greyed outThe optional openpyxl package is not installed. Use the CSV folder or zip — the same tables, same rows.
PermissionError writing a fileThe path is outside the notebook’s folder. Use save_csv / save_figure, or a path under workdir().
ImportError for seaborn or torchThose are imported on first use. The message names the package and the pip install line for it.
A cell hangsPress Interrupt. If the cell is inside one long native call it finishes that first; Restart after it returns.
My chart looks blankA figure with no axes drawn on it is treated as a step rather than a result and is not shown. Plot something onto it.
Outputs vanished after reopening the studyExpected — stored notebooks keep code, not outputs. Press Run all.
Excel shows mangled subject idsShould not happen: CSVs are written with a UTF-8 byte-order mark for exactly this. If it does, open the file with UTF-8 encoding explicitly.

Related

Results Validation for the built-in statistics tab this notebook shares its module with, Custom blocks for running your own Python as a pipeline step instead of as an analysis, and Extensions for bringing a whole third-party library into the palette.
Results Export & Notebook v1.0 · BCINexus Platform · 2026-09-05