Skip to content

Worked example: stable subspace, unstable FPC labels

This example shows why near-tied FPCs should sometimes be interpreted as an eigenspace rather than as individually fixed axes.

Fit a reference MFPCA

import numpy as np
from dataclasses import replace

from eyetrajectoriespy import (
    compare_fpca_subspaces,
    fit_mfpca,
    fpca_eigenvalue_gap_table,
    simulate_planar_trajectories,
)

gaze = simulate_planar_trajectories(
    n_participants=12,
    trials_per_participant=3,
    n_time=61,
    random_state=42,
)

fit = fit_mfpca(
    gaze,
    n_components=4,
    scaling="dimension_sd",
)

Inspect the retained adjacent eigengaps:

print(fpca_eigenvalue_gap_table(fit))

No near-tie label is created unless you explicitly provide a threshold.

Rotate FPC1 and FPC2 without changing their span

A 45-degree rotation creates new basis axes inside the same two-dimensional space:

theta = np.pi / 4
rotation = np.array([
    [np.cos(theta), -np.sin(theta)],
    [np.sin(theta),  np.cos(theta)],
])

components = fit.components.copy()
components[:2] = np.einsum(
    "ab,btd->atd",
    rotation,
    fit.components[:2],
)

scores = fit.scores.copy()
scores[:, :2] = fit.scores[:, :2] @ rotation.T

rotated = replace(
    fit,
    components=components,
    scores=scores,
)

Compare one FPC versus the two-FPC span

fpc1 = compare_fpca_subspaces(
    fit,
    rotated,
    start_component=0,
    n_components=1,
)

pair = compare_fpca_subspaces(
    fit,
    rotated,
    start_component=0,
    n_components=2,
)

print(fpc1.principal_cosines)
print(pair.principal_cosines)

For a 45-degree rotation, FPC1 alone has cosine similarity about 0.707.

The two-dimensional span is unchanged, so both principal cosines are approximately 1 and the normalized projector distance is approximately 0.

That is the central distinction:

an individual FPC axis can move while the scientifically relevant eigenspace remains stable.

Bootstrap the observed eigenspace

from eyetrajectoriespy import (
    bootstrap_fpca_subspace_stability,
    fpca_subspace_stability_reporting_text,
    plot_fpca_subspace_stability,
    summarise_fpca_subspace_stability,
)

stability = bootstrap_fpca_subspace_stability(
    gaze,
    start_component=0,
    n_components=2,
    n_bootstrap=200,
    scaling="dimension_sd",
    resample_unit="participant",
    participant_column="participant_id",
    random_state=42,
)

print(summarise_fpca_subspace_stability(stability))
print(fpca_subspace_stability_reporting_text(stability))
plot_fpca_subspace_stability(stability)

Interpretation

If individual FPC1/FPC2 matching is weak but the two-dimensional subspace is stable, report the block-level result and avoid assigning rigid psychological meanings to the orientation of each axis.

If the subspace is also unstable, interpret the entire low-dimensional structure cautiously and inspect sample size, influential participants, preprocessing, and the retention boundary.

Failure cases

The API rejects:

  • component blocks outside the common fitted range;
  • mismatched grids, dimensions, coordinate systems, or time units;
  • invalid bootstrap counts or resampling units;
  • component blocks exceeding the non-zero sample rank;
  • missing participant IDs in participant-level bootstrap.