This page was generated from a Jupyter notebook. View on GitHub Binder badge

Quickstart#

This page walks through a typical analysis: load a recording, look at it, deal with blinks, filter the signal and compute the average pupil response to an event. Every step links to a page with more details.

We start by importing the package. u holds the units (e.g., u.ms, u.sec) that can be attached to times and distances. By default, pypillometry logs every step; here we only show warnings.

[1]:
import pypillometry as pp
from pypillometry import u
import matplotlib.pyplot as plt
import pandas as pd

pp.logging_set_level("WARNING")  # show only warnings, not every step

Load data#

pypillometry comes with a few example datasets. rlmw_002_short is the first 40 seconds of a binocular recording from a reinforcement-learning task, recorded with an Eyelink eye tracker at 500 Hz.

[2]:
d = pp.get_example_data("rlmw_002_short")
d
[2]:

EyeData — rlmw_002_short

Samples
20,465
Sampling rate
500.0 Hz
Duration
0.68 min (start 0.00, end 0.68)
Eyes
left, right
Variables
left_x, left_y, left_pupil, right_x, right_y, right_pupil
Events
40
Size
1.4MiB
Signal overview
left_pupil
left_pupil sparkline
left_x
left_x sparkline
left_y
left_y sparkline
right_pupil
right_pupil sparkline
right_x
right_x sparkline
right_y
right_y sparkline
Data glimpse
EyeDataDict(vars=6,n=20465,shape=(20465,)):
  left_x (float64): 655.6, 655.7, 655.0, 654.5, 655.0...
  left_y (float64): 599.9, 598.9, 597.6, 597.8, 597.8...
  left_pupil (float64): 1121.0, 1122.0, 1124.0, 1126.0, 1126.0...
  right_x (float64): 773.1, 773.8, 774.8, 776.3, 776.7...
  right_y (float64): 594.2, 593.9, 596.5, 597.4, 597.3...
  right_pupil (float64): 949.0, 951.0, 956.0, 959.0, 960.0...
Recent history
  1. reset_time()

The result is an EyeData object holding pupil size and gaze position (x, y) of both eyes, the time vector, the events (triggers sent by the experiment) and a description of the experimental setup. At the bottom, you can see the history of all operations applied to the data.

Your own Eyelink files are read with EyeData.from_eyelink():

d = pp.EyeData.from_eyelink("recording.edf")

See Loading data for other formats.

The data is stored under keys of the form <eye>_<variable>:

[3]:
print(d.eyes, d.variables)
d["left_pupil"][:10]
['left', 'right'] ['x', 'y', 'pupil']
[3]:
masked_array(data=[1121.0, 1122.0, 1124.0, 1126.0, 1126.0, 1124.0, 1123.0,
                   1122.0, 1122.0, 1122.0],
             mask=[False, False, False, False, False, False, False, False,
                   False, False],
       fill_value=nan)

Look at the data#

All plotting functions are available under d.plot. Here we plot the pupil size of both eyes for the first 20 seconds; the vertical lines mark events and the shaded areas are blinks (none have been detected yet).

[4]:
plt.figure(figsize=(12, 4))
d.plot.plot_pupil(plot_range=(0, 20) * u.sec);
../_images/getting_started_quickstart_7_0.png

For long recordings, the interactive viewer is faster: pp.view(d) opens the data in a separate window (see Interactive tools).

Data quality#

Before processing, it is worth checking how good the recording is. quality_report() summarizes missing data, dropouts and noise of the pupil, missing and off-screen samples and precision of the gaze, the agreement between the eyes and the calibration errors stored in the Eyelink file. Each measure is labeled “good”, “ok”, “poor” or “bad” (the thresholds are rules of thumb); after preprocessing, the report also covers blinks and interpolated data. See Data quality for the measures and how to change the thresholds.

[5]:
d.quality_report()
[5]:

Quality report — rlmw_002_short (EyeData)

ok
Recording
duration 0.682 min
sampling rate 500 Hz
samples 20465
events 40
time gaps 0
Pupil
left right
missing data 2.7% good 1.9% good
dropouts 16.1 /min good 14.7 /min good
longest dropout 0.14 s good 0.12 s good
noise 0.094% good 0.11% good
noise (SD) 1.05 1.05
jumps 0 /min good 0 /min good
Gaze
left right
missing data 2.7% good 1.9% good
off-screen 0.21% good 0.24% good
precision 0.0202 deg good 0.02 deg good
Binocular
left/right
pupil correlation 0.866 ok
gaze disparity 1.61 deg ok
Thresholds
pupil.missing_pctgood <= 10, ok <= 20, poor <= 40, else bad
pupil.dropout_rategood <= 30, ok <= 60, poor <= 150, else bad
pupil.longest_dropoutgood <= 2, ok <= 5, poor <= 30, else bad
pupil.noisegood <= 0.5, ok <= 1, poor <= 2, else bad
pupil.jump_rategood <= 1, ok <= 10, poor <= 100, else bad
gaze.missing_pctgood <= 10, ok <= 20, poor <= 40, else bad
gaze.dropout_rategood <= 30, ok <= 60, poor <= 150, else bad
gaze.longest_dropoutgood <= 2, ok <= 5, poor <= 30, else bad
gaze.offscreen_pctgood <= 5, ok <= 15, poor <= 30, else bad
gaze.precision_deggood <= 0.05, ok <= 0.1, poor <= 0.2, else bad
binocular.pupil_correlationgood >= 0.9, ok >= 0.8, poor >= 0.6, else bad
binocular.gaze_disparity_deggood <= 1, ok <= 2, poor <= 3, else bad
blinks.rategood <= 20, ok <= 35, poor <= 70, else bad
blinks.time_pctgood <= 10, ok <= 20, poor <= 35, else bad
artifacts.pupil_masked_pctgood <= 10, ok <= 20, poor <= 35, else bad
artifacts.gaze_masked_pctgood <= 10, ok <= 20, poor <= 35, else bad
artifacts.interpolated_pctgood <= 10, ok <= 20, poor <= 30, else bad
calibration.mean_errorgood <= 0.5, ok <= 1, poor <= 1.5, else bad
calibration.max_errorgood <= 1, ok <= 1.5, poor <= 2, else bad
The overall label is the worst label of all measures. Hover over a measure for a description.

Events#

The events are the triggers that the experiment software sent to the eye tracker. d.events holds them as an Events object, which displays as a table:

[6]:
events = d.events
events[:8]
[6]:
Onset (ms) Label
0 5177.0 F_1
1 5677.0 S_1_1_5
2 6773.0 C_1_5_2
3 7010.0 R_1_15
4 8776.0 F_2
5 9575.0 S_2_6_2
6 10581.0 C_2_6_1
7 10808.0 R_2_20

Total: 8 events

In this task, each trial has four events whose labels start with F, S, C and R, followed by the trial number and some trial-specific information. filter() selects events, e.g., all labels containing "S_":

[7]:
events.filter("S_")
[7]:
Onset (ms) Label
0 5677.0 S_1_1_5
1 9575.0 S_2_6_2
2 12574.0 S_3_2_1
3 16473.0 S_4_4_1
4 20388.0 S_5_3_5
5 23987.0 S_6_4_5
6 27002.0 S_7_3_1
7 30601.0 S_8_6_5
8 34799.0 S_9_4_6
9 38415.0 S_10_2_5

Total: 10 events

More in the Events tutorial.

Filtering#

Pupil size changes slowly, so high-frequency noise is usually removed with a low-pass filter (here, 4 Hz):

[10]:
d.pupil.lowpass_filter(cutoff=4);

Every step was recorded in the object’s history:

[11]:
d.print_history()
* rlmw_002_short
└ reset_time()
 └ pupil.blinks_detect()
  └ pupil.blinks_merge_close(distance=100 ms)
   └ pupil.blinks_interpolate()
    └ pupil.lowpass_filter(cutoff=4)

Note that the method calls (d.pupil.blinks_interpolate(), d.pupil.lowpass_filter()) changed d itself (they work in place), whereas d | step returned a changed copy. Use inplace=False or d.copy() to keep the original with a method call; see Pipelines and history.

Save the result#

write_file() saves the processed object including its history; read it back with pp.EyeData.from_file():

d.write_file("subject01.pypil")
d = pp.EyeData.from_file("subject01.pypil")

Next steps#

  • The User guide explains the concepts used here in more detail.

  • The Tutorials contain worked examples for specific tasks.

  • The API reference lists all classes and functions.