Tutorial: synthetic gaze to auditable QC¶
This tutorial runs entirely on deterministic synthetic data bundled through simulate_gaze(). It is useful for checking an installation, learning the canonical schema, and understanding GazeForge's quality-control philosophy before touching empirical data.
Synthetic example
This workflow is for software learning and pipeline validation. It is not empirical evidence and does not validate a tracker, QC threshold, or scientific exclusion rule.
1. Simulate a small recording¶
from gazeforge import simulate_gaze
raw = simulate_gaze(
n_participants=4,
n_trials=3,
samples_per_trial=240,
sampling_rate_hz=60,
random_state=42,
)
print(raw.head())
print(raw.shape)
The synthetic table contains participant and trial identity, millisecond timestamps, pixel coordinates, and pupil values. Occasional missing gaze samples and saccade-like jumps make it useful for a first QC pass.
2. Canonicalise explicitly¶
from gazeforge import canonicalize_gaze
gaze = canonicalize_gaze(
raw,
sampling_rate_hz=60,
screen_size_px=(1920, 1080),
)
print(gaze.sampling_rate_hz)
print(gaze.data.columns.tolist())
The important idea is not the synthetic source. It is that the analysis now has an explicit sampling rate, coordinate convention, participant/trial boundary, and canonical table.
3. Add anomaly flags without deleting rows¶
from gazeforge import ai_flag_anomalies
flagged = ai_flag_anomalies(
gaze.data,
sampling_rate_hz=gaze.sampling_rate_hz,
random_state=42,
)
print(flagged[["qc_anomaly_score", "qc_flag"]].head())
ai_flag_anomalies() adds an Isolation Forest score and binary flag. The original rows remain present.
That distinction matters: an unusual sample is a review target, not automatically an invalid observation.
4. Summarise trial quality¶
from gazeforge import score_trial_quality
quality = score_trial_quality(
flagged,
screen_size_px=(1920, 1080),
)
print(
quality[
[
"participant_id",
"trial_id",
"missing_rate",
"offscreen_rate",
"anomaly_rate",
"large_gap_rate",
"quality_score",
]
].to_string(index=False)
)
The summary is descriptive. A low quality_score should trigger inspection and a prespecified decision rule; it should not silently remove a trial.
5. Inspect the rows behind a suspicious trial¶
worst = quality.sort_values("quality_score").iloc[0]
mask = (
(flagged["participant_id"] == worst["participant_id"])
& (flagged["trial_id"] == worst["trial_id"])
)
review = flagged.loc[
mask,
[
"participant_id",
"trial_id",
"timestamp_ms",
"x_px",
"y_px",
"pupil",
"qc_anomaly_score",
"qc_flag",
],
]
print(review.head(20).to_string(index=False))
This is the intended review loop: summary → candidate problem → underlying rows → documented decision.
6. Continue to a transparent event baseline¶
Once the QC layer is understood, continue with the I-VT baseline tutorial. It uses the same synthetic recording and creates deterministic fixation/saccade/noise labels without fitting a learned model.
Complete runnable example¶
The repository contains the same workflow as examples/01_synthetic_qc.py.
What to carry into empirical work¶
Record at least:
- GazeForge version or commit SHA;
- tracker/export source;
- native sampling rate and any inferred/declared rate;
- coordinate basis and screen geometry;
- QC algorithm and random seed;
- prespecified exclusion/review rule;
- number of samples/trials flagged and number actually excluded;
- whether the raw/canonical rows were retained for audit.
The core principle is simple: QC should create inspectable evidence before it creates an exclusion.