Competition submissions (PNPL 2026)#
pnpl.competition turns your model’s predictions into a valid PNPL 2026 Kaggle
submission and uploads it. There are three pieces:
LibriBrainCompetitionHoldout— downloads the holdout MEG and enumerates the canonical rows of a submission (one row per word).write_submission— writes those rows + your probabilities to CSV in the exact leaderboard format.submit_to_kaggle— uploads the CSV via the Kaggle CLI.
The submission format#
index, <50 primary-vocab probs>, moses_<word> × 50
The first 50 probability columns are a distribution over the competition vocabulary (
load_vocabulary("primary")), in vocab order.The next 50 columns are a distribution over the Moses-50 vocabulary (
load_vocabulary("moses")), each prefixedmoses_.Scoring is Top-10 Balanced Accuracy over the primary distribution; the Moses block is tracked as a secondary metric. Provide full probability distributions (not argmax) so Top-10 can be computed.
The holdout data#
The holdout lives in the public dataset
pnpl/LibriBrain-Competition-2026
(COMPETITION_HOLDOUT/). Each of the 40 subjects (subj00–subj39) has two
files:
file |
contents |
|---|---|
|
sentence-epoched MEG |
|
isolated 1 s word epochs |
LibriBrainCompetitionHoldout expands both into (306, 250) windows (1 s @
250 Hz): for the sentence source it cuts a 1 s window at each word onset; for the
word source it uses the stored epoch directly.
Tracks#
track |
subjects |
meaning |
|---|---|---|
|
subject 0 |
within-subject decoding |
|
subjects 1–39 |
cross-subject generalisation |
Each track is submitted as its own CSV.
Canonical row order#
The loader is the source of truth for index. Rows are ordered:
subjects ascending → per subject the sentence source then the word source →
sentences/epochs in stored order → words within a sentence in stored order. Every
valid word (word_mask) becomes a row. Always pass holdout.indices to
write_submission and never reorder your predictions.
End-to-end#
import numpy as np
from pnpl.competition import (
LibriBrainCompetitionHoldout, write_submission, submit_to_kaggle,
)
holdout = LibriBrainCompetitionHoldout(track="deep") # subj00; downloads on first use
print(len(holdout), holdout.counts()) # 960 {'total':960,'sentence':868,'word':92}
primary, secondary = [], []
for meg, metas in holdout.iter_windows(batch_size=256): # meg: (B, 306, 250)
p, m = my_model(meg) # -> (B, 50), (B, 50)
primary.append(p); secondary.append(m)
csv_path = write_submission(
"submission_deep.csv",
indices=holdout.indices,
primary_probs=np.concatenate(primary),
secondary_probs=np.concatenate(secondary),
)
submit_to_kaggle(csv_path, competition="<slug>", message="baseline")
iter_windows() keeps only one subject’s MEG in memory at a time, so the large
broad track streams comfortably. For random access / a PyTorch DataLoader,
holdout[i] returns (window, meta); use shuffle=False at inference so the
one-file cache stays warm.
Kaggle auth#
submit_to_kaggle shells out to the official Kaggle CLI, so any standard auth
works: KAGGLE_API_TOKEN (modern KGAT_… token, CLI ≥ 2.0),
KAGGLE_USERNAME/KAGGLE_KEY, or ~/.kaggle/kaggle.json.
See the runnable examples/submit_pnpl2026.ipynb.