Pupil Preprocessing

This is an R markdown file explaining pupil pre-processing functions contained in the gazeR package. The example dataset is from a lexical decision task that had individuals judge the lexicality of printed and cursive stimuli. Cursive stimuli are ambiguous and non-segmented making them really hard to recognize. It was predicted that it should be harder to recognize cursive words than printed words. Good thing we have this new package to help us test this hypothesis!

Please see our pre-print for a more in-depth walkthrough of the functions below.

Loading package

remotes::install_github("dmirman/gazer")
library(gazer)
library(data.table)
library(tidyverse)
library(knitr)

Preparing Your Data

Before using this package, a number of steps are required. In order for gazeR to work you need several columns.

You should also include other columns of interest.

Colnames can be changes to match gazer conventions


gazer_pupil <- make_gazer(data, subject="subject", trial="subject", time="time", x="x", y="y", pupil=NULL)

Loading Data

We need to read in the file that contains data from 3 participants. Given how large pupil files can be, I did this to reduce computational processing.

pupil_path <- system.file("extdata", "pupil_sample_files_edf.xls.gz", package = "gazer")
pupil_sub1<-fread(pupil_path)

In reality, you will have many Ss files. The function merge_gazer_files will take all your pupil files from a folder path and merge them together.

pupil_files<-merge_gazer_files(file_list, filetype="edf")

Behavioral Data

If you are also interested in analyzing behavioral data (RTs and accuracy), the behave_pupil function will cull the important behavioral data from the Sample Report. behave_pupil will return a data frame without errors when omiterrors=TRUE or a data frame with errors for accuracy/error analysis when omiterrors=FALSE. The columns relevant for your experiment need to be specified within the behave_col names argument. This does not eliminate outliers. You must use your preferred method. I recommend the very good trimr package by Jim Grange.

behave_data<-behave_pupil(pupil_sub1, omiterrors = FALSE, behave_colnames = c("subject","script","alteration", "trial", "target","accuracy","rt", "block", "cb"))

Interpolation and Filter of Pupil Values

Smoothing

Pupil data can be extremely noisy! There are many ways to smooth pupil data. In gazeR, you can use a n-point moving average or a hanning filter. To smooth the data using a n-point moving average set the filter argument to “moving.” To use this function, you need to specify the column that contains the interpolated pupil values and how many samples you want to average over. In this example, we use a 5-point moving average (n= 5).

Interpolation

Missing data stemming from blinks or failure of the eye tracker need to be interpolated.smooth_interpolate_pupil searches the data and reconstructs the pupil size for each trial from the relevant samples using either linear interpolation [@Bradley2008; @Cohen2015; @Siegle2003;@Steinhauer2000] or cubic-spline interpolation (@Mathot2018). If extendblinks argument = FALSE, samples with blinks are turned into “NAs” and are then interpolated linearly or by cubic interpolation using the type argument. This function returns a tibble with a column called interp which contains interpolated values. As an important note, if you had Data Viewer extend blinks, the extendblink argument must be set to FALSE. If you used our function, the extendblink argument should be set to TRUE. It is important to note that SR only extends the blink column and does not set blinks to NA in the sample report. For this example, we will set extendblinks to false and use cubic interpolation.

smooth_interp3 <- smooth_interpolate_pupil(pup_extend, pupil="pupil", extendpupil="extendpupil", extendblinks=TRUE, step.first="interp",filter="moving",  maxgap=Inf, type="linear", hz=250, n=5)
#> Performing linear interpolation
#> Smoothing the pupil trace with moving average

Baseline correction

To control for variability arising from non-task related processing, baseline correction is commonly done. There are several different types of baseline correction. In a recent paper by Mathot et al. (2018), it was recommended that a subtractive baseline correction be done based on the median. The baseline_correction_pupil_msg function finds the median pupil size during a specified baseline period for each trial. In this example, we use messages to calcuate our baseline. We take 100ms before the target event.

baseline_pupil<-baseline_correction_pupil_msg(smooth_interp3, pupil_colname='pup_interp', baseline_dur=100, event="target", baseline_method = "sub")
#> Calculating median baseline from:target
#> Joining with `by = join_by(subject, trial)`
#> Joining with `by = join_by(subject, trial)`
#> Merging baseline
#> Performing subtractive baseline correction

head(baseline_pupil)
#> # A tibble: 6 × 19
#>   subject trial baseline extendpupil  time pupil     x     y blink message   acc
#>   <chr>   <int>    <dbl>       <int> <int> <int> <dbl> <dbl> <int> <chr>   <int>
#> 1 11c.edf     1    3584.        3635     0  3635  972.  563.     0 MODERE…     1
#> 2 11c.edf     1    3584.        3634     4  3634  973.  565.     0 <NA>        1
#> 3 11c.edf     1    3584.        3626     8  3626  972.  567.     0 <NA>        1
#> 4 11c.edf     1    3584.        3622    12  3622  971   566.     0 <NA>        1
#> 5 11c.edf     1    3584.        3621    16  3621  972.  566.     0 <NA>        1
#> 6 11c.edf     1    3584.        3621    20  3621  972.  566.     0 <NA>        1
#> # ℹ 8 more variables: block <int>, rt <int>, item <chr>, script <chr>,
#> #   alt <chr>, interp <dbl>, pup_interp <dbl>, baselinecorrectedp <dbl>

Missing Data

Subjects and trials with a lot of missing data–due to blinks–should be removed. count_missing_pupil will remove subjects and items that pass a specified missing threshold (we have it set at .2, but users can change this to whatever value they would like). The percentage of subjects and trials thrown out are returned for reporting.


pup_missing<-count_missing_pupil(baseline_pupil, pupil="pupil", missingthresh = .2)
#> % trials excluded:0.079
#> Participants taken out:character(0)

Median Absoulate Deviation (MAD)

Artifacts can arise from quick changes in pupil size (Kret & Sjak-Shie, in press). The max_dilation function calculates the normalized dilation speed, which is the max absolute change between samples divided by the temporal separation for each sample, preceding or succeeding the sample. To detect out liters, the median absolute deviation is calculated from the speed dilation variable, multiplied by a constant, and added to the median dilation speed variable–values above this threshold are then removed.

max_pup<-pup_missing  %>% 
  dplyr::group_by(subject, trial) %>% 
  dplyr::mutate(speed=speed_pupil(pup_interp,time)) %>%
  dplyr::mutate(MAD=calc_mad(speed))%>%
  dplyr::filter(speed < MAD)

Trial Clipping

In most psychological experiments, each trial includes several events. We can use this information to start our trial onset at zero. To do this we can use the onset_pupil function. This function requires three arguments to be specified: time column, sample message column, and the event of interest (“target”) in our example). This will allow us to start the trial at the onset of the target.


baseline_pupil_onset<-baseline_pupil %>% 
  dplyr::group_by(subject, trial) %>%  
  dplyr::mutate(time_zero=onset_pupil(time, message, event=c("target"))) %>%
  ungroup() %>% 
  relocate(time_zero,time, baseline, .after="trial")

baseline_pupil_onset
#> # A tibble: 585,898 × 20
#>    subject trial time_zero  time baseline extendpupil pupil     x     y blink
#>    <chr>   <int>     <int> <int>    <dbl>       <int> <int> <dbl> <dbl> <int>
#>  1 11c.edf     1      -212     0    3584.        3635  3635  972.  563.     0
#>  2 11c.edf     1      -208     4    3584.        3634  3634  973.  565.     0
#>  3 11c.edf     1      -204     8    3584.        3626  3626  972.  567.     0
#>  4 11c.edf     1      -200    12    3584.        3622  3622  971   566.     0
#>  5 11c.edf     1      -196    16    3584.        3621  3621  972.  566.     0
#>  6 11c.edf     1      -192    20    3584.        3621  3621  972.  566.     0
#>  7 11c.edf     1      -188    24    3584.        3618  3618  972.  565.     0
#>  8 11c.edf     1      -184    28    3584.        3618  3618  971.  564.     0
#>  9 11c.edf     1      -180    32    3584.        3617  3617  971.  564.     0
#> 10 11c.edf     1      -176    36    3584.        3611  3611  971.  565.     0
#> # ℹ 585,888 more rows
#> # ℹ 10 more variables: message <chr>, acc <int>, block <int>, rt <int>,
#> #   item <chr>, script <chr>, alt <chr>, interp <dbl>, pup_interp <dbl>,
#> #   baselinecorrectedp <dbl>

Decimation or Downsampling

Place data into timebins (users can specifiy a timebin to use). For this example, we will use 200 ms timebins. The argument aggvars requires a vector of coloumn names to aggregate on. If you are using pupil data, type should set to “pupil.”

timebins1<- downsample_gaze(baseline_pupil_onset, bin.length=200, timevar = "time_zero", aggvars = c("subject", "script", "timebins"), type="pupil")

timebins1
#> # A tibble: 319 × 4
#>    subject script  timebins aggbaseline
#>    <chr>   <chr>      <dbl>       <dbl>
#>  1 11c.edf cursive     -200      0.865 
#>  2 11c.edf cursive        0      0.0982
#>  3 11c.edf cursive      200      1.36  
#>  4 11c.edf cursive      400      8.13  
#>  5 11c.edf cursive      600      4.09  
#>  6 11c.edf cursive      800      1.13  
#>  7 11c.edf cursive     1000     12.7   
#>  8 11c.edf cursive     1200     34.7   
#>  9 11c.edf cursive     1400     59.4   
#> 10 11c.edf cursive     1600     80.4   
#> # ℹ 309 more rows