Temporal trajectory alignment#

TemporalProcrustesAlignment puts participant-specific PCA trajectories into a common coordinate system. It expects an (observation, feature, time) array and one participant ID per observation.

The transform is deliberately small: it fits a shared PCA/template, fits one PCA per participant, and uses an orthogonal Procrustes rotation to map each participant’s grand-mean temporal trajectory onto the template. It never uses the target labels.

Decoding integration#

Enable alignment on an ordinary temporal decoding experiment:

from coco_pipe.decoding import (
    Experiment,
    ExperimentConfig,
    TemporalAlignmentConfig,
)

config = ExperimentConfig(
    task="classification",
    models={"sliding": temporal_decoder},
    metrics=["balanced_accuracy"],
    cv=subject_aware_cv,
    temporal_alignment=TemporalAlignmentConfig(
        enabled=True,
        n_components=30,
    ),
)

result = Experiment(config).run(X, y, groups=participant_ids, time_axis=times)

Experiment applies the alignment separately in every outer cross-validation fold, before fitting the temporal estimator. The shared PCA and template see only that fold’s training observations.

The transductive assumption#

For a participant present during fit, transform reuses the stored PCA and rotation. For a wholly unseen participant, coco-pipe estimates that participant’s PCA and rotation from the unlabelled transform batch. This is transductive domain adaptation: it does not use test labels, but it does use the available test-participant observations.

Use it when that calibration policy matches the scientific question, and report it explicitly. It is not blind single-trial generalization to a new participant. The current configuration names this supported policy with adaptation="transductive".

Direct use#

The same transform can be used outside decoding for descriptive analyses:

from coco_pipe.transforms import TemporalProcrustesAlignment

aligner = TemporalProcrustesAlignment(n_components=3, random_state=42)
aligned = aligner.fit_transform(X, groups=participant_ids)

Direct use does not create cross-validation boundaries for you. For predictive evaluation, prefer the ExperimentConfig integration above.