> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nimbusbci.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bayesian LDA

> Bayesian LDA with shared covariance for fast motor-imagery BCI inference (10-15ms), including calibrated uncertainty and posterior probabilities.

# Bayesian LDA - Bayesian Linear Discriminant Analysis

**Python**: `NimbusLDA` | **Julia**: `NimbusLDA`\
**Mathematical Model**: Pooled Gaussian Classifier (PGC)

Bayesian LDA is a Bayesian classification model with uncertainty quantification. It uses a **shared precision matrix** across all classes, making it fast and efficient for BCI classification.

<Note>
  **Available in Both SDKs:**

  * **Python SDK**: `NimbusLDA` class (sklearn-compatible)
  * **Julia SDK**: `NimbusLDA` (RxInfer.jl-based)

  Both implementations provide the same Bayesian inference with uncertainty quantification.
</Note>

## Start Here

<CardGroup cols={3}>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Start with SDK setup and first inference workflow.
  </Card>

  <Card title="Model Selection" icon="box" href="/model-specification">
    Compare Nimbus models by data characteristics and use case.
  </Card>

  <Card title="Examples" icon="braces" href="/examples/basic-examples">
    See practical BCI examples for training and inference.
  </Card>
</CardGroup>

## Overview

Bayesian LDA extends traditional Linear Discriminant Analysis with full Bayesian inference, providing:
✅ **Posterior probability distributions** (not just point estimates)<br />
✅ **Uncertainty quantification** for each prediction<br />
✅ **Probabilistic confidence scores**<br />
✅ **Fast inference** (\<20ms per trial)<br />
✅ **Training and calibration** support<br />
✅ **Batch and streaming** inference modes

## Quick Start

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    import numpy as np

    # Create and fit classifier
    clf = NimbusLDA(mu_scale=3.0)
    clf.fit(X_train, y_train)

    # Predict with uncertainty
    predictions = clf.predict(X_test)
    probabilities = clf.predict_proba(X_test)

    # Online learning
    clf.partial_fit(X_new, y_new)
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    using NimbusSDK

    # Train model
    model = train_model(
        NimbusLDA,
        train_data;
        iterations=50
    )

    # Predict
    results = predict_batch(model, test_data)
    ```
  </Tab>
</Tabs>

### When to Use Bayesian LDA

**Bayesian LDA is ideal for:**

* Motor Imagery classification (2-4 classes)
* Well-separated class distributions
* Fast inference requirements (\<20ms)
* Interpretable results for medical applications
* When classes have similar covariance structures

**Consider [Bayesian QDA](/models/rxgmm) instead if:**

* Classes have significantly different covariance structures
* Complex, overlapping distributions
* Need more flexibility in modeling per-class variances

## Model Architecture

### Mathematical Foundation (Pooled Gaussian Classifier)

Bayesian LDA implements a Pooled Gaussian Classifier (PGC), which models class-conditional distributions with a shared precision matrix:

```
p(x | y=k) = N(μ_k, W^-1)
```

Where:

* `μ_k` = mean vector for class k (learned from data)
* `W` = **shared** precision matrix (same for all classes)
* Assumes classes have similar covariance structure

**Key Assumption**: All classes share the same covariance structure, which makes training and inference faster.

### Hyperparameters

Bayesian LDA supports configurable hyperparameters for optimal performance tuning:

**Available Hyperparameters (training):**

| Parameter              | Type    | Default | Range         | Description                                 |
| ---------------------- | ------- | ------- | ------------- | ------------------------------------------- |
| `dof_offset`           | Int     | 2       | \[1, 5]       | Degrees of freedom offset for Wishart prior |
| `mean_prior_precision` | Float64 | 0.01    | \[0.001, 0.1] | Prior precision for class means             |

**Parameter Effects:**

* **dof\_offset**: Controls regularization strength
  * Lower values (1) → More data-driven, less regularization
  * Higher values (3-5) → More regularization, more conservative

* **mean\_prior\_precision**: Controls prior strength on class means
  * Lower values (0.001) → Weaker prior, trusts data more
  * Higher values (0.05-0.1) → Stronger prior, more regularization

### Model Structure

```julia theme={null}
struct NimbusLDA <: BCIModel
    mean_posteriors::Vector          # MvNormal posteriors for class means
    precision_posterior::Any         # Wishart posterior for shared precision
    priors::Vector{Float64}          # Empirical class priors
    metadata::ModelMetadata          # Model info
    dof_offset::Int                  # Degrees of freedom offset (training)
    mean_prior_precision::Float64    # Mean prior precision (training)
end
```

### RxInfer Implementation

The Bayesian LDA model uses RxInfer.jl for variational Bayesian inference:

**Learning Phase:**

```julia theme={null}
@model function NimbusLDA_learning_model(y, labels, n_features, n_classes, dof_offset, mean_prior_precision)
    # Prior on shared precision
    dof = n_features + dof_offset
    W ~ Wishart(dof, I)
    
    # Priors on class means
    for k in 1:n_classes
        m[k] ~ MvNormal(mean=zeros(n_features), precision=mean_prior_precision * I)
    end
    
    # Likelihood with known labels
    for i in eachindex(y)
        k = labels[i]
        y[i] ~ MvNormal(mean=m[k], precision=W)
    end
end
```

**Prediction Phase (Mixture Likelihood):**

Inference uses the learned posterior distributions as priors in a mixture model:

```julia theme={null}
@model function NimbusLDA_predictive(y, empirical_priors, n_features)
    n_classes = length(empirical_priors)
    
    # Class means and shared precision are initialized from trained posteriors.
    local m
    for k in 1:n_classes
        m[k] ~ MvNormal(mean=zeros(n_features), precision=I)
    end

    local p
    for k in 1:n_classes
        p[k] ~ Wishart(n_features + 2, I)
    end
    
    z ~ Categorical(empirical_priors)
    for i in eachindex(y)
        y[i] ~ NormalMixture(switch = z, m = m, p = p)
    end
end
```

The trained mean and precision posteriors are supplied through RxInfer initialization so uncertainty is carried into prediction.

## Usage

### 1. Load Pre-trained Model

<Note>
  **Python SDK**: The Python SDK (`nimbus-bci`) trains models locally and doesn't require pre-trained model loading. See [Python SDK Quickstart](/python-sdk/quickstart) for training examples.
</Note>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    import pickle

    # Python SDK: Train your own model locally
    # No authentication or model zoo needed

    # Quick training example
    clf = NimbusLDA(mu_scale=3.0)
    clf.fit(X_train, y_train)

    # Save for later use
    with open("my_motor_imagery.pkl", "wb") as f:
        pickle.dump(clf, f)

    # Load saved model
    with open("my_motor_imagery.pkl", "rb") as f:
        clf = pickle.load(f)

    print("Model info:")
    print(f"  Classes: {clf.classes_}")
    print(f"  Features: {clf.n_features_in_}")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    using NimbusSDK

    # Authenticate
    NimbusSDK.install_core("nbci_live_your_key")

    # Load from Nimbus model zoo
    model = load_model(NimbusLDA, "motor_imagery_4class_v1")

    println("Model loaded:")
    println("  Features: $(get_n_features(model))")
    println("  Classes: $(get_n_classes(model))")
    println("  Paradigm: $(get_paradigm(model))")
    ```
  </Tab>
</Tabs>

### 2. Train Custom Model

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    from nimbus_bci.compat import extract_csp_features
    import mne
    import pickle

    # Load and preprocess EEG data
    raw = mne.io.read_raw_gdf("motor_imagery.gdf", preload=True)
    raw.filter(8, 30)

    events = mne.find_events(raw)
    event_id = {'left': 1, 'right': 2, 'feet': 3, 'tongue': 4}
    epochs = mne.Epochs(raw, events, event_id, tmin=0, tmax=4, preload=True)

    # Extract CSP features
    csp_features, csp = extract_csp_features(epochs, n_components=8)
    labels = epochs.events[:, 2]

    # Train NimbusLDA model with default hyperparameters
    clf = NimbusLDA()
    clf.fit(csp_features, labels)

    # Or train with custom hyperparameters
    clf_tuned = NimbusLDA(
        mu_scale=3.0,        # Prior strength for means (default: 1.0)
        class_prior_alpha=1.0
    )
    clf_tuned.fit(csp_features, labels)

    # Save for later use
    with open("my_motor_imagery.pkl", "wb") as f:
        pickle.dump(clf_tuned, f)

    print("Model trained successfully!")
    print(f"Training accuracy: {clf_tuned.score(csp_features, labels):.1%}")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    using NimbusSDK

    # Prepare training data with labels
    train_features = csp_features  # (16 × 250 × 100)
    train_labels = [1, 2, 3, 4, 1, 2, ...]  # 100 labels

    train_data = BCIData(
        train_features,
        BCIMetadata(
            sampling_rate = 250.0,
            paradigm = :motor_imagery,
            feature_type = :csp,
            n_features = 16,
            n_classes = 4,
            chunk_size = nothing
        ),
        train_labels  # Required for training!
    )

    # Train NimbusLDA model with default hyperparameters
    model = train_model(
        NimbusLDA,
        train_data;
        iterations = 50,        # Inference iterations
        showprogress = true,    # Show progress bar
        name = "my_motor_imagery",
        description = "4-class MI classifier with CSP"
    )

    # Or train with custom hyperparameters (v0.2.0+)
    model = train_model(
        NimbusLDA,
        train_data;
        iterations = 50,
        showprogress = true,
        name = "my_motor_imagery_tuned",
        description = "4-class MI with tuned hyperparameters",
        dof_offset = 2,                    # DOF offset (default: 2)
        mean_prior_precision = 0.01        # Prior precision (default: 0.01)
    )

    # Save for later use
    save_model(model, "my_model.jld2")
    ```
  </Tab>
</Tabs>

**Training Parameters:**

* `iterations`: Number of variational inference iterations (default: 50)
  * More iterations = better convergence but slower training
  * 50-100 is typically sufficient
* `showprogress`: Display progress bar during training
* `name`: Model identifier
* `description`: Model description for documentation
* `dof_offset`: Degrees of freedom offset (default: 2, range: \[1, 5])
* `mean_prior_precision`: Prior precision for means (default: 0.01, range: \[0.001, 0.1])

### 3. Subject-Specific Calibration

Fine-tune a pre-trained model with subject-specific data (much faster than training from scratch):

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    import numpy as np
    import pickle

    # Load baseline model (trained on multiple subjects)
    with open("motor_imagery_baseline.pkl", "rb") as f:
        base_clf = pickle.load(f)

    # Collect 10-20 calibration trials from new subject
    X_calib, y_calib = collect_calibration_trials()  # Your function

    # Personalize using online learning (partial_fit)
    personalized_clf = NimbusLDA()
    personalized_clf.fit(X_baseline, y_baseline)  # Start with baseline

    # Fine-tune on calibration data
    for _ in range(10):  # Multiple passes for adaptation
        personalized_clf.partial_fit(X_calib, y_calib)

    # Save personalized model
    with open("subject_001_calibrated.pkl", "wb") as f:
        pickle.dump(personalized_clf, f)

    print("Model personalized successfully!")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Load base model
    base_model = load_model(NimbusLDA, "motor_imagery_baseline_v1")

    # Collect 10-20 calibration trials from new subject
    calib_features = collect_calibration_trials()  # Your function
    calib_labels = [1, 2, 3, 4, 1, 2, ...]

    calib_data = BCIData(calib_features, metadata, calib_labels)

    # Calibrate (personalize) the model
    personalized_model = calibrate_model(
        base_model,
        calib_data;
        iterations = 20  # Fewer iterations needed
    )

    save_model(personalized_model, "subject_001_calibrated.jld2")
    ```
  </Tab>
</Tabs>

**Calibration Benefits:**

* Requires only 10-20 trials per class (vs 50-100 for training from scratch)
* Faster: 20 iterations vs 50-100
* Better generalization: Uses pre-trained model as prior
* Typical accuracy improvement: 5-15% over generic model
* **Hyperparameters automatically preserved**: `calibrate_model()` inherits training hyperparameters from the base model (`dof_offset`, `mean_prior_precision`) ensuring consistency

<Note>
  **Important**: You don't need to specify hyperparameters when calibrating - they are automatically inherited from the base model. This ensures the calibrated model uses the same regularization strategy that worked well during initial training.
</Note>

### 4. Batch Inference

Process multiple trials efficiently:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    import numpy as np

    # Run batch inference
    predictions = clf.predict(X_test)
    probabilities = clf.predict_proba(X_test)
    confidences = np.max(probabilities, axis=1)

    # Analyze results
    print(f"Predictions: {predictions}")
    print(f"Mean confidence: {np.mean(confidences):.3f}")

    # Calculate accuracy
    accuracy = np.mean(predictions == y_test)
    print(f"Accuracy: {accuracy * 100:.1f}%")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Prepare test data
    test_data = BCIData(test_features, metadata, test_labels)

    # Run batch inference
    results = predict_batch(model, test_data; iterations=10)

    # Analyze results
    println("Predictions: ", results.predictions)
    println("Mean confidence: ", mean(results.confidences))

    # Calculate accuracy
    accuracy = sum(results.predictions .== test_labels) / length(test_labels)
    println("Accuracy: $(round(accuracy * 100, digits=1))%")

    # Calculate ITR
    itr = calculate_ITR(accuracy, 4, 4.0)  # 4 classes, 4-second trials
    println("ITR: $(round(itr, digits=1)) bits/minute")
    ```
  </Tab>
</Tabs>

### 5. Streaming Inference

Real-time chunk-by-chunk processing:

<Note>
  For detailed Python streaming examples, see [Python SDK Streaming Inference](/python-sdk/streaming-inference).
</Note>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import StreamingSession
    from nimbus_bci.data import BCIMetadata

    # Configure metadata with chunk size
    metadata = BCIMetadata(
        sampling_rate=250.0,
        paradigm="motor_imagery",
        feature_type="csp",
        n_features=16,
        n_classes=4,
        chunk_size=250  # 1-second chunks
    )

    # Initialize streaming session
    session = StreamingSession(clf.model_, metadata)

    # Process chunks as they arrive
    for chunk in eeg_feature_stream:
        result = session.process_chunk(chunk)
        print(f"Chunk: pred={result.prediction}, conf={result.confidence:.3f}")

    # Finalize trial with aggregation
    final_result = session.finalize_trial()
    print(f"Final: pred={final_result.prediction}, conf={final_result.confidence:.3f}")

    # Reset for next trial
    session.reset()
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Initialize streaming session
    session = init_streaming(model, metadata_with_chunk_size)

    # Process chunks as they arrive
    for chunk in eeg_feature_stream
        result = process_chunk(session, chunk; iterations=10)
        println("Chunk: pred=$(result.prediction), conf=$(round(result.confidence, digits=3))")
    end

    # Finalize trial with aggregation
    final_result = finalize_trial(session; method=:weighted_vote)
    println("Final: pred=$(final_result.prediction), conf=$(round(final_result.confidence, digits=3))")
    ```
  </Tab>
</Tabs>

## Hyperparameter Tuning (v0.2.0+)

Fine-tune Bayesian LDA for optimal performance on your specific dataset.

### When to Tune Hyperparameters

Consider tuning when:

* Default performance is unsatisfactory
* You have specific data characteristics (very noisy or very clean)
* You have limited or extensive training data
* You want to optimize for your specific paradigm

### Tuning Strategies

#### For High SNR / Clean Data / Many Trials

Use lower regularization to let the data drive the model:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA

    # Lower regularization for clean data
    clf = NimbusLDA(
        mu_scale=1.0,      # Weaker prior, trust data more
        class_prior_alpha=0.5
    )
    clf.fit(X_train, y_train)
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    model = train_model(
        NimbusLDA,
        train_data;
        iterations = 50,
        dof_offset = 1,               # Less regularization
        mean_prior_precision = 0.001  # Weaker prior, trust data more
    )
    ```
  </Tab>
</Tabs>

**Use when:**

* SNR > 5 dB
* 100+ trials per class
* Clean, artifact-free data
* Well-controlled experimental conditions

#### For Low SNR / Noisy Data / Few Trials

Use higher regularization for stability:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA

    # Higher regularization for noisy data
    clf = NimbusLDA(
        mu_scale=5.0,      # Stronger prior
        class_prior_alpha=2.0
    )
    clf.fit(X_train, y_train)
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    model = train_model(
        NimbusLDA,
        train_data;
        iterations = 50,
        dof_offset = 3,               # More regularization
        mean_prior_precision = 0.05   # Stronger prior
    )
    ```
  </Tab>
</Tabs>

**Use when:**

* SNR \< 2 dB
* 40-80 trials per class
* Noisy data or limited artifact removal
* Challenging recording conditions

#### Balanced / Default Settings

The defaults work well for most scenarios:

```julia theme={null}
model = train_model(
    NimbusLDA,
    train_data;
    iterations = 50,
    dof_offset = 2,               # Balanced (default)
    mean_prior_precision = 0.01   # Balanced (default)
)
```

**Use when:**

* Moderate SNR (2-5 dB)
* 80-150 trials per class
* Standard BCI recording conditions
* Starting point for experimentation

### Hyperparameter Search Example

Systematically search for optimal hyperparameters:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    from sklearn.model_selection import GridSearchCV, train_test_split

    # Define parameter grid
    param_grid = {
        'mu_scale': [1.0, 3.0, 5.0, 10.0],
        'class_prior_alpha': [0.5, 1.0, 2.0]
    }

    # Split data
    X_train_sub, X_val, y_train_sub, y_val = train_test_split(
        X_train, y_train, test_size=0.2, stratify=y_train, random_state=42
    )

    print("Searching hyperparameters...")
    clf = NimbusLDA()
    grid_search = GridSearchCV(
        clf,
        param_grid,
        cv=5,
        scoring='accuracy',
        verbose=1
    )

    grid_search.fit(X_train_sub, y_train_sub)

    print("\nBest hyperparameters:")
    print(f"  mu_scale: {grid_search.best_params_['mu_scale']}")
    print(f"  class_prior_alpha: {grid_search.best_params_['class_prior_alpha']}")
    print(f"  CV accuracy: {grid_search.best_score_*100:.1f}%")

    # Get best model
    best_clf = grid_search.best_estimator_

    # Retrain on all training data
    best_clf.fit(X_train, y_train)
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    using NimbusSDK

    # Define search grid
    dof_values = [1, 2, 3, 4]
    prior_values = [0.001, 0.01, 0.05, 0.1]

    # Split data into train/validation
    train_data, val_data = split_data(all_data, ratio=0.8)

    best_accuracy = 0.0
    best_params = nothing

    println("Searching hyperparameters...")
    for dof in dof_values
        for prior in prior_values
            # Train model with these hyperparameters
            model = train_model(
                NimbusLDA,
                train_data;
                iterations = 50,
                dof_offset = dof,
                mean_prior_precision = prior,
                showprogress = false
            )
            
            # Validate
            results = predict_batch(model, val_data)
            accuracy = sum(results.predictions .== val_data.labels) / length(val_data.labels)
            
            println("  dof=$dof, prior=$prior: $(round(accuracy*100, digits=1))%")
            
            # Track best
            if accuracy > best_accuracy
                best_accuracy = accuracy
                best_params = (dof=dof, prior=prior)
            end
        end
    end

    println("\nBest hyperparameters:")
    println("  dof_offset: $(best_params.dof)")
    println("  mean_prior_precision: $(best_params.prior)")
    println("  Validation accuracy: $(round(best_accuracy*100, digits=1))%")

    # Retrain on all data with best hyperparameters
    final_model = train_model(
        NimbusLDA,
        all_data;
        iterations = 50,
        dof_offset = best_params.dof,
        mean_prior_precision = best_params.prior
    )
    ```
  </Tab>
</Tabs>

### Quick Tuning Guidelines

| Scenario                   | `dof_offset` | `mean_prior_precision` | Notes                  |
| -------------------------- | ------------ | ---------------------- | ---------------------- |
| **Excellent data quality** | 1            | 0.001                  | Minimal regularization |
| **Good data quality**      | 2 (default)  | 0.01 (default)         | Balanced approach      |
| **Moderate data quality**  | 2-3          | 0.01-0.03              | Slight regularization  |
| **Poor data quality**      | 3-4          | 0.05-0.1               | Strong regularization  |
| **Very limited trials**    | 4            | 0.1                    | Maximum regularization |

<Tip>
  **Pro Tip**: Start with defaults (`dof_offset=2`, `mean_prior_precision=0.01`) and only tune if performance is unsatisfactory. The defaults are optimized for typical BCI scenarios.
</Tip>

## Training Requirements

### Data Requirements

* **Minimum**: 40 trials per class (160 total for 4-class)
* **Recommended**: 80+ trials per class (320+ total for 4-class)
* **For calibration**: 10-20 trials per class sufficient

<Warning>
  **Bayesian LDA requires at least 2 trials** to estimate class statistics and shared precision matrix. Single-trial training is not statistically valid for LDA and will raise an `ArgumentError`.

  Your training data must have shape `(n_features, n_samples, n_trials)` where `n_trials >= 2`.
</Warning>

### Feature Normalization

<Tip>
  **Critical for cross-session BCI performance!**

  Normalize your features before training for 15-30% accuracy improvement across sessions.
</Tip>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from sklearn.preprocessing import StandardScaler
    import pickle

    # Estimate normalization from training data
    scaler = StandardScaler()
    X_train_norm = scaler.fit_transform(X_train)

    # Train with normalized features
    clf = NimbusLDA()
    clf.fit(X_train_norm, y_train)

    # Save model and scaler together
    with open("model_with_scaler.pkl", "wb") as f:
        pickle.dump({'model': clf, 'scaler': scaler}, f)

    # Later: Apply same normalization to test data
    X_test_norm = scaler.transform(X_test)
    predictions = clf.predict(X_test_norm)
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Estimate normalization from training data
    norm_params = estimate_normalization_params(train_features; method=:zscore)
    train_norm = apply_normalization(train_features, norm_params)

    # Train with normalized features
    train_data = BCIData(train_norm, metadata, labels)
    model = train_model(NimbusLDA, train_data)

    # Save params with model
    @save "model.jld2" model norm_params

    # Later: Apply same params to test data
    test_norm = apply_normalization(test_features, norm_params)
    ```
  </Tab>
</Tabs>

See [Feature Normalization](/inference-configuration/feature-normalization) for the recommended train/test scaling workflow.

### Feature Requirements

Bayesian LDA expects **preprocessed features**, not raw EEG:

✅ **Required preprocessing:**

* Bandpass filtering (8-30 Hz for motor imagery)
* Artifact removal (ICA recommended)
* Spatial filtering (CSP for motor imagery)
* Feature extraction (log-variance for CSP features)
* **Temporal aggregation** (handled automatically during training/inference)

❌ **NOT accepted:**

* Raw EEG channels
* Unfiltered data
* Non-extracted features

See [Preprocessing Requirements](/inference-configuration/preprocessing-requirements) for details.

### Temporal Aggregation

<Warning>
  **Critical Preprocessing Step**: Before training, the SDK automatically aggregates the temporal dimension of each trial into a single feature vector. This prevents treating temporally correlated samples as independent observations, which would violate the i.i.d. assumption of the model.
</Warning>

The aggregation method depends on your feature type and paradigm:

* **CSP features**: Log-variance aggregation (default for motor imagery)
* **Power spectral features**: Mean or median aggregation
* **Other features**: Configurable via `BCIMetadata.temporal_aggregation`

This aggregation happens automatically during `train_model()` and `predict_batch()` calls.

## Performance Characteristics

### Computational Performance

| Operation           | Latency           | Notes                               |
| ------------------- | ----------------- | ----------------------------------- |
| **Training**        | 10-30 seconds     | 50 iterations, 100 trials per class |
| **Calibration**     | 5-15 seconds      | 20 iterations, 20 trials per class  |
| **Batch Inference** | 10-20ms per trial | 10 iterations                       |
| **Streaming Chunk** | 10-20ms           | 10 iterations per chunk             |

All measurements on standard CPU (no GPU required).

### Classification Accuracy

| Paradigm      | Classes               | Typical Accuracy | ITR            |
| ------------- | --------------------- | ---------------- | -------------- |
| Motor Imagery | 2 (L/R hand)          | 75-90%           | 15-25 bits/min |
| Motor Imagery | 4 (L/R/Feet/Tongue)   | 70-85%           | 20-35 bits/min |
| P300          | 2 (Target/Non-target) | 80-95%           | 25-40 bits/min |

<Note>
  Accuracy is highly subject-dependent. Subject-specific calibration typically improves accuracy by 5-15%.
</Note>

## Model Inspection

### View Model Parameters

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import numpy as np

    params = clf.model_.params

    # Posterior class means
    print("Posterior class means:")
    for k, class_label in enumerate(clf.classes_):
        print(f"  Class {class_label}: {params['mu'][k]}")

    # Shared Normal-Wishart posterior parameters
    print("\nShared precision posterior:")
    print(f"  nu: {params['nu']}")
    print(f"  psi shape: {params['psi'].shape}")

    # Model info
    print("\nModel info:")
    print(f"  Features: {clf.n_features_in_}")
    print(f"  Classes: {clf.classes_}")

    # Class priors (learned from data)
    print("\nClass priors:")
    priors = np.exp(params["log_priors"])
    for k, class_label in enumerate(clf.classes_):
        print(f"  Class {class_label}: {priors[k]:.3f}")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Extract point estimates from posterior distributions
    using Distributions

    # Class means (extract from posterior distributions)
    println("Class means:")
    for (k, mean_posterior) in enumerate(model.mean_posteriors)
        mean_point = mean(mean_posterior)  # Extract point estimate
        println("  Class $k: ", mean_point)
    end

    # Shared precision matrix (extract from posterior distribution)
    println("\nShared precision matrix (first 3x3):")
    precision_point = mean(model.precision_posterior)  # Extract point estimate
    println(precision_point[1:3, 1:3])

    # Model metadata
    println("\nMetadata:")
    println("  Name: ", model.metadata.name)
    println("  Paradigm: ", model.metadata.paradigm)
    println("  Features: ", model.metadata.n_features)
    println("  Classes: ", model.metadata.n_classes)

    # Class priors
    println("\nClass priors:")
    for (k, prior) in enumerate(model.priors)
        println("  Class $k: ", prior)
    end
    ```
  </Tab>
</Tabs>

<Note>
  **Accessing model parameters**: The SDK stores full posterior distributions (not just point estimates) for proper Bayesian inference. To get point estimates, use `mean(posterior)` to extract the mean of the posterior distribution. For precision matrices, use `mean(precision_posterior)` to get the expected precision matrix.
</Note>

### Compare Models

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from nimbus_bci import NimbusLDA
    import numpy as np

    # Train multiple models with different hyperparameters and compare
    models = []
    for mu_scale in [1.0, 3.0, 5.0]:
        clf = NimbusLDA(mu_scale=mu_scale)
        clf.fit(X_train, y_train)
        accuracy = clf.score(X_test, y_test)
        
        print(f"mu_scale: {mu_scale}, Accuracy: {accuracy*100:.1f}%")
        models.append((mu_scale, clf, accuracy))

    # Select best model
    best_config, best_clf, best_acc = max(models, key=lambda x: x[2])
    print(f"\nBest configuration: mu_scale={best_config} ({best_acc*100:.1f}%)")
    ```
  </Tab>

  <Tab title="Julia">
    ```julia theme={null}
    # Train multiple models and compare
    models = []
    for n_iter in [20, 50, 100]
        model = train_model(NimbusLDA, train_data; iterations=n_iter)
        results = predict_batch(model, test_data)
        accuracy = sum(results.predictions .== test_labels) / length(test_labels)
        
        println("Iterations: $n_iter, Accuracy: $(round(accuracy*100, digits=1))%")
        push!(models, (n_iter, model, accuracy))
    end
    ```
  </Tab>
</Tabs>

## Advantages & Limitations

### Advantages

✅ **Fast Training**: Shared covariance estimation is efficient\
✅ **Fast Inference**: Analytical posterior computation (\<20ms)\
✅ **Interpretable**: Clear probabilistic formulation\
✅ **Memory Efficient**: Single shared precision matrix\
✅ **Robust**: Handles uncertainty naturally via Bayesian inference\
✅ **Production-Ready**: Battle-tested in real BCI applications

### Limitations

❌ **Shared Covariance Assumption**: May not fit well if classes have very different spreads\
❌ **Linear Decision Boundary**: Cannot capture non-linear class boundaries\
❌ **Gaussian Assumption**: Assumes normal class distributions\
❌ **Not Ideal for Overlapping Classes**: Use NimbusQDA for complex distributions

## Model Selection Context

Use `NimbusLDA` when classes are mostly stationary and well separated, and speed or interpretability matter. If classes overlap with different covariance structure, consider `NimbusQDA`. If the decision boundary is non-Gaussian, consider `NimbusSoftmax` in Python or `NimbusProbit` in Julia. If distributions drift over time, consider `NimbusSTS` in Python.

For the canonical side-by-side comparison, see [Model Specification](/model-specification).

## Next Read

<CardGroup cols={2}>
  <Card title="Bayesian QDA (NimbusQDA)" icon="brain" href="/models/rxgmm">
    More flexible classifier with class-specific covariances
  </Card>

  <Card title="NimbusSoftmax (Python)" icon="brain" href="/models/rxpolya">
    Python non-Gaussian static classifier for complex decision boundaries
  </Card>

  <Card title="NimbusProbit (Julia)" icon="brain" href="/models/nimbusprobit">
    Julia non-Gaussian static classifier for complex decision boundaries
  </Card>

  <Card title="Bayesian STS (NimbusSTS)" icon="wave-pulse" href="/models/rxsts">
    Adaptive model for non-stationary data and long sessions
  </Card>

  <Card title="Training Tutorial" icon="graduation-cap" href="/examples/advanced-applications">
    Complete training walkthrough
  </Card>

  <Card title="Code Examples" icon="braces" href="/examples/basic-examples">
    Working examples
  </Card>
</CardGroup>

## References

**Implementation:**

* RxInfer.jl: [https://rxinfer.com/](https://rxinfer.com/)
* Source code: `src/models/nimbus_lda/` in NimbusSDKCore

**Theory:**

* Fisher, R. A. (1936). "The use of multiple measurements in taxonomic problems"
* Bishop, C. M. (2006). "Pattern Recognition and Machine Learning" (Chapter 4)
* Pooled Gaussian Classifier (PGC) with shared covariance structure

**BCI Applications:**

* Blankertz et al. (2008). "Optimizing spatial filters for robust EEG single-trial analysis"
* Lotte et al. (2018). "A review of classification algorithms for EEG-based BCI"

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
  '@context': 'https://schema.org',
  '@type': 'TechArticle',
  headline: 'Bayesian LDA (NimbusLDA)',
  description:
    'Bayesian Linear Discriminant Analysis with uncertainty quantification using RxInfer.jl. Fast 10-15ms inference for motor imagery BCI with shared covariance modeling.',
  author: {
    '@type': 'Organization',
    name: 'Nimbus BCI',
    url: 'https://nimbusbci.com',
  },
  publisher: {
    '@type': 'Organization',
    name: 'Nimbus BCI',
    url: 'https://nimbusbci.com',
  },
  about: {
    '@type': 'Thing',
    name: 'Brain-Computer Interface',
    description: 'Bayesian classification for motor imagery BCI',
  },
  keywords:
    'Bayesian LDA, NimbusLDA, Linear Discriminant Analysis, motor imagery, BCI classification, uncertainty quantification, RxInfer, NimbusLDA',
  inLanguage: 'en-US',
  isAccessibleForFree: true,
}),
}}
/>
