Skip to content

Covariance-structure sensitivity

This worked example compares four independently fitted covariance structures against one declared reference without asking the package to select among them.

Predeclare the structures

from eyetrajectoriespy import (
    FunctionalMixedEffectsCovarianceSpecification,
)

specifications = (
    FunctionalMixedEffectsCovarianceSpecification(
        "M1",
        residual_correlation="iid",
    ),
    FunctionalMixedEffectsCovarianceSpecification(
        "M2",
        trial_random_effect="functional_intercept",
        residual_correlation="iid",
    ),
    FunctionalMixedEffectsCovarianceSpecification(
        "M3",
        residual_correlation="exponential",
    ),
    FunctionalMixedEffectsCovarianceSpecification(
        "M4",
        trial_random_effect="functional_intercept",
        residual_correlation="exponential",
    ),
)

Fit the models independently

fit_m1 = fit_functional_mixed_effects_regression(
    trajectories,
    design,
    predictors=("condition",),
    participant_column="participant_id",
    dimension="metric",
    fixed_basis_size=3,
    random_basis_size=2,
    residual_correlation="iid",
    reml=False,
)

fit_m2 = fit_functional_mixed_effects_regression(
    trajectories,
    design,
    predictors=("condition",),
    participant_column="participant_id",
    trial_column="trial_id",
    trial_random_effect="functional_intercept",
    trial_random_basis_size=2,
    dimension="metric",
    fixed_basis_size=3,
    random_basis_size=2,
    residual_correlation="iid",
    reml=False,
)

fit_m3 = fit_functional_mixed_effects_regression(
    trajectories,
    design,
    predictors=("condition",),
    participant_column="participant_id",
    dimension="metric",
    fixed_basis_size=3,
    random_basis_size=2,
    residual_correlation="exponential",
    reml=False,
)

fit_m4 = fit_functional_mixed_effects_regression(
    trajectories,
    design,
    predictors=("condition",),
    participant_column="participant_id",
    trial_column="trial_id",
    trial_random_effect="functional_intercept",
    trial_random_basis_size=2,
    dimension="metric",
    fixed_basis_size=3,
    random_basis_size=2,
    residual_correlation="exponential",
    reml=False,
)

The response functions, fixed design, fixed basis, participant mapping, time grid, response dimension, and likelihood mode are unchanged. Only declared covariance structure changes.

Compare against M1

from eyetrajectoriespy import (
    functional_mixed_effects_covariance_sensitivity,
)

result = functional_mixed_effects_covariance_sensitivity(
    fits={
        "M1": fit_m1,
        "M2": fit_m2,
        "M3": fit_m3,
        "M4": fit_m4,
    },
    specifications=specifications,
    reference="M1",
    max_lag=3,
)

Inspect the model-level table without sorting it:

result.model_summary[
    [
        "model",
        "status",
        "log_likelihood",
        "n_parameters",
        "aic",
        "bic",
        "delta_aic",
        "delta_bic",
        "trial_covariance_trace",
        "residual_variance",
        "residual_correlation_parameter",
        "whitened_max_abs_acf_positive_lags",
    ]
]

These values are descriptive. No row is labelled best.

Inspect the scientific estimand first

result.coefficient_summary

For each fixed coefficient, this reports the observed-grid supremum and integrated \(L_2\) difference from M1.

from eyetrajectoriespy import (
    plot_covariance_sensitivity_coefficients,
)

plot_covariance_sensitivity_coefficients(
    result,
    coefficient="condition",
)

If the condition coefficient changes sign, timing, or shape materially across defensible covariance structures, that sensitivity is scientifically more important than a small information-criterion difference.

Inspect covariance attribution

result.variance_decomposition
from eyetrajectoriespy import plot_functional_variance_decomposition

plot_functional_variance_decomposition(
    result,
    model="M4",
)

Participant intercept variance, participant slope variance when present, intercept/slope cross-covariance, trial variance, and residual variance remain separate.

Inspect raw and whitened residual structure

diagnostics = result.residual_diagnostics

diagnostics.loc[
    diagnostics["model"].eq("M4"),
    [
        "residual_scale",
        "lag_index",
        "lag_time_mean",
        "n_pairs",
        "autocorrelation",
        "semivariance",
    ],
]

For correlated-error models, raw residual dependence is expected. The whitened rows diagnose remaining structure after applying the fitted residual covariance.

Include a failed predeclared model

Suppose a fifth predeclared AR(1) model cannot be fitted because the observed time grid is irregular. Keep the failure in the sensitivity analysis:

specifications_with_failure = specifications + (
    FunctionalMixedEffectsCovarianceSpecification(
        "M5",
        trial_random_effect="functional_intercept",
        residual_correlation="ar1",
    ),
)

result = functional_mixed_effects_covariance_sensitivity(
    fits={
        "M1": fit_m1,
        "M2": fit_m2,
        "M3": fit_m3,
        "M4": fit_m4,
    },
    specifications=specifications_with_failure,
    failures={
        "M5": "AR(1) rejected because the common grid is irregular."
    },
    reference="M1",
    max_lag=3,
)

M5 remains in model_summary with status failed. Its numerical fields are NaN; it is not silently removed.

Add paired simultaneous-band sensitivity

If band-width sensitivity is required, create the bands with the same participant bootstrap draws for each supplied model:

boot_m1 = bootstrap_functional_mixed_effects_coefficients(
    fit_m1,
    n_bootstrap=1000,
    random_state=50,
)
boot_m3 = bootstrap_functional_mixed_effects_coefficients(
    fit_m3,
    n_bootstrap=1000,
    random_state=50,
)

band_m1 = functional_mixed_effects_simultaneous_bands(boot_m1)
band_m3 = functional_mixed_effects_simultaneous_bands(boot_m3)

result = functional_mixed_effects_covariance_sensitivity(
    fits={"M1": fit_m1, "M3": fit_m3},
    reference="M1",
    max_lag=3,
    bands={"M1": band_m1, "M3": band_m3},
)

The paired-draw guard prevents different bootstrap samples from masquerading as covariance sensitivity.

Report the analysis

from eyetrajectoriespy import (
    functional_mixed_effects_covariance_sensitivity_reporting_text,
)

print(
    functional_mixed_effects_covariance_sensitivity_reporting_text(
        result
    )
)

The reporting helper explicitly states that the analysis is predeclared, descriptive, and does not rank or automatically select a covariance structure.