Repeated-measures functional regression¶
This example fits a condition effect that varies over trial time while preserving repeated trials within participant.
Simulate repeated trajectories¶
import numpy as np
import pandas as pd
from eyetrajectoriespy import TrajectorySet
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
linear_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,
)
curves = []
curve_ids = []
for i in range(n_participants):
participant_function = random_coefficients[i] @ linear_basis.T
for j in range(trials_per_participant):
row = i * trials_per_participant + j
curves.append(
(
beta0
+ condition[row] * beta_condition
+ participant_function
+ rng.normal(0.0, 0.04, size=time.size)
)[:, None]
)
curve_ids.append(f"C{row:03d}")
gaze_metric = TrajectorySet(
time=time,
values=np.asarray(curves),
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": gaze_metric.curve_ids,
"condition": condition,
})
The condition predictor varies within every participant, so participant-level aggregation would erase the repeated-measures contrast.
Fit one joint functional mixed model¶
from eyetrajectoriespy import fit_functional_mixed_effects_regression
fit = fit_functional_mixed_effects_regression(
gaze_metric,
design,
predictors=("condition",),
participant_column="participant_id",
dimension="metric",
fixed_basis_size=2,
random_basis_size=2,
spline_degree=1,
reml=True,
)
The model is
All time points and trials enter a single mixed-effects likelihood.
Inspect the coefficient function¶
from eyetrajectoriespy import plot_functional_mixed_effects_coefficient
ax = plot_functional_mixed_effects_coefficient(
fit,
coefficient="condition",
)
The shaded region is a 95% pointwise Wald interval. It should not be described as a simultaneous whole-function confidence band.
Add a simultaneous whole-function band¶
from eyetrajectoriespy import (
bootstrap_functional_mixed_effects_coefficients,
functional_mixed_effects_simultaneous_bands,
)
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",
)
ax = plot_functional_mixed_effects_coefficient(
band,
coefficient="condition",
)
This band is simultaneous over the observed time grid for the declared coefficient. Whole participant trial bundles are resampled; the fitted random-effect covariance and residual variance are held fixed.
Refit variance components under participant resampling¶
For sensitivity to covariance-estimation uncertainty, version 0.46 provides a separate full-refit participant bootstrap:
from eyetrajectoriespy import (
bootstrap_functional_mixed_effects_full_refit,
)
full_boot = bootstrap_functional_mixed_effects_full_refit(
fit,
n_bootstrap=500,
random_state=46,
)
full_band = functional_mixed_effects_simultaneous_bands(
full_boot,
confidence_level=0.95,
simultaneous_scope="coefficient",
)
Every selected participant occurrence receives a distinct bootstrap group ID. Fixed effects, the complete random-effect covariance, and residual variance are refit in every bootstrap replicate while the declared basis/model specification remains fixed.
Use the dedicated full-refit worked example to inspect source/bootstrap participant identities, variance-component stability, and full-refit versus fixed-covariance band-width sensitivity.
Export the coefficient table¶
from eyetrajectoriespy import functional_mixed_effects_coefficient_frame
table = functional_mixed_effects_coefficient_frame(fit, band=band)
print(table.query("coefficient == 'condition'"))
Report the model contract¶
from eyetrajectoriespy import functional_mixed_effects_reporting_text
print(functional_mixed_effects_reporting_text(fit, band=band))
# Or, for the full-refit uncertainty contract:
print(functional_mixed_effects_reporting_text(fit, band=full_band))
Report the participant count, trial count, exact scalar design coding, fixed and random basis sizes, spline degree, ML/REML choice, optimizer, convergence, boundary-fit status, residual structure, whether predictors varied within participant, bootstrap replicate count, and simultaneous scope.
For the fixed-covariance bootstrap, state that variance components were held fixed. For the full-refit bootstrap, state that fixed effects, the random-effect covariance, and residual variance were re-estimated while the declared model specification remained fixed.
The executable counterpart is
examples/functional_mixed_effects_regression.py.
Participant-specific random condition effects¶
If the scientific question is not only the population condition coefficient but whether participants differ in that time-varying condition response, use the guarded 0.45 random-slope extension rather than manually fitting separate participant curves:
fit_slope = fit_functional_mixed_effects_regression(
gaze_metric,
design,
predictors=("condition",),
participant_column="participant_id",
dimension="metric",
fixed_basis_size=2,
random_basis_size=2,
random_slope_predictor="condition",
spline_degree=1,
)
The predictor must vary within every participant and the participant count must exceed the number of free unstructured random-effect covariance parameters. See the random-functional-slope worked example for covariance diagnostics and participant BLUP slope inspection.