Worked example: disclosure inspection¶
This example begins with synthetic trial windows plus fixation rows rather than a pre-censored table. The target is a disclosure AOI. Some trials never inspect it before the valid trial window ends and therefore become right-censored observations.
1. Generate synthetic trial/event inputs¶
from eyeprocesspy.survival import simulate_gaze_survival_inputs
raw = simulate_gaze_survival_inputs(
"disclosure",
seed=20260918,
n_participants=36,
trials_per_participant=3,
)
The synthetic event table intentionally omits participant_id, mirroring canonical episode workflows where recording_id + trial_id identifies the event stream.
2. Construct event and censoring rows¶
from eyeprocesspy.survival import prepare_gaze_survival_data
data = prepare_gaze_survival_data(
raw["trials"],
raw["events"],
target_aoi="disclosure",
event_type="first_fixation",
condition_col="condition_id",
min_valid_fraction=0.90,
event_detector="synthetic_truth",
aoi_specification="fixed synthetic disclosure AOI",
quality_rules={"minimum_fixation_ms": 80, "valid_fraction_min": 0.90},
)
No target event inside a complete usable trial produces event_observed = 0 and analysis_time = censor_time. Unusable or incomplete trials would instead retain an unknown event status plus review_required = True.
3. Audit censoring¶
from eyeprocesspy.survival import summarise_gaze_censoring, validate_gaze_survival_data
print(validate_gaze_survival_data(data, raise_on_error=False))
print(summarise_gaze_censoring(data, by="condition"))
Report the censoring proportion by condition; a condition-specific difference in usable follow-up or event occurrence can itself be substantively important.
4. Plot Kaplan–Meier curves¶
from eyeprocesspy.survival import plot_gaze_survival_curve
ax = plot_gaze_survival_curve(data, group="condition")
ax.set_title("Time to first disclosure fixation")
Lower survival at a given time means a larger share of trials has already inspected the target by that point.
5. Fit repeated-participant Cox and AFT models¶
from eyeprocesspy.survival import fit_gaze_aft_model, fit_gaze_mixed_cox_model
cox = fit_gaze_mixed_cox_model(
data,
"C(condition)",
participant_col="participant_id",
structure="cluster_robust",
)
weibull = fit_gaze_aft_model(data, "C(condition)", distribution="weibull")
lognormal = fit_gaze_aft_model(data, "C(condition)", distribution="lognormal")
The Cox model reports hazard ratios; the AFT models report time ratios. They should be interpreted as different estimands rather than ranked automatically.
6. Diagnose and compare transparently¶
from eyeprocesspy.survival import (
check_gaze_proportional_hazards,
compare_gaze_survival_models,
tidy_gaze_survival_model,
)
print(tidy_gaze_survival_model(cox))
print(tidy_gaze_survival_model(weibull))
print(check_gaze_proportional_hazards(cox))
print(compare_gaze_survival_models(cox, weibull, lognormal))
The comparison table includes a likelihood-basis field and a flag showing whether information criteria are directly comparable. Cox-versus-AFT comparisons are deliberately marked non-comparable on AIC/BIC grounds.
7. Run explicit sensitivity branches¶
from eyeprocesspy.survival import compare_gaze_survival_specifications
expanded = data.copy()
expanded["aoi_specification"] = "synthetic expanded disclosure AOI"
sensitivity = compare_gaze_survival_specifications(
{"primary": data, "expanded_aoi_demo": expanded},
"C(condition)",
model_families=["cox_cluster_robust", "aft_weibull"],
)
In a real study, rebuild expanded from the alternative AOI geometry or detector; do not merely change its label. The example relabels it only to demonstrate the output contract without fabricating a second scientific truth.
8. Produce manuscript-ready output¶
from eyeprocesspy.survival import report_gaze_survival_model
report = report_gaze_survival_model(cox)
print(report["N_participants"])
print(report["N_censored_trials"])
print(report["effects"])
The repository script examples/worked_gaze_survival_analysis.py executes the entire workflow and writes censoring, effect, diagnostic, sensitivity, report, and SVG figure files.
Failure case: unusable gaze is not censoring¶
bad = raw["trials"].copy()
bad.loc[0, "valid_data_fraction"] = 0.20
reviewed = prepare_gaze_survival_data(
bad,
raw["events"],
target_aoi="disclosure",
min_valid_fraction=0.90,
)
assert reviewed.loc[0, "review_required"]
assert reviewed.loc[0, "event_observed"] != 0
That row must be resolved or handled in an explicit sensitivity/exclusion branch before inferential model fitting.