Functional RQA sensitivity and participant-level inference¶
This example shows a complete 0.25 workflow for a repeated-trial design.
The goal is not to find a window size automatically. We predeclare several plausible window/step specifications, inspect their consequences, then fit inference for one separately justified primary specification.
Synthetic repeated-trial data¶
import numpy as np
import pandas as pd
from eyetrajectoriespy import TrajectorySet
time = np.arange(180, dtype=float) * 0.01
values = []
participants = []
curve_ids = []
for participant_index in range(6):
participant_id = f"P{participant_index + 1:02d}"
for trial in range(2):
phase = 0.18 * participant_index + 0.08 * trial
signal = (
np.sin(2 * np.pi * 1.7 * time + phase)
+ 0.12 * np.sin(2 * np.pi * 0.55 * time + 0.4 * phase)
)
values.append(signal)
participants.append(participant_id)
curve_ids.append(f"{participant_id}_T{trial + 1}")
gaze = TrajectorySet(
time=time,
values=np.asarray(values)[:, :, None],
curve_ids=tuple(curve_ids),
dimension_names=("x",),
metadata=pd.DataFrame({"participant_id": participants}),
coordinate_system="normalized",
time_unit="s",
)
1. Declare a sensitivity grid¶
from eyetrajectoriespy import windowed_rqa_sensitivity
sensitivity = windowed_rqa_sensitivity(
gaze,
metrics=("recurrence_rate", "determinism", "laminarity"),
window_step_pairs=((40, 20), (40, 10), (60, 20)),
radius=0.35,
theiler_window=2,
dimensions=("x",),
)
Nothing in this function chooses among the three specifications.
Inspect the design consequences:
print(
sensitivity.design_table[
[
"specification_id",
"window_samples",
"step_samples",
"overlap_fraction",
"profile_grid_spacing_time",
"fraction_analyzed_samples_reused",
"mean_window_memberships_per_analyzed_sample",
"max_window_memberships",
]
]
)
For the same 40-sample window, moving from a 20-sample step to a 10-sample step makes the derived functional grid denser and increases source-sample reuse. It does not create twice as many independent observations.
2. Compare profiles without interpolation¶
print(
sensitivity.pairwise_table[
[
"specification_a",
"specification_b",
"curve_id",
"metric",
"n_common_centers",
"rmse",
"correlation",
]
].head()
)
Comparisons are restricted to exact shared centers. If different window widths shift all centers relative to one another, n_common_centers can be zero. The package leaves the corresponding RMSE/correlation undefined instead of silently interpolating.
A visual sensitivity overlay is also available:
from eyetrajectoriespy import plot_windowed_rqa_sensitivity
ax = plot_windowed_rqa_sensitivity(
sensitivity,
curve="P01_T1",
metric="determinism",
)
3. Infer a mean at the participant level¶
Suppose the 40/20 specification was the predeclared primary analysis. Because each participant contributes two trials, the independent inference unit should be the participant rather than the twelve trial curves.
from eyetrajectoriespy import windowed_rqa_functional_mean_band
mean_band = windowed_rqa_functional_mean_band(
gaze,
metrics=("recurrence_rate", "determinism", "laminarity"),
window=40,
step=20,
unit="participant",
participant_column="participant_id",
radius=0.35,
theiler_window=2,
dimensions=("x",),
confidence_level=0.95,
n_multiplier=2000,
random_state=42,
)
The six participants, not the window rows and not the twelve repeated trial curves, are the six independent units used by the band.
print(mean_band.band.n_units)
print(mean_band.band.unit_ids)
Within each multiplier draw, each participant residual function remains a complete time-by-metric object. This preserves the observed within-function covariance structure used by the maximum statistic.
4. Reporting¶
from eyetrajectoriespy import (
windowed_rqa_mean_band_reporting_text,
windowed_rqa_sensitivity_reporting_text,
)
print(windowed_rqa_sensitivity_reporting_text(sensitivity))
print(windowed_rqa_mean_band_reporting_text(mean_band))
Report the sensitivity grid even when it does not change the substantive conclusion. If profiles differ materially across specifications, that dependency is part of the result rather than a nuisance to hide.
Boundaries¶
This workflow does not justify treating overlapping windows as independent. It also does not implement a moving/block bootstrap inside a single participant trajectory. If there is only one long time series and the inferential target depends on within-series resampling, a separate time-series resampling contract is required.
The executable version is examples/rqa_functional_sensitivity.py.