This file was created from the following Jupyter-notebook: eyes_and_variables.ipynb
Interactive version: Binder badge

Processing monocular and binocular data

[1]:
%load_ext autoreload
%autoreload 2
import sys
sys.path.insert(0,"..")
import pypillometry as pp
import numpy as np

Data in pypillometry can contain different variables from different eyes. The variables and eyes supported when importing raw data are

  • left_x, right_x (x-coordinate in screen coordinates from the eyetracker)

  • left_y, right_y (y-coordinate in screen coordinates from the eyetracker)

  • left_pupil, right_pupil (pupil size from left and right eye)

Depending on which class is chosen (PupilData, GazeData or EyeData), some of these variables are required:

  • PupilData: requires at least one of left_pupil, right_pupil (or both)

  • GazeData: requires at least one of (left_x, left_y) and/or (right_x, right_y)

  • EyeData: requires x,y and pupil from at least one eye

For example, let’s simulate some basic data:

[2]:
left_x = np.random.randn(1000)
right_x = np.random.randn(1000)
left_y = np.random.randn(1000)
right_y = np.random.randn(1000)
left_pupil = np.random.randn(1000)
right_pupil = np.random.randn(1000)
time = np.arange(1000)

# these are all ok
dpupil = pp.PupilData(left_pupil=left_pupil, right_pupil=right_pupil, time=time)
dgaze = pp.GazeData(left_x=left_x, left_y=left_y, right_x=right_x, right_y=right_y, time=time)
deye = pp.EyeData(left_x=left_x, left_y=left_y, left_pupil=left_pupil, time=time)

# these are not ok
#pp.PupilData(left_x=left_x, time=time)
#pp.GazeData(left_x=left_x, time=time)
pp.EyeData(left_x=left_x, time=time)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 17
     12 deye = pp.EyeData(left_x=left_x, left_y=left_y, left_pupil=left_pupil, time=time)
     14 # these are not ok
     15 #pp.PupilData(left_x=left_x, time=time)
     16 #pp.GazeData(left_x=left_x, time=time)
---> 17 pp.EyeData(left_x=left_x, time=time)

File ~/Dropbox/work/projects/pupil/pypillometry/docs/../pypillometry/eyedata/eyedata.py:96, in EyeData.__init__(self, time, left_x, left_y, left_pupil, right_x, right_y, right_pupil, event_onsets, event_labels, sampling_rate, screen_resolution, physical_screen_size, screen_eye_distance, name, fill_time_discontinuities, keep_orig, notes, inplace, use_cache, cache_dir, max_memory_mb)
     94 logger.debug("Creating EyeData object")
     95 if (left_x is None or left_y is None) and (right_x is None or right_y is None):
---> 96     raise ValueError("At least one of the eye-traces must be provided (both x and y)")
     97 self.data=EyeDataDict(left_x=left_x, left_y=left_y, left_pupil=left_pupil,
     98                         right_x=right_x, right_y=right_y, right_pupil=right_pupil)
    100 self._init_common(time, sampling_rate,
    101                   event_onsets, event_labels,
    102                   name, fill_time_discontinuities,
   (...)
    105                   cache_dir=cache_dir,
    106                   max_memory_mb=max_memory_mb)

ValueError: At least one of the eye-traces must be provided (both x and y)

Once the data is loaded, we can check which variables and eyes are available using the .eyes and .variables attribute:

[ ]:
deye.eyes, deye.variables
(['left'], ['x', 'pupil', 'y'])

Simply printing an object will also show what data sources are available and give a glimpse into the data structure:

[3]:
deye
[3]:
EyeData(petokiga, 55.6KiB):
 n                   : 1000
 sampling_rate       : 1000.0
 data                : ['left_x', 'left_y', 'left_pupil']
 nevents             : 0
 screen_limits       : not set
 physical_screen_size: not set
 screen_eye_distance : not set
 duration_minutes    : 0.016666666666666666
 start_min           : 0.0
 end_min             : 0.01665
 parameters          : {}
 glimpse             : EyeDataDict(vars=3,n=1000,shape=(1000,)):
  left_x (float64): -0.05614305080280796, -0.6414526083279584, -1.704862857943198, 1.4798705854852807, 0.8472706708383512...
  left_y (float64): -0.7499467879300401, 0.20023206315811148, 1.2681504964811687, -0.9232746614049664, -1.6396223281209275...
  left_pupil (float64): 0.28284068252769706, -0.4969359675178975, -1.7109730132713936, -1.024197014557436, -0.7792117291273035...

 eyes                : ['left']
 nblinks             : {}
 blinks              : {'left': None}
 params              : {}
 History:
 *
 └ fill_time_discontinuities()
[4]:
d = pp.get_example_data("rlmw_002_short")
d.variables, d.eyes
[4]:
(['pupil', 'y', 'x'], ['left', 'right'])

Almost all of pypillometry’s functions have keyword arguments eyes= and variables= that specify which eyes/variables to operate on. By default, all of the variables and eyes are processed.

For example, we can run the scale() function that will re-scale the data to have mean=0 and standard devation 1. Here, we use the context manager pp.loglevel("DEBUG") to get output from pypillometry internals:

[5]:
with pp.loglevel("DEBUG"):
    deye.scale(eyes="left")
pp: 12:59:13 | DEBUG    | _get_eye_var:194 | scale(): eyes=['left'], vars=['pupil', 'y', 'x']
pp: 12:59:13 | DEBUG    | scale:820 | Mean: {'left': {'pupil': 0.0017743030582778934, 'y': -0.02958842274723986, 'x': 0.025062813034298286}}
pp: 12:59:13 | DEBUG    | scale:821 | SD: {'left': {'pupil': 0.9685169385611292, 'y': 1.012354831683368, 'x': 0.9977838504196321}}

The output shows that all variables from the left eye have been processed.

Which functions work on which data?

Not all of pypillometrys functions can be applied to all variables. Functions that are specific to pupil data have the prefix pupil_* and functions that only work on gaze (x/y) data, have the prefix gaze_. The other functions will operate on all variables (which may or may not make sense, it is up to you to check!).

Functions that work only on pupillometric data are implemented in the PupilData class and are therefore not available when using GazeData:

[32]:
dpupil.pupil_blinks_detect() # works fine
dgaze.pupil_blinks_detect() # fails
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[32], line 2
      1 dpupil.pupil_blinks_detect() # works fine
----> 2 dgaze.pupil_blinks_detect()

AttributeError: 'GazeData' object has no attribute 'pupil_blinks_detect'

Functions that are implemented in GenericEyeData will work for both:

[34]:
dpupil.scale() # works
dgaze.scale() # works
[34]:
GazeData(gememoda, 103.9KiB):
 n                   : 1000
 sampling_rate       : 1000.0
 data                : ['left_x', 'left_y', 'right_x', 'right_y', 'mean_y', 'mean_x']
 nevents             : 0
 screen_limits       : not set
 physical_screen_size: not set
 screen_eye_distance : not set
 duration_minutes    : 0.016666666666666666
 start_min           : 0.0
 end_min             : 0.01665
 parameters          : {scale: {...}}
 glimpse             : EyeDataDict(vars=6,n=1000,shape=(1000,)):
  left_x (float64): -0.0813862278918966, -0.6679957999740566, -1.7337679601147598, 1.458039004980029, 0.8240340404971094...
  left_y (float64): -0.711567073755129, 0.22701574459144955, 1.2819012451103702, -0.8827796447335403, -1.5903849667971501...
  right_x (float64): 0.9626973487677861, -0.17827876962019754, 0.1106137723940685, -0.9696903516215334, 0.6644680267943046...
  right_y (float64): 0.31340189774712973, 1.1308736355320714, -0.7814852910860114, -1.2597233647796173, -0.3645995245346349...
  mean_y (float64): -0.2900743012870026, 0.9746687313634781, 0.3671378610511327, -1.541330111846402, -1.4116343476312987...
  mean_x (float64): 0.6144940257680912, -0.5963646337735499, -1.1476480335279726, 0.353192240734368, 1.0459758453244563...

 History:
 *
 └ fill_time_discontinuities()
  └ merge_eyes()
   └ scale()

Creating new variables or eyes

In some cases, new variables or “eyes” can be created. For example, we might consider to reduce a binocular dataset to one where we average the timeseries from the two eyes. In that case, we can use function merge_eyes():

[6]:
dpupil.merge_eyes(eyes=["left", "right"], variables=["pupil"], method="mean")
[6]:
PupilData(debibiki, 56.3KiB):
 n               : 1000
 sampling_rate   : 1000.0
 eyes            : ['mean', 'left', 'right']
 data            : ['left_pupil', 'right_pupil', 'mean_pupil']
 nevents         : 0
 nblinks         : {}
 blinks          : {'mean': None, 'left': None, 'right': None}
 duration_minutes: 0.016666666666666666
 start_min       : 0.0
 end_min         : 0.01665
 params          : {}
 glimpse         : EyeDataDict(vars=3,n=1000,shape=(1000,)):
  left_pupil (float64): 0.28284068252769706, -0.4969359675178975, -1.7109730132713936, -1.024197014557436, -0.7792117291273035...
  right_pupil (float64): 2.5061323051140176, 0.5500808555889285, -0.4194523189113897, 0.5384978966510412, -1.3899010906520874...
  mean_pupil (float64): 1.3944864938208574, 0.026572444035515508, -1.0652126660913916, -0.24284955895319738, -1.0845564098896956...

 History:
 *
 └ fill_time_discontinuities()
  └ merge_eyes(eyes=['left', 'right'],variables=['pupil'],method=mean)

We can see that a new “eye” with variable “pupil” called mean_pupil has been created. In this case, the original data left_pupil and right_pupil have been preserved (this can be changed by using keep_eyes=False).

In other cases, the package can create new variables. For example, the function pupil_estimate_baseline() will estimate tonic fluctuation in the pupil (see https://osf.io/preprints/psyarxiv/7ju4a_v2/) and will create a new variable <eye>_baseline.

[16]:
d = pp.get_example_data("rlmw_002_short")
d.pupil_estimate_baseline()
d.variables
[16]:
['pupil', 'baseline', 'y', 'x']

Debugging

If you want to be sure what steps pupillometry is taking, and which variables/eyes are being processed,
you can use the pp.loglevel() context manager to temporarily increase the logging level (the result is a rather lengthy and detailed debug-output):
[28]:
with pp.loglevel("DEBUG"):
    d.pupil_estimate_baseline()

pp: 13:12:00 | DEBUG    | _get_eye_var:194 | pupil_estimate_baseline(): eyes=['left', 'right'], vars=['pupil', 'baseline', 'y', 'x']
pp: 13:12:00 | DEBUG    | pupil_estimate_baseline:413 | Estimating baseline for eye left
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:198 | Downsampling factor is 50
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:208 | Downsampling done
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:214 | Peak-detection done, 42 peaks detected
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:217 | B-spline matrix built, dims=(410, 46)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:228 | Compiling Stan model: /home/mmi041/Dropbox/work/projects/pupil/pypillometry/docs/../pypillometry/stan/baseline_model_asym_laplac.stan
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:250 | Optimizing Stan model
13:12:00 - cmdstanpy - INFO - Chain [1] start processing
13:12:00 - cmdstanpy - INFO - Chain [1] done processing
13:12:00 - cmdstanpy - WARNING - The default behavior of CmdStanVB.stan_variable() will change in a future release to return the variational sample, rather than the mean.
To maintain the current behavior, pass the argument mean=True
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:259 | Estimating PRF model (NNLS)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:270 | 2nd Peak-detection done, 32 peaks detected
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:274 | 2nd B-spline matrix built, dims=(410, 36)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:291 | Optimizing 2nd Stan model
13:12:00 - cmdstanpy - INFO - Chain [1] start processing
13:12:00 - cmdstanpy - INFO - Chain [1] done processing
13:12:00 - cmdstanpy - WARNING - The default behavior of CmdStanVB.stan_variable() will change in a future release to return the variational sample, rather than the mean.
To maintain the current behavior, pass the argument mean=True
pp: 13:12:00 | DEBUG    | pupil_estimate_baseline:413 | Estimating baseline for eye right
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:198 | Downsampling factor is 50
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:208 | Downsampling done
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:214 | Peak-detection done, 42 peaks detected
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:217 | B-spline matrix built, dims=(410, 46)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:228 | Compiling Stan model: /home/mmi041/Dropbox/work/projects/pupil/pypillometry/docs/../pypillometry/stan/baseline_model_asym_laplac.stan
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:250 | Optimizing Stan model
13:12:00 - cmdstanpy - INFO - Chain [1] start processing
13:12:00 - cmdstanpy - INFO - Chain [1] done processing
13:12:00 - cmdstanpy - WARNING - The default behavior of CmdStanVB.stan_variable() will change in a future release to return the variational sample, rather than the mean.
To maintain the current behavior, pass the argument mean=True
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:259 | Estimating PRF model (NNLS)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:270 | 2nd Peak-detection done, 35 peaks detected
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:274 | 2nd B-spline matrix built, dims=(410, 39)
pp: 13:12:00 | DEBUG    | baseline_envelope_iter_bspline:291 | Optimizing 2nd Stan model
13:12:00 - cmdstanpy - INFO - Chain [1] start processing
13:12:00 - cmdstanpy - INFO - Chain [1] done processing
13:12:00 - cmdstanpy - WARNING - The default behavior of CmdStanVB.stan_variable() will change in a future release to return the variational sample, rather than the mean.
To maintain the current behavior, pass the argument mean=True
This file was created from the following Jupyter-notebook: eyes_and_variables.ipynb
Interactive version: Binder badge