Tutorial: Processing Eye-Tracking Data with PyNeon#
Step 1: Loading Sample Data#
First, we’ll load sample eye-tracking data provided by PyNeon.
[1]:
import numpy as np
from pyneon import NeonRecording, get_sample_data
from pyneon.preprocess import *
# Load the sample recording
recording_dir = get_sample_data("OfficeWalk") / "Timeseries Data" / "walk1-e116e606"
recording = NeonRecording(recording_dir)
Step 2: Constructing Event Times#
We’ll create a list of event times from 0 to 100 seconds at intervals of 0.1 seconds. These events will serve as reference points for creating epochs.
[2]:
# Create a list of event times from 0 to 100 seconds at 0.1-second intervals
tlist = np.arange(0, 100, 0.1)
global_ref_time = recording.start_time
# Construct event times DataFrame
event_times = construct_event_times(
t_refs=tlist,
t_before=0.1, # 0.1 seconds before the event
t_after=0.5, # 0.5 seconds after the event
description="test_event",
global_t_ref=global_ref_time,
time_unit="s", # Specify that t_refs are in seconds
)
# Display the first few event times
print(event_times.head())
t_ref t_before t_after description
0 1.725032e+18 100000000.0 500000000.0 test_event
1 1.725032e+18 100000000.0 500000000.0 test_event
2 1.725032e+18 100000000.0 500000000.0 test_event
3 1.725032e+18 100000000.0 500000000.0 test_event
4 1.725032e+18 100000000.0 500000000.0 test_event
Step 3: Verifying Event Intervals#
Check the average interval between events to confirm they are correctly spaced.
[3]:
# Calculate the average difference between subsequent event times
average_interval = np.mean(np.diff(event_times["t_ref"]))
print(f"Average interval between events: {average_interval} nanoseconds")
Average interval between events: 100000000.0 nanoseconds
This confirms that events are spaced 0.1 seconds (100,000,000 nanoseconds) apart.
Step 4: Creating Epochs from the Data#
Use the create_epoch function to create epochs from the gaze data based on the event times.
[4]:
# Create epochs from the gaze data
epochs_df, annotated_data = create_epoch(recording.gaze.data, event_times)
[5]:
print(epochs_df.iloc[0])
epoch id 29
t_ref 1725032224427000064
t_before 100000000.0
t_after 500000000.0
description test_event
epoch data timestamp [ns] gaze x [px] gaze y [p...
Name: 0, dtype: object
Every row in the epochs_df file has information about the epoch as well as the data assigned to this epoch.
[26]:
print(annotated_data.iloc[0])
timestamp [ns] 1725032224852161732
gaze x [px] 1067.486
gaze y [px] 620.856
worn True
fixation id 1
blink id <NA>
azimuth [deg] 16.21303
elevation [deg] -0.748998
time [s] 0.0
epoch id 34
description test_event
t_rel -74838272.0
Name: 0, dtype: object
On the other hand, annotated data has the information about the current epoch appended at the end.
Alternatively, an epoch object can also be created without an event_times object but rather the info needed to create one
[29]:
epochs_df, annotated_data = create_epoch(
recording.gaze.data,
times_df=None,
t_refs=tlist,
t_before=0.1,
t_after=0.5,
global_t_ref=global_ref_time,
time_unit="s",
description="test_event",
)
This object is equivalent to the ones created before
Step 5: Initializing the Epoch Class#
Initialize the Epoch class with the gaze data and event times.
[14]:
# Initialize the Epoch object
epochs = Epoch(recording.gaze.data, event_times)
Step 6: Converting Epochs to NumPy Array#
Convert the epochs into a NumPy array, specifying the columns of interest
[15]:
# Convert epochs to NumPy array, selecting specific columns
ep_np, info = epochs.to_numpy(columns=["gaze x [px]", "gaze y [px]"])
# Display information about the epochs
print("Column IDs:", info["column_ids"])
print("Time relative to reference (s):", info["t_rel"] * 1e-9)
print("Shape of epochs array:", ep_np.shape)
NaN values were found in the data.
Column IDs: ['gaze x [px]', 'gaze y [px]']
Time relative to reference (s): [-0.1 -0.09 -0.08 -0.07 -0.06 -0.05 -0.04 -0.03 -0.02 -0.01 0. 0.01
0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1 0.11 0.12 0.13
0.14 0.15 0.16 0.17 0.18 0.19 0.2 0.21 0.22 0.23 0.24 0.25
0.26 0.27 0.28 0.29 0.3 0.31 0.32 0.33 0.34 0.35 0.36 0.37
0.38 0.39 0.4 0.41 0.42 0.43 0.44 0.45 0.46 0.47 0.48 0.49
0.5 ]
Shape of epochs array: (955, 61, 2)
Column IDs: The data channels extracted. Times: The common time grid for all epochs. Shape: Indicates there are 1000 epochs, each with 61 time points and 2 data channels.
Step 7: Averaging Across Epochs#
Compute the average across all epochs to get the mean time-series.
[16]:
# Average across epochs (resulting in a 2D array)
integrated_epochs = np.nanmean(ep_np, axis=0)
print("Shape after averaging across epochs:", integrated_epochs.shape)
Shape after averaging across epochs: (61, 2)
C:\Users\jan-gabriel.hartel\AppData\Local\Temp\ipykernel_12180\2544218633.py:2: RuntimeWarning: Mean of empty slice
integrated_epochs = np.nanmean(ep_np, axis=0)
The resulting array has 61 time points and 2 channels.
Step 8: Averaging Over Time#
Further, average over time to get a single value per channel.
[17]:
# Average over time (resulting in a 1D array)
integrate_in_time = np.nanmean(integrated_epochs, axis=0)
print("Averaged values over time:", integrate_in_time)
Averaged values over time: [784.02903257 539.72164082]
his gives the overall mean for each data channel.
Conclusion#
In this tutorial, we’ve demonstrated how to:
Load sample eye-tracking data using PyNeon.
Construct event times for epoching the data.
Create epochs from continuous gaze data.
Convert epochs into a NumPy array for analysis.
Perform averaging across epochs and over time.
These steps are essential for preprocessing eye-tracking data and can be extended for more advanced analyses.