Skip to content

Repository files navigation

🧠 BBDC 2026 – Detection of Age-Related Diseases

Welcome to my official repository for Team Solo Leveling! I am thrilled to share that I took first place on the leaderboard at the Bremen Big Data Challenge 2026 (BBDC). The competition was hosted by Universität Bremen and the Cognitive Systems Lab (CSL), and my winning submission achieved a Macro-F1 score of 0.4213.

BBDC 2026 leaderboard

This project focuses on multi-class classification. I take physiological signals—specifically EEG, ECG, and EDA—and classify them into one of seven age-related impairment categories.

🏆 Competition metric: Macro-F1 (the unweighted, macro-averaged F1 score across all seven classes).

💡 The TL;DR: What Actually Worked

If you just want the winning formula, look no further than my tabular AutoGluon pipeline. To get my top leaderboard result, I combined:

  • Official and custom-engineered features
  • Optional ROCKET features
  • KS (Kolmogorov–Smirnov) filtering
  • Quantile preprocessing
  • AutoGluon's best_quality preset
  • Test-Time Augmentation (TTA)
  • Optional pseudo-labeling

A quick note: I also built a deep learning (DL) path (train_dl). While it’s robust, it didn't end up beating the tabular approach for my final submission. I left it in the repo because it's interesting, but consider it strictly experimental/optional.


🩺 The 7 Impairment Classes

Here are the specific categories I am predicting from the physiological segments:

ID Name
0 no_impairment
1 glaucoma
2 macular_degeneration
3 cognitive_decline
4 hand_tremors
5 diabetes
6 parkinsons

🗂️ Repository Layout

Here is a map of the project so you can easily find your way around:

project/
├── pipeline/               ← Main pipeline package (the core logic lives here)
│   ├── config.py           ← ALL hyperparameters live here in one place
│   ├── data.py             ← Shared data loading and preprocessing
│   ├── preprocess.py       ← Entry-point: caches tensors and extracts features
│   ├── train_gbdt.py       ← GBDT ensemble (LightGBM + XGBoost), optional blend
│   ├── train_dl.py         ← DL ensemble (experimental; not used for best LB)
│   ├── model_dl.py         ← DL model architecture and losses
│   ├── train_autogluon.py  ← AutoGluon — the primary winning path!
│   └── ensemble.py         ← Optional blend of tabular + DL soft probabilities
│
├── scripts/
│   ├── compute_feature_importance.py  ← LightGBM gains importance from the AutoGluon
│   └── plot_feature_importance.py     ← Creates a bar chart from the JSON above
├── results/                ← Optional JSON summaries (e.g., feature importance rankings)
│
├── models/                 ← Trained AutoGluon stores (git-ignored except .gitkeep)
│   └── autogluon_models/   ← Default path set in pipeline/config.py
│
├── src/
│   └── features/           ← Feature engineering scripts
│       ├── cache_raw_data.py       ← Caches raw EEG/ECG/EDA as .pt tensors
│       ├── extract_features.py     ← Creates hand-crafted features per segment
│       └── extract_rocket_features.py ← Optional ROCKET features (merged with --use-rocket)
│
├── submissions/ ← Archived autogluon_* CSV/NPY copies per run (optional)
├── train/       ← Competition training data (not in git)
├── test/        ← Competition test data + output submissions (not in git)
├── logs/        ← Slurm .out/.err files (git-ignored)
└── requirements.txt

🚀 Quick Start (The Validated AutoGluon Path)

Ready to run the code? Let's walk through reproducing the winning score step-by-step.

1. Set Up Your Environment

First, create a virtual environment and install the required packages:

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Crucial Step: The base requirements.txt might not include AutoGluon, which is absolutely required for the winning path. Make sure to run:

pip install "autogluon.tabular[all]"

2. Preprocess the Data (Run Once)

To extract everything you need from the raw data, just run:

python -m pipeline.preprocess

What’s happening behind the scenes? This runs two stages sequentially:

  1. src/features/cache_raw_data.py: This reads the raw data and creates raw_train_tensor.pt and raw_test_tensor.pt. These are dense (N, 18, 2500) float tensors (plus ids), which are needed if you want to use ROCKET or any raw-waveform scripts.
  2. src/features/extract_features.py: This calculates hand-crafted metrics for each segment from data.csv, spitting out train_engineered_features.csv and test_engineered_features.csv.

Pro-tip: If you only want to run one of these, you can pass the --tensors-only or --features-only flags.

Optional — ROCKET Features: If you want to train AutoGluon using the --use-rocket flag, you'll need to generate those high-dimensional features first (make sure the tensors exist before running this!):

python -m src.features.extract_rocket_features

(This generates rocket_train_features.csv and rocket_test_features.csv)

3. Train AutoGluon (The Main Event)

Time to train the model! Choose your command based on your hardware:

# Typical local run (Uses defaults from config.py, like a 2h time limit)
python -m pipeline.train_autogluon

# Cluster / long run (Gives it a larger budget, more bag folds, and more TTA iterations)
python -m pipeline.train_autogluon --server

Where are my predictions? Check the test/ folder for your submission files:

  • autogluon_tta_submission.csvHighly Recommended. Created when TTA (Test-Time Augmentation) is on (which is the default). It averages out predictions over noisy versions of the test data for better stability.
  • autogluon_submission.csv — Created if you explicitly pass --no-tta.
  • autogluon_pseudo_submission.csv — Created if you use --pseudo-label. (Note: If both are on, TTA uses the last pseudo-round predictor).

Just pick the CSV whose generation settings you trust the most and submit it!

4. Optional Extras: GBDT and Ensembling

You do not need to run these to get my best score. But if you're curious and want to experiment:

python -m pipeline.train_gbdt        # Takes ~30 min on a CPU
python -m pipeline.ensemble          # Blends available *.npy files under test/

The ensemble script looks for autogluon_tta_proba.npy, autogluon_proba.npy, gbdt_proba.npy, dl_proba.npy, etc., and blends them into test/ensemble_submission.csv. (If you only have AutoGluon files, it just uses the AutoGluon weights).


⚙️ Configuration Cheat Sheet

I hate digging through code for variables as much as you do. All shared constants live in pipeline/config.py. Here's what they do:

Parameter Default Used by What it does
KS_THRESHOLD 0.60 Tabular The maximum allowed two-sample KS statistic between train and test distributions. Columns above this are dropped to avoid covariate shift.
MIN_FEATURES (config) Tabular A safety net: if too many columns get dropped by the KS filter, I force it to keep at least this many (the ones with the lowest KS).
QUANTILE_N (config) Tabular Sets the output resolution for the QuantileTransformer (which maps data to a Gaussian shape).
MODELS_DIR models/ AutoGluon The main folder where predictors are saved.
AUTOGLUON_MODEL_DIR models/autogluon_models AutoGluon The specific directory for the fit output (wiped clean before each full fit).
AUTOGLUON_TIME_LIMIT 7200 AutoGluon Default fit time (in seconds). Using --server swaps this to AUTOGLUON_SERVER_TIME_LIMIT (e.g., 8 hours).
AUTOGLUON_SERVER_* varies AutoGluon Server mode tweaks: bag folds, stack depth, and TTA iteration counts.
FOCAL_GAMMA, P2_LAMBDA_* DL only Parameters for Focal loss, CORAL, and DANN-style terms in train_dl and model_dl.
Pseudo-labeling CLI flags AutoGluon Controlled via command line: --pseudo-label, --pseudo-rounds (default is 2), and thresholds.

🧩 Algorithm Overview: How It Works

The Goal: Classify each 10-second segment (row) into an impairment_type (0 through 6). Because the metric is macro-F1, rare conditions are just as important to get right as common ones.

Step 1: Building the Feature Matrix (load_tabular_features)

  1. Load the official features.csv for both train and test.
  2. Left-join my custom train/test_engineered_features.csv using the ID.
  3. Merge the ROCKET CSVs (if --use-rocket is flagged).
  4. Pull in the training labels from labels.csv. (Result: A massive table with hundreds to thousands of columns of data).

Step 2: Tabular Preprocessing (preprocess_features)

Important: All transformations are "fit" on the train data only to prevent data leakage. The test data is just transformed.

  1. Variance filter: Throws out columns with almost zero variance to keep the models stable.
  2. Kolmogorov–Smirnov (KS) filter: Checks how much a feature's distribution changes between the train and test sets. If the shift is too big (KS > KS_THRESHOLD), the feature is dropped so the model doesn't learn artifacts. (It respects the MIN_FEATURES safety net).
  3. QuantileTransformer (Gaussian): Fits on the training data (QUANTILE_N quantiles) and applies to both. This squashes weirdly scaled data into a normal Gaussian distribution, which tree models and neural networks love. Any missing values are patched up (e.g., nan_to_num) afterward.

Step 3: Optional Training Tweaks

You can adjust how the model sees the training data using command-line flags:

  • --use-artifacts: Looks at artifact_report.csv and drops training segments that have flat EEG channels. It also reduces the weight of segments that look too Gaussian or have high spectral flatness.
  • --use-stages: Trains a LightGBM model to predict the impairment severity stage. It adds an out-of-fold pred_stage column to the training data, and direct predictions to the test data.
  • --use-weights: Turns on adversarial weighting. It trains a logistic regression model to tell train rows apart from test rows. Training rows that "look" like the test set get a higher weight, forcing the main model to focus on the most relevant data.

Step 4: The AutoGluon Magic (TabularPredictor)

  • I set problem_type='multiclass' and eval_metric='f1_macro' to match the competition.
  • I use presets='best_quality', which tells AutoGluon to build a massive stack of gradient boosting, random forests, extra trees, kNNs, etc.
  • It uses num_bag_folds and num_stack_levels to build highly stable meta-models using out-of-fold predictions.
  • Speed tweaks: FASTAI and NN_TORCH models are disabled by default because they are slow on super-wide datasets (you can turn them on with --include-nn or --nn-only). I also use fold_fitting_strategy: 'sequential_local' so I don't crash shared clusters by hogging all the CPUs/GPUs at once.
  • The score_val you see on the leaderboard is the out-of-fold macro-F1.

Step 5: Final Polish (Pseudo-labeling & TTA)

  • Pseudo-labeling (--pseudo-label): The model predicts on the test set, finds the ones it is super confident about (≥ --pseudo-threshold, capped by --pseudo-balance), and adds them back into the training data with a weight of 0.5. Then, it quickly refits a new model.
  • Test-time Augmentation (TTA): Unless you pass --no-tta, the model creates slightly altered versions of the test data by adding tiny amounts of Gaussian noise (scale 0.01). It predicts on all these noisy versions for tta_n iterations and averages the results to smooth out its final decisions.

🔬 Datasets, Signals, and Feature Engineering

Each data segment is a 10-second multi-channel recording sampled at 250Hz (18 channels × 2500 samples). The channels include 16 EEG lines (EEG_CZEEG_OZ), 1 ECG, and 1 EDA. Note: The training classes are highly imbalanced, which is why weighting and macro-F1 optimization were so critical.

The Feature Engineering Pipeline (3 Stages)

Stage 1: Raw Tensor Cache (src/features/cache_raw_data.py)

This script reads the data.csv files, groups everything by ID, and pads or truncates the traces so they are exactly 2500 samples long. It saves them as raw_train_tensor.pt and raw_test_tensor.pt. (You only need to do this if you are using ROCKET or Deep Learning).

Stage 2: Hand-Crafted Engineered Features (src/features/extract_features.py)

This is where I extract ~500–1000+ domain-specific metrics per segment. Here is exactly what I pull out:

  • Per EEG channel (16 channels): * Welch PSD band powers (δ, θ, α, β, γ) as absolute and relative values.
    • Band ratios (like θ/α, θ/β, α/β, δ/β).
    • Hjorth activity, mobility, and complexity.
    • Time-domain stats: mean, std, skew, kurtosis, zero-crossing rate, line length, RMS, peak-to-peak, mean absolute amplitude.
    • Spectral entropy and permutation entropy.
    • Median and peak frequency.
    • θ–γ PAC proxy (Phase-Amplitude Coupling).
    • DWT (db4) normalized energies per level.
    • 1/f PSD slope (1–40 Hz).
    • Hurst exponent and DFA α.
    • Individual alpha frequency (IAF) and peak alpha amplitude.
    • SEF50 / SEF95 (Spectral Edge Frequency).
    • Teager–Kaiser (TKEO) energy.
    • Lempel–Ziv (LZC) complexity.
    • Trimmed mean/std and stationarity (how much the sub-segment means and stds vary).
  • Sample entropy: Calculated on a smaller subset of channels (EEG_CZ, EEG_FZ, EEG_OZ, ECG, EDA) to save processing time.
  • ECG: * Basic morphology stats and Hjorth parameters.
    • Spectral entropy, median/peak frequency.
    • R-peak detection leading to time-domain HRV (heart rate, RR intervals, RMSSD, SDNN, pNN50/20, Poincaré SD1/SD2).
    • LF/HF ratio from the detrended RR tachogram (interpolated to 4 Hz + Welch).
    • Raw-signal band powers, wavelet energies, autocorrelations, TKEO, SEF95, stationarity.
    • Extra autocorrelation lags and PSD slope.
  • EDA: * Moments, range, and SCL (via low-pass slow component).
    • Linear trend (slope, R²).
    • Derivative / SCR features (number of peaks, slopes).
    • Hjorth, spectral entropy, TKEO, stationarity, LZC, Hurst, autocorrelation lags.
    • Recovery half-time after the maximum peak.
  • Cross-channel EEG: * Hemispheric asymmetry: Calculated as (R-L)/(R+L) per band for paired electrodes.
    • Frontal vs occipital band ratios.
    • Global Field Power (GFP) statistics.
    • Mean/std of pairwise channel correlations and band power across all EEG channels.
    • Coherence across selected pairs and bands.
  • Composites / Domain Proxies: * Occipital vs global α/β, motor cortex β/γ ratios, frontal θ vs parietal α.
    • Interaction features (visual×motor, motor×cognitive, etc.).
    • ECG/EDA × proxy products.
    • Severity metrics (EEG vs peripheral energy ratio, cross-channel band variability).
  • Temporal Windows: I take five 2-second sub-windows on key channels for α, β, and θ to calculate variability (std, CV), trend slope, and the range of band power.
  • Additional Dynamics: Hurst, DFA, autocorrelation, and PSD slope on many EEG channels; percentiles (p5, p25, p75, p95, IQR) on selected channels; and PLV and PLI for key pairs in α, β, θ.
  • ECG–EDA Cross-Modal: RMS and std ratios, Hilbert envelope correlation, relative energy.

(Note: extract_features.py produces about ~986 engineered columns per segment. The optional ROCKET columns (rkt_*) are generated separately by extract_rocket_features.py and are only added if you use --use-rocket).

Stage 3: ROCKET Features (src/features/extract_rocket_features.py)

If enabled, this applies fixed random 1D convolutions (kernels 7, 9, and 11 with various dilations) across all 18 z-scored channels. It extracts max pooling and the Proportion of Positive Values (PPV) per map. Using the default NUM_KERNELS = 10_000 creates about ~20,000 highly shift-resistant numeric columns!

(Note: Hand-crafted features use raw amplitudes, but ROCKET uses z-scored channels. Tabular paths apply QuantileTransformer on the final merged columns).


🏗️ End-to-End Architecture

Here is a visual map of how data flows from the raw files all the way to my winning submission.

The Data Flow

flowchart TD
    subgraph inputs [Raw competition data]
        TR[train/*/ time series + labels]
        TE[test/ segments]
    end

    subgraph fe [Feature construction]
        PT[raw_*_tensor.pt optional cache]
        ENG[train/test_engineered_features.csv]
        OFF[Official features.csv per split]
        RKT[rocket_*_features.csv optional]
    end

    subgraph tab [Shared tabular pipeline]
        MERGE[load_tabular_features merges OFF + ENG + RKT]
        ART[Optional artifact filter flat drop gaussian downweight]
        STG[Optional pred_stage from impairment_stage]
        PP[variance filter + KS train vs test + QuantileTransformer fit on train only]
        ADV[Optional adversarial sample weights]
    end

    subgraph ag [AutoGluon path — primary]
        AG[TabularPredictor best_quality bagging stacking]
        PL[Optional pseudo-label rounds refit]
        TTA[Test-time augmentation noisy features]
        OUT[test/autogluon_*.csv + .npy]
    end

    subgraph alt [Optional extras]
        GBDT[train_gbdt LightGBM XGBoost]
        DL[train_dl CNN Transformer — experimental]
    end

    TR --> PT
    TR --> ENG
    TE --> ENG
    TR --> OFF
    TE --> OFF
    RKT -.-> MERGE
    MERGE --> ART --> STG --> PP
    PP --> GBDT
    PP --> ADV
    ADV --> AG
    AG --> PL --> TTA --> OUT
    PT -.-> DL
    OUT --> ENS[ensemble optional]
    GBDT --> ENS
    DL -.-> ENS
    ENS --> SUB[ensemble_submission.csv optional]
Loading

AutoGluon ensemble (example trained run): model graph / stack produced by TabularPredictor with best_quality bagging and stacking.

AutoGluon ensemble: stacked bagged models

AutoGluon Deep Dive & Grouped Bagging

AutoGluon gets fed one giant DataFrame. Every row gets a _weight (which might combine artifact, adversarial, or pseudo weights) and potentially a _cv_group. If you use --group-val, I group the data by segment IDs (--group-strategy id) or PCA+KMeans clusters (cluster). AutoGluon uses these groups (groups='_cv_group') to make sure its bagging process doesn't overfit on dependent segments.

All files are neatly backed up using archive_submissions, which saves test/autogluon_*.{csv,npy} and run information to submissions/<dated_job_tag>/. Models are safely stored in models/ (which is git-ignored).

The "Road Not Taken": Deep Learning (MultiScaleModelV4)

Even though it didn't make my best submission, my experimental DL model (pipeline/model_dl.py) is pretty cool. It includes:

  • Three parallel CNN branches (kernels 7 / 31 / 63) to capture different temporal scales.
  • A 4-layer Transformer Encoder (pre-norm, GELU, d_model=256).
  • A 3-layer residual MLP for tabular data, combined with sequence embeddings via gating.
  • It uses contrastive pretraining, then supervised training (with Mixup, CORAL / SWD / DANN-style alignment, and focal loss) with TTA and temperature scaling. Output: test/dl_proba.npy.

GBDT & Final Blend

  • train_gbdt.py: Runs a standard 5-fold LightGBM + XGBoost setup using the exact same preprocessing path as AutoGluon.
  • pipeline/ensemble.py: Averages out the probabilities from AutoGluon, DL, and GBDT (defaulting to heavily favor TTA AutoGluon).

But honestly? You can skip the ensemble and just submit autogluon_tta_submission.csv!


Reference cluster run & feature importance

Example run

Curious what a full run looks like? Here are the specs from one full server-mode training using scripts/run_autogluon.sh. Note that this run rebuilt engineered features and cached ROCKET tensors (the default train_autogluon command doesn't pass --use-rocket, so this specific run had ~977 preprocessed columns comprising official + engineered + pred_stage). It also used adversarial sample weighting (--use-weights) and pred_stage (--use-stages).

Item Value
Host / GPU gpu-a3090-01 (NVIDIA A3090)
Engineered features (per segment) 986 columns (train_engineered_features.csv)
Train / test segments 7,287 / 611
Preprocessing Variance filter: 90 near-zero columns dropped; KS ≤ 0.5; 977 columns after KS + pred_stage
Fit budget 8 h (time_limit=28800 s), 10 bag folds, 1 stack level
Resources 16 CPUs, 1 GPU (AutoGluon sequential_local folding)
CV / leaderboard (base fit) Macro-F1 ≈ 0.6746 (WeightedEnsemble_L3 top; strong LightGBM / XGBoost stackers)
Pseudo-labeling Round 1: 306 / 611 test rows ≥ 0.85 → CV 0.6894; Round 2: 300 → CV 0.6922
TTA 60 noisy averages (Gaussian noise σ = 0.01 on the preprocessed feature matrix)
Outputs archived submissions/20260327_job21529_cvf1-0.6746_8h_10bag_1stack_advw_stages/

The log prints out diagnostics (like max-probability confidence and prediction entropy) so you can compare the base model, pseudo-labeled model, and TTA-averaged probabilities. Again, your best bet is usually test/autogluon_tta_submission.csv.

LightGBM feature importance (gain)

Because AutoGluon mixes so many models together, the easiest way to see which features actually matter is to look at the mean LightGBM gain across bag folds for the LightGBM_BAG_L1 model (stack level 1). This tells us which raw inputs the trees found most useful.

To generate this (once you have a trained model in models/):

python scripts/compute_feature_importance.py --model-dir models/autogluon_models
python scripts/plot_feature_importance.py

This creates a JSON file results/feature_importance_lgb_l1_gain.json (top 200 rows) and a bar chart results/feature_importance_lgb_l1_gain.png.

The plot below shows the top 35 features by mean gain. I hid pred_stage from the chart so you can actually read the scale, but it ranks #1 in the JSON if --use-stages is enabled! This data includes official, engineered, and ROCKET features. Keep in mind, "Gain" just means how much a tree split improved the model; it is not permutation importance for the whole ensemble.

LightGBM_BAG_L1 mean gain — top 35 features (pred_stage omitted)

Looking at the top entries, the model relies heavily on EDA (percentiles, TKEO, derivatives, SCL), coherence and asymmetry (δ/γ across central and temporal sites), composite stress/cognition proxies, and occipital / frontal EEG (like PO8 γ, F3 θ, and Hurst). If you want to see the massive pred_stage bar, just run the plot script with --include-pred-stage.


Acknowledgements

A massive thank you to Universität Bremen, the Cognitive Systems Lab (CSL), and the BBDC 2026 organizers and staff for putting together such a fantastic challenge, providing the data, and supporting all the participants!


Citation

If you found this repository or approach helpful in your own work, I'd appreciate a citation!

@misc{alasti2026bbdc_physiological_signals,
  title        = {Detection of Age-Related Diseases Based on Multiple Physiological Signals ({BBDC} 2026)},
  author       = {Alasti, Amirreza},
  year         = {2026},
  howpublished = {\url{https://github.com/amirrezaalasti/detection-of-age-related-diseases-based-on-multiple-physiological-signals}},
  note         = {Team Solo Leveling; 1st place BBDC 2026 leaderboard (Macro-F1 0.4213)}
}

Author: Amirreza Alasti — GitHub @amirrezaalastiamirreza.alasti@stud.uni-hannover.de
Team (BBDC 2026): Solo Leveling

About

Bremen Big Data Challenge 2026

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages