Skip to content

Worked random functional slope example

This example allows participants to differ over trial time in how strongly a within-participant condition changes the functional response.

import numpy as np
import pandas as pd

from eyetrajectoriespy import (
    TrajectorySet,
    fit_functional_mixed_effects_regression,
    functional_random_effect_frame,
    plot_functional_random_effects,
)

rng = np.random.default_rng(2045)
n_participants = 20
trials_per_participant = 4
time = np.linspace(0.0, 1.0, 9)
basis = np.column_stack([1.0 - time, time])

condition = np.tile(
    np.array([-0.75, -0.25, 0.25, 0.75]),
    n_participants,
)
participants = np.repeat(
    [f"P{i:03d}" for i in range(n_participants)],
    trials_per_participant,
)

beta0 = 0.25 + 0.12 * time
beta_condition = 0.18 + 0.30 * time

covariance = np.array(
    [
        [0.030, 0.004, 0.010, 0.002],
        [0.004, 0.022, 0.002, 0.008],
        [0.010, 0.002, 0.022, 0.003],
        [0.002, 0.008, 0.003, 0.017],
    ]
)
random_coefficients = rng.multivariate_normal(
    np.zeros(4),
    covariance,
    size=n_participants,
)
random_intercept = random_coefficients[:, :2] @ basis.T
random_slope = random_coefficients[:, 2:] @ basis.T

values = []
curve_ids = []
for participant_index in range(n_participants):
    for trial_index in range(trials_per_participant):
        row = participant_index * trials_per_participant + trial_index
        values.append(
            (
                beta0
                + condition[row] * beta_condition
                + random_intercept[participant_index]
                + condition[row] * random_slope[participant_index]
                + rng.normal(0.0, 0.025, size=time.size)
            )[:, None]
        )
        curve_ids.append(f"C{row:04d}")

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

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,
    random_slope_predictor="condition",
    spline_degree=1,
    maxiter=1000,
)

With \(q=2\), the stacked random-effect vector has dimension four and its unstructured covariance has ten free parameters. The 20-participant example therefore passes the 0.45 covariance-complexity guard.

Inspect the covariance structure

print(fit.random_effect_dimension)
print(fit.random_effect_covariance_parameter_count)
print(fit.random_effect_covariance_eigenvalues)
print(fit.random_effect_covariance_condition_number)

print(fit.random_intercept_covariance)
print(fit.random_slope_covariance)
print(fit.random_intercept_slope_covariance)

Do not interpret a large covariance matrix as scientifically stable merely because the optimizer converged. Inspect the retained eigenvalues, condition number, boundary diagnostics, participant count, and substantive plausibility.

Inspect participant slope functions

slope_table = functional_random_effect_frame(
    fit,
    effect="slope",
)

ax = plot_functional_random_effects(
    fit,
    effect="slope",
)

Each plotted curve is a participant BLUP \(\widehat b_{1i}(t)\). Positive values indicate a participant-specific condition response above the population fixed condition coefficient at that time; negative values indicate a response below it.

Keep 0.44 simultaneous inference conditional

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=45,
)

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

This bootstrap keeps the fitted random intercept/slope covariance fixed. It does not refit the covariance matrix inside each bootstrap sample.