# Pupil preprocessing and analysis workflow

# Overview

This vignette shows a practical pupil workflow for Gazepoint GP3 / Gazepoint Analysis exports using `gp3tools`.

The workflow covers:

1. creating a master table;
2. running a light pupil preprocessing branch;
3. running a conservative artifact-cleaned pupil branch;
4. auditing pupil preprocessing quality;
5. summarising pupil windows;
6. fitting confirmatory pupil-window models;
7. running time-course models and sensitivity checks.

The vignette chunks are not evaluated during package checks, so the file can serve as a readable workflow template.

The first section uses built-in example data; the optional import chunk shows how to replace those objects with data from a private Gazepoint export folder.

When adapting the code, replace the placeholder paths and column names with those used in your own study.

# 1. Load example data or import your own exports

For a quick read-through, start with the lightweight synthetic example data included in the package.

These objects make the later workflow easier to follow because they already use the column names expected by the examples.

    library(gp3tools)

    data("gazepoint_example_master")
    data("gazepoint_example_pupil_windows")

    master <- gazepoint_example_master
    pupil_windows <- gazepoint_example_pupil_windows

For a real study, replace the example objects above by importing a Gazepoint export folder.

This optional chunk creates `all_gaze` and then rebuilds `master` from the imported rows:

    export_dir <- "C:/Users/YourName/Desktop/gp3_test_exports"
    output_dir <- "C:/Users/YourName/Desktop/gp3_outputs"

    results <- run_gazepoint_workflow(
      export_dir = export_dir,
      output_dir = output_dir,
      prefix = "study1",
      save_plots = TRUE,
      create_report = TRUE
    )

    all_gaze <- results$all_gaze
    master <- create_gazepoint_master(all_gaze)

# 2. Validate the master table

    master_audit <- audit_gazepoint_master(master)

    validation <- validate_gazepoint_master(master)

    master_audit$overview
    validation$summary
    validation$checks

Use the validation output before continuing to pupil preprocessing.

If you imported real Gazepoint exports, this is the point where you should confirm that the expected participant, trial, time, gaze, and pupil columns were created correctly.

# 3. Light pupil preprocessing branch

Use the light branch when you need a transparent minimal preprocessing pipeline.

    pupil_summary <- summarise_gazepoint_pupil(master)

    flagged_pupil <- flag_gazepoint_pupil(
      master,
      pupil_col = "pupil"
    )

    interpolated_pupil <- interpolate_gazepoint_pupil(
      flagged_pupil,
      pupil_col = "pupil_for_preprocessing",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    baseline_corrected <- baseline_correct_gazepoint_pupil(
      interpolated_pupil,
      pupil_col = "pupil_interpolated",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global"),
      baseline_window = c(0, 200),
      min_baseline_samples = 1
    )

    smoothed_pupil <- smooth_gazepoint_pupil(
      baseline_corrected,
      pupil_col = "pupil_baseline_corrected",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global"),
      window_samples = 5,
      method = "mean",
      align = "center",
      min_points = 2
    )

Create pupil-window summaries.

These summaries are useful for confirmatory window analyses, where the time window should be specified before testing the main condition effect.

    pupil_windows <- summarise_gazepoint_pupil_windows(
      smoothed_pupil,
      pupil_col = "pupil_smoothed",
      time_col = "time",
      windows = c(0, 500, 1000, 2000, 5000),
      group_cols = c("subject", "MEDIA_ID", "trial_global", "condition"),
      min_valid_samples = 1
    )

    pupil_windows

# 4. Conservative artifact-cleaned branch

Use the conservative branch when blink/trackloss padding, pupil-speed artifacts, or stricter preprocessing decisions are important.

    registry <- create_gazepoint_preprocessing_registry(
      blink_padding_ms = 50,
      interpolation_max_gap_ms = 150,
      smoothing_window_samples = 5,
      baseline_window = c(0, 200)
    )

    artifact_pupil <- flag_gazepoint_pupil_artifacts(
      master,
      registry = registry,
      pupil_col = "pupil",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    interpolated_artifact_pupil <- interpolate_gazepoint_pupil(
      artifact_pupil,
      pupil_col = "pupil_clean",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    baseline_artifact_pupil <- baseline_correct_gazepoint_pupil(
      interpolated_artifact_pupil,
      pupil_col = "pupil_interpolated",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global"),
      baseline_window = c(0, 200),
      min_baseline_samples = 1
    )

    smoothed_artifact_pupil <- smooth_gazepoint_pupil(
      baseline_artifact_pupil,
      pupil_col = "pupil_baseline_corrected",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global"),
      window_samples = 5,
      method = "mean",
      align = "center",
      min_points = 2
    )

    pupil_windows_conservative <- summarise_gazepoint_pupil_windows(
      smoothed_artifact_pupil,
      pupil_col = "pupil_smoothed",
      time_col = "time",
      windows = c(0, 500, 1000, 2000, 5000),
      group_cols = c("subject", "MEDIA_ID", "trial_global", "condition"),
      min_valid_samples = 1
    )

# 5. Pupil quality audits

    gap_audit <- audit_gazepoint_pupil_gaps(
      interpolated_artifact_pupil,
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    baseline_audit <- audit_gazepoint_pupil_baseline(
      baseline_artifact_pupil,
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    imbalance_audit <- audit_gazepoint_pupil_imbalance(
      smoothed_artifact_pupil,
      group_cols = "condition"
    )

    drift_audit <- audit_gazepoint_pupil_drift(
      smoothed_artifact_pupil,
      pupil_col = "pupil_smoothed",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    overlap_audit <- audit_gazepoint_pupil_overlap_risk(
      master,
      time_col = "time",
      event_col = "event_label",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    gap_audit$overview
    baseline_audit$overview
    imbalance_audit$overview
    drift_audit$overview
    overlap_audit$overview

# 6. Reliability and sensitivity checks

The checks below use the conservative pupil-window summary. In a real analysis, compare this branch with the lighter preprocessing branch to assess whether conclusions depend on preprocessing choices.

    pupil_reliability <- audit_gazepoint_pupil_reliability(
      pupil_windows_conservative,
      subject_col = "subject",
      outcome_col = "mean_pupil",
      condition_col = "condition"
    )

    pchip_pupil <- interpolate_gazepoint_pupil_pchip(
      artifact_pupil,
      pupil_col = "pupil_clean",
      time_col = "time",
      group_cols = c("subject", "MEDIA_ID", "trial_global")
    )

    hampel_pupil <- flag_gazepoint_pupil_hampel(
      smoothed_artifact_pupil,
      pupil_col = "pupil_smoothed",
      time_col = "time",
      grouping_cols = c("subject", "MEDIA_ID", "trial_global"),
      window_size_samples = 7,
      k = 3,
      min_valid_samples = 3,
      corrected_col = "pupil_hampel_corrected"
    )

    pupil_reliability$overview
    attr(pchip_pupil, "gp3_pchip_overview")
    attr(hampel_pupil, "gp3_hampel_overview")

# 7. Confirmatory pupil-window LMM

Prepare pupil-window model data:

    pupil_window_model_data <- prepare_gazepoint_pupil_window_model_data(
      pupil_windows_conservative,
      outcome_col = "mean_pupil",
      subject_col = "subject",
      condition_col = "condition",
      window_col = "window_label",
      trial_col = "trial_global",
      valid_samples_col = "n_valid_samples",
      total_samples_col = "n_samples",
      min_valid_samples = 1
    )

    dplyr::count(
      pupil_window_model_data,
      pupil_window_condition,
      pupil_window_label,
      pupil_window_model_status
    )

Fit the confirmatory model:

    pupil_window_lmm <- fit_gazepoint_pupil_window_lmm(
      pupil_window_model_data,
      random_window_slopes = FALSE,
      use_weights = TRUE,
      REML = FALSE
    )

    pupil_window_lmm$model_status
    pupil_window_lmm$formula
    pupil_window_lmm$fixed_effects
    pupil_window_lmm$comparison

Run sensitivity checks:

    pupil_window_sensitivity <- fit_gazepoint_pupil_window_sensitivity(
      pupil_window_model_data,
      model_types = c(
        "unweighted_lmm",
        "weighted_lmm",
        "fixed_lm",
        "weighted_lm"
      ),
      include_condition = TRUE,
      include_window = TRUE,
      include_interaction = TRUE
    )

    pupil_window_sensitivity$comparison
    pupil_window_sensitivity$fixed_effects

# 8. Pupil time-course models

Prepare binned pupil time-course data:

    pupil_gamm_data <- prepare_gazepoint_pupil_gamm_data(
      smoothed_artifact_pupil,
      pupil_col = "pupil_smoothed",
      time_col = "time",
      subject_col = "subject",
      condition_col = "condition",
      trial_col = "trial_global",
      bin_width_ms = 50,
      min_valid_samples = 1
    )

Fit the main pupil GAMM:

    pupil_gamm_fit <- fit_gazepoint_pupil_gamm(
      pupil_gamm_data,
      n_time_basis = 10,
      discrete = TRUE
    )

    pupil_gamm_fit$model_status
    pupil_gamm_fit$formula

Run a gaze-position/PFE sensitivity GAMM:

    pupil_pfe_fit <- fit_gazepoint_pupil_pfe_gamm(
      pupil_gamm_data,
      n_time_basis = 10,
      n_position_basis = 8,
      discrete = TRUE
    )

    pupil_pfe_fit$sensitivity_status
    pupil_pfe_fit$comparison

Prepare and fit a Growth Curve Analysis model:

    pupil_gca_data <- prepare_gazepoint_gca_data(
      pupil_gamm_data,
      pupil_col = "mean_pupil",
      time_col = "time_bin_center_ms",
      subject_col = "subject",
      condition_col = "condition",
      degree = 3,
      orthogonal = TRUE,
      valid_samples_col = "n_valid_samples",
      min_valid_samples = 1
    )

    pupil_gca_fit <- fit_gazepoint_gca(
      pupil_gca_data,
      REML = FALSE
    )

    pupil_gca_fit$model_status
    pupil_gca_fit$comparison

# 9. Cluster permutation and divergence point

Cluster-permutation and divergence-point analyses are exploratory time-course tools. Use enough permutations or bootstrap samples for the final analysis, and avoid using exploratory results to define a later confirmatory window on the same data.

    cluster_data <- prepare_gazepoint_cluster_data(
      pupil_gamm_data,
      outcome_col = "mean_pupil",
      time_col = "time_bin_center_ms",
      subject_col = "subject",
      condition_col = "condition"
    )

    cluster_results <- run_gazepoint_cluster_permutation(
      cluster_data,
      condition_levels = c("control", "treatment"),
      n_permutations = 1000,
      seed = 123
    )

    cluster_summary <- summarise_gazepoint_clusters(cluster_results)

    cluster_summary$overview
    cluster_summary$significant_clusters

Estimate the earliest reliable divergence point:

    divergence <- estimate_gazepoint_divergence_point(
      data = pupil_gamm_data,
      outcome_col = "mean_pupil",
      time_col = "time_bin_center_ms",
      condition_col = "condition",
      participant_col = "subject",
      comparison = c("control", "treatment"),
      bootstrap_unit = "participant",
      n_boot = 1000,
      consecutive_points = 2,
      seed = 123
    )

    divergence$overview
    divergence$divergence_point

Do not use a detected exploratory cluster to define a confirmatory window and then retest that same window as if it had been specified a priori.

# 10. Reporting

Run diagnostics and create manuscript-ready tables:

    pupil_diagnostics <- diagnose_gazepoint_glmm(
      pupil_window_lmm,
      model_name = "pupil_window_lmm",
      use_dharma = FALSE
    )

    pupil_model_summary <- tidy_gazepoint_model_summary(
      pupil_window_lmm,
      model_name = "pupil_window_lmm"
    )

    pupil_diagnostics$overview
    pupil_model_summary$fixed_effects

Create a reporting checklist:

    reporting <- create_gazepoint_reporting_checklist(
      data = master,
      objects = list(
        validation = validation,
        gap_audit = gap_audit,
        baseline_audit = baseline_audit,
        pupil_model = pupil_window_lmm,
        diagnostics = pupil_diagnostics
      ),
      analysis_type = "pupil",
      study_title = "Gazepoint pupil study"
    )

    reporting$overview
    reporting$checklist