Skip to content

Worked simultaneous mixed-effects coefficient bands

This example fits a trial-varying condition effect and then constructs a participant-cluster simultaneous band for the complete observed coefficient trajectory.

import numpy as np
import pandas as pd

from eyetrajectoriespy import (
    TrajectorySet,
    bootstrap_functional_mixed_effects_coefficients,
    fit_functional_mixed_effects_regression,
    functional_mixed_effects_coefficient_frame,
    functional_mixed_effects_reporting_text,
    functional_mixed_effects_simultaneous_bands,
    plot_functional_mixed_effects_coefficient,
)

rng = np.random.default_rng(2026)
n_participants = 12
trials_per_participant = 3
time = np.linspace(0.0, 1.0, 9)

participants = np.repeat(
    [f"P{i:02d}" for i in range(n_participants)],
    trials_per_participant,
)
condition = np.tile([0.0, 1.0, 0.5], n_participants)

beta0 = 0.20 + 0.15 * time
beta_condition = 0.15 + 0.40 * time

basis = np.column_stack([1.0 - time, time])
random_coefficients = rng.multivariate_normal(
    [0.0, 0.0],
    [[0.030, 0.004], [0.004, 0.020]],
    size=n_participants,
)

values = []
curve_ids = []
for participant_index in range(n_participants):
    random_function = random_coefficients[participant_index] @ basis.T
    for trial_index in range(trials_per_participant):
        row = participant_index * trials_per_participant + trial_index
        values.append(
            (
                beta0
                + condition[row] * beta_condition
                + random_function
                + rng.normal(0.0, 0.04, size=time.size)
            )[:, None]
        )
        curve_ids.append(f"C{row:03d}")

trajectories = TrajectorySet(
    time=time,
    values=np.asarray(values),
    curve_ids=tuple(curve_ids),
    dimension_names=("metric",),
    metadata=pd.DataFrame({"participant_id": participants}),
    coordinate_system="unknown",
    time_unit="s",
)

design = pd.DataFrame(
    {
        "curve_id": trajectories.curve_ids,
        "condition": condition,
    }
)

fit = fit_functional_mixed_effects_regression(
    trajectories,
    design,
    predictors=("condition",),
    participant_column="participant_id",
    dimension="metric",
    fixed_basis_size=2,
    random_basis_size=2,
    spline_degree=1,
)

boot = bootstrap_functional_mixed_effects_coefficients(
    fit,
    n_bootstrap=500,
    random_state=44,
)

band = functional_mixed_effects_simultaneous_bands(
    boot,
    confidence_level=0.95,
    simultaneous_scope="coefficient",
)

Inspect the whole-function band

ax = plot_functional_mixed_effects_coefficient(
    band,
    coefficient="condition",
)

The shaded region is now a 95% observed-grid simultaneous band, not the pointwise Wald interval shown when the plotting function receives the raw fit.

Export auditable values

table = functional_mixed_effects_coefficient_frame(
    fit,
    band=band,
)

print(
    table.query("coefficient == 'condition'")[
        [
            "time",
            "estimate",
            "bootstrap_standard_error",
            "lower_simultaneous",
            "upper_simultaneous",
            "simultaneous_critical_value",
        ]
    ]
)

Verify the resampling contract

print(boot.sampled_participant_indices.shape)
print(
    boot.provenance[
        "functional_mixed_effects_bootstrap"
    ]
)

Each row of sampled_participant_indices contains one bootstrap draw of participant indices. Every occurrence contributes that participant's complete trial bundle.

The covariance model is held fixed; this is therefore not a full variance-component bootstrap.

Familywise coefficient scope

To control one maximum across the intercept, condition coefficient, and the observed time grid:

family_band = functional_mixed_effects_simultaneous_bands(
    boot,
    confidence_level=0.95,
    simultaneous_scope="family",
)

Use family scope only when that coefficient family is genuinely the planned inferential family.

Reporting text

print(
    functional_mixed_effects_reporting_text(
        fit,
        band=band,
    )
)

Report the participant count, number of source curves, fixed/random basis sizes, covariance structure, number of participant bootstrap replicates, simultaneous scope, observed-grid interpretation, and the fact that variance components and basis choices were conditioned on rather than re-estimated.