OSCR

Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load.

Code ↔ Paper

9 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 9 matches
  1. [1] § 2. Method › 2.4 Data preparation and statistical modelling › 2.4.1 Pupillometry pre-processing. ↔ Code/1_pupils_cleaning.Rmd, lines 184–260 · score 0.67 · 0.05–4 Hz, linearly interpolated, Hanning, butterworth, tagged, tonic
  2. [2] § 2. Method › 2.3 Design and procedure ↔ Code/pupils_entropy_subjective.Rmd, lines 311–348 · score 0.66 · NASA TLX, subjective workload, critical event, vehicle
  3. [3] § 2. Method › 2.4 Data preparation and statistical modelling › 2.4.1 Pupillometry pre-processing. ↔ Code/2_pupils_modelling.Rmd, lines 646–690 · score 0.64 · pseudo change, cubic, subtracted, evoked, fitted, dilation
  4. [4] § 2. Method › 2.1 Participants ↔ Code/2_pupils_modelling.Rmd, lines 145–174 · score 0.64 · gender split, annually, miles, UK, license, age
  5. [5] § 3. Results › 3.6 Post-hoc analysis: driver gaze during transitions of control ↔ Code/2_pupils_modelling.Rmd, lines 1324–1364 · score 0.60 · pitch angle, post hoc, gaze distribution, variables, modelled, windows
  6. [6] § 2. Method › 2.2 Apparatus and materials ↔ Code/pupils_entropy_subjective.Rmd, lines 311–348 · score 0.58 · NASA TLX, subjective workload, gaze, pupil
  7. [7] § 3. Results › 3.6 Post-hoc analysis: driver gaze during transitions of control ↔ Code/2_pupils_modelling.Rmd, lines 1413–1504 · score 0.53 · dashboard area, post transition, pre RTI, gaze
  8. [8] § 3. Results › 3.1 Effect of N-back and lead vehicle presence on pupil diameter during hands-off L2 driving ↔ Code/2_pupils_modelling.Rmd, lines 446–527 · score 0.51 · credible intervals, pupil diameter increased, lead vehicle, CI, predicted
  9. [9] § 3. Results › 3.6 Post-hoc analysis: driver gaze during transitions of control ↔ Code/2_pupils_modelling.Rmd, lines 1324–1364 · score 0.50 · pitch angle, pre RTI, Modelling, Post, window, hoc

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R Markdown · 1,504 lines · 65 KB · no license · 6 matches

  1. ---
  2. title: "Pupillometry modelling"
  3. author: "Courtney Goodridge"
  4. date: "25/11/2024"
  5. output: html_document
  6. ---
  7. ```{r setup, include=FALSE}
  8. knitr::opts_chunk$set(echo = TRUE)
  9. ```
  10. ## Preamble
  11. ## Packages
  12. ```{r}
  13. if(!require(here)) install.packages("here")
  14. library(here)
  15. if(!require(ggplot2)) install.packages("ggplot2")
  16. library(ggplot2)
  17. if(!require(dplyr)) install.packages("dplyr")
  18. library(dplyr)
  19. if(!require(tidyr)) install.packages("tidyr")
  20. library(tidyr)
  21. if(!require(viridis)) install.packages("viridis")
  22. library(viridis)
  23. if(!require(gridExtra)) install.packages("gridExtra")
  24. library(gridExtra)
  25. if(!require(readr)) install.packages("readr")
  26. library(readr)
  27. if(!require(plyr)) install.packages("plyr")
  28. library(plyr)
  29. if(!require(stringr)) install.packages("stringr")
  30. library(stringr)
  31. if(!require(readxl)) install.packages("readxl")
  32. library(readxl)
  33. if(!require(data.table)) install.packages("data.table")
  34. library(data.table)
  35. if(!require(lmerTest)) install.packages("lmerTest")
  36. library(lmerTest)
  37. if(!require(plotly)) install.packages("plotly")
  38. library(plotly)
  39. if(!require(zoo)) install.packages("zoo")
  40. library(zoo)
  41. if(!require(viridis)) install.packages("viridis")
  42. library(viridis)
  43. if(!require(brms)) install.packages("brms")
  44. library(brms)
  45. if(!require(purrr)) install.packages("purrr")
  46. library(purrr)
  47. if(!require(MASS)) install.packages("MASS")
  48. library(MASS)
  49. if(!require(rstan)) install.packages("rstan")
  50. library(rstan)
  51. if(!require(ggdist)) install.packages("ggdist")
  52. library(ggdist)
  53. if(!require(bayestestR)) install.packages("bayestestR")
  54. library(bayestestR)
  55. if(!require(posterior)) install.packages("posterior")
  56. library(posterior)
  57. if(!require(distributional)) install.packages("distributional")
  58. library(distributional)
  59. if(!require(cowplot)) install.packages("cowplot")
  60. library(cowplot)
  61. if(!require(modelr)) install.packages("modelr")
  62. library(modelr)
  63. if(!require(purrr)) install.packages("purrr")
  64. library(purrr)
  65. if(!require(forcats)) install.packages("forcats")
  66. library(forcats)
  67. if(!require(tidybayes)) install.packages("tidybayes")
  68. library(tidybayes)
  69. if(!require(bayesplot)) install.packages("bayesplot")
  70. library(bayesplot)
  71. if(!require(BayesFactor)) install.packages("BayesFactor")
  72. library(BayesFactor)
  73. if(!require(patchwork)) install.packages("patchwork")
  74. library(patchwork)
  75. if(!require(scales)) install.packages("scales")
  76. library(scales)
  77. if(!require(emmeans)) install.packages("emmeans")
  78. library(emmeans)
  79. if(!require(zoo)) install.packages("zoo")
  80. library(zoo)
  81. if(!require(changepoint)) install.packages("changepoint")
  82. library(changepoint)
  83. if(!require(PupillometryR)) install.packages("PupillometryR")
  84. library(PupillometryR)
  85. if(!require(zoo)) install.packages("zoo")
  86. library(zoo)
  87. if(!require(rstudioapi)) install.packages("rstudioapi")
  88. library(rstudioapi)
  89. ```
  90. ## Loading linearly interpolated data and participant information
  91. ```{r}
  92. # loading eye tracking data with gaze direction columns
  93. options(digits = 15)
  94. int_data_df <- fread(file = here::here("Data/updated data with gaze/pupil_timecourse_lin_int_with_gaze.csv"))
  95. # load participant information
  96. participant_info <- read.csv(here::here("Data/participant_info.csv")) %>%
  97. dplyr::select(1:5) %>%
  98. dplyr::rename("age" = "Age", "ppid" = "Participant.ID")
  99. ```
  100. ## Participant information and computing variables during automation
  101. ```{r}
  102. # calculate avg and sd of pupil diameter for each automation period
  103. trial_pupil_diameter <- int_data_df %>%
  104. dplyr::group_by(trialid_new) %>%
  105. dplyr::mutate(time = frame / 60) %>%
  106. dplyr::filter(critical_or_no == "automation", time <= 60) %>%
  107. dplyr::group_by(trialid_new, ppid, n_back, lead) %>%
  108. dplyr::summarise(mean_pupil_diameter = mean(mean_pupil_butter), sd_pupil_diameter = sd (mean_pupil_butter), sd_yaw = sd(yaw_angle_deg))
  109. # merging participant information
  110. trial_pupil_diameter <- merge(trial_pupil_diameter, participant_info, by = c("ppid"))
  111. trial_pupil_diameter$age.c <- scale(trial_pupil_diameter$age, center = T, scale = F)
  112. # age, driving experience, demographics
  113. trial_pupil_diameter %>%
  114. dplyr::group_by(ppid) %>%
  115. dplyr::slice(1) %>%
  116. dplyr::ungroup() %>%
  117. dplyr::summarise(min_age = min(age), max_age = max(age), mean_age = mean(age), sd_age = sd(age), min_miles = min(Approximate.Annual.Mileage..Miles.), max_miles = max(Approximate.Annual.Mileage..Miles.), mean_miles = mean(Approximate.Annual.Mileage..Miles.), sd_miles = sd(Approximate.Annual.Mileage..Miles.), min_license = min(No..of.years.holding.a.full.UK.driving.license), max_license = max(No..of.years.holding.a.full.UK.driving.license), mean_license = mean(No..of.years.holding.a.full.UK.driving.license), sd_license = sd(No..of.years.holding.a.full.UK.driving.license)) %>%
  118. View()
  119. # gender split
  120. trial_pupil_diameter %>%
  121. dplyr::group_by(ppid) %>%
  122. dplyr::slice(1) %>%
  123. dplyr::group_by(Gender) %>%
  124. dplyr::summarise(n = n())
  125. ```
  126. ## DATA SAVING AND LOADING
  127. Due to the size of the timecourse dataframe, it is not included in the Github repo. However, the trial averaged pupil diameters are. The following code chunk loads this dataframe from the Github repo folder
  128. ```{r}
  129. # loading eye tracking data with gaze direction columns
  130. options(digits = 15)
  131. trial_pupil_diameter <- fread(file = here::here("Data/updated data with gaze/trial_pupil_diameter.csv"))
  132. ## Data saving mean diameter
  133. fwrite(trial_pupil_diameter, file = here::here("Data/updated data with gaze/trial_pupil_diameter.csv"))
  134. ```
  135. ## Correlation between pupil diameter and sd of yaw
  136. In this code chunk, we look at the relationship between pupil diameter and horizontal gaze dispersion. Overall, no consistent relationship is present.
  137. ```{r}
  138. ggplot(trial_pupil_diameter, mapping = aes(x = sd_yaw, y = mean_pupil_diameter, col = n_back)) +
  139. geom_point() +
  140. facet_wrap(~ lead) +
  141. xlim(0, 25)
  142. mean_pupil_sd_yaw <- lmer(mean_pupil_diameter ~ sd_yaw * n_back * lead + (sd_yaw * n_back * lead | ppid), data = trial_pupil_diameter)
  143. summary(mean_pupil_sd_yaw)
  144. ggplot(trial_pupil_diameter, mapping = aes(x = sd_yaw, y = sd_pupil_diameter, col = n_back)) +
  145. geom_point() +
  146. facet_wrap(~ lead) +
  147. xlim(0, 25)
  148. sd_pupil_sd_yaw <- lmer(sd_pupil_diameter ~ sd_yaw * n_back * lead + (sd_yaw * n_back + lead | ppid), data = trial_pupil_diameter)
  149. summary(sd_pupil_sd_yaw)
  150. ```
  151. ## FIGURE 1
  152. # Distributions of raw mean pupil diameter by conditions
  153. ```{r}
  154. # labels
  155. lead_labs <- c("No lead vehicle", "Lead vehicle")
  156. names(lead_labs) <- c(FALSE, TRUE)
  157. fig1 <- ggplot(trial_pupil_diameter, mapping = aes(mean_pupil_diameter, fill = as.factor(n_back))) +
  158. geom_histogram(alpha = .5) +
  159. xlab("Mean pupil diameter (mm)") +
  160. ylab("Count") +
  161. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  162. scale_y_continuous(expand = c(0, 0), limits = c(0, 50)) +
  163. scale_x_continuous(limits = c(2.5, 5.5), breaks = seq(3, 5, 1), labels = label_number(accuracy = .50)) +
  164. facet_wrap(~ lead, labeller = labeller(lead = lead_labs)) +
  165. theme_bw() +
  166. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  167. ggsave(here::here("Plots/fig1.tiff"), plot = fig1, width = 10, height = 6, units = 'cm', dpi = 300, type = 'cairo')
  168. ```
  169. ## Model 1
  170. # mean pupil diameter as a function of n-back and lead vehicle during SAE L2 driving
  171. ```{r}
  172. # regularising priors (https://github.com/stan-dev/stan/wiki/prior-choice-recommendations)
  173. prior(normal(0, 10)) %>%
  174. parse_dist() %>%
  175. ggplot(aes(xdist = .dist_obj, y = prior)) +
  176. stat_halfeye(.width = c(.5, .99)) +
  177. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  178. scale_x_continuous(expression(italic(p)(beta[1]))) +
  179. theme_bw()
  180. # mean pupil diameter
  181. mod_1_mean <- brm(data = trial_pupil_diameter,
  182. family = gaussian(),
  183. mean_pupil_diameter ~ n_back * lead + (n_back * lead | ppid),
  184. prior = c(prior(normal(0, 10), class = "Intercept"),
  185. prior(normal(0, 10), class = "b"),
  186. prior(cauchy(0, 2), class = sd),
  187. prior(lkj(2), class = cor)),
  188. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_1_mean"))
  189. # reload mean pupil diameter model
  190. mod_1_mean <- readRDS(here::here("Models/mod_1_mean.rds"))
  191. # model summaries for mean pupil diameter
  192. print(summary(mod_1_mean), digits = 5)
  193. describe_posterior(mod_1_mean)
  194. # calculating the marginal effect of n-back
  195. mod_1_mean %>%
  196. emmeans(~ n_back,
  197. at = list(lead = FALSE),
  198. epred = TRUE, re_formula = NA) %>%
  199. contrast(method = "revpairwise") %>%
  200. gather_emmeans_draws() %>%
  201. mean_hdi() %>%
  202. View()
  203. # calculating the marginal effect of lead vehicle
  204. mod_1_mean %>%
  205. emmeans(~ lead,
  206. at = list(n_back = FALSE),
  207. epred = TRUE, re_formula = NA) %>%
  208. contrast(method = "revpairwise") %>%
  209. gather_emmeans_draws() %>%
  210. mean_hdi() %>%
  211. View()
  212. predicted_mean_pupils <- mod_1_mean %>%
  213. epred_draws(newdata = expand_grid(lead = c(FALSE, TRUE),
  214. n_back = c(FALSE, TRUE)),
  215. re_formula = NA)
  216. predicted_mean_pupils %>% mean_hdi() %>% View()
  217. ```
  218. ## Figure 2
  219. # Plotting heterogenity of effects
  220. ```{r}
  221. # labels
  222. lead_labs <- c("No lead vehicle", "Lead vehicle")
  223. names(lead_labs) <- c(FALSE, TRUE)
  224. fig2a <- ggplot(predicted_mean_pupils, aes(x = .epred, y = " ", fill = as.factor(n_back))) +
  225. stat_histinterval(alpha = .5) +
  226. ylab(NULL) +
  227. xlab("Predicted mean pupil diameter (mm)") +
  228. #xlim(3.5, 4.5) +
  229. scale_x_continuous(limits = c(3.5, 4.5), breaks = seq(3.60, 4.40, .40), labels = label_number(accuracy = .50)) +
  230. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  231. facet_wrap(~ lead, labeller = labeller(lead = lead_labs)) +
  232. ggtitle("A") +
  233. theme_bw() +
  234. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 12), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  235. # relative size of heterogeneity effect for N-back - more than .5 is note worthy (variance in the effect is half the size of the average effect)
  236. summary(mod_1_mean)[["random"]][["ppid"]][2,1] / summary(mod_1_mean)[["fixed"]][2,1]
  237. # CI interval for average effect
  238. bayes_credible_intervals <- as.data.frame(summary(mod_1_mean)[["fixed"]][3:4])
  239. # individual effects - random effects added to the average effect
  240. ppid_effects <- as.data.frame(ranef(mod_1_mean)) %>%
  241. dplyr::select(ppid.Estimate.n_backTRUE) %>%
  242. dplyr::mutate(ranef = ppid.Estimate.n_backTRUE + summary(mod_1_mean)[["fixed"]][2,1])
  243. ppid_effects$x <- "x"
  244. # calculate individual effects
  245. ran_effects <- summary(mod_1_mean)[["random"]][["ppid"]][2,1]
  246. # strip plot highlighting the individual effects of N-back on mean pupil diameter, alongside the average effct, the confidence intervals, and the heterogeneity intervals.
  247. fig2b <- ggplot() +
  248. geom_jitter(ppid_effects, mapping = aes(x = x, y = ranef), width = 0.01, height = 0, size = 3,
  249. shape = 21, colour = "black", alpha = .95, stroke = 1) +
  250. #scale_y_continuous(limits = c(-.35, .1), breaks = seq(-.3, .1, 0.1), labels = label_number(accuracy = 0.01)) +
  251. ylab("N-back effect on mean pupil diameter (mm)") +
  252. xlab("") +
  253. geom_hline(aes(yintercept = bayes_credible_intervals$`l-95% CI`[2], linetype ="95% CI"), size = 1.5, color = viridis(5)[3]) +
  254. geom_hline(aes(yintercept = bayes_credible_intervals$`u-95% CI`[2], linetype="95% CI"), size = 1.5, color = viridis(5)[3]) +
  255. geom_hline(aes(yintercept = summary(mod_1_mean)[["fixed"]][2,1], linetype = "beta_1"), size = 1.5, color = "black") +
  256. geom_hline(aes(yintercept = summary(mod_1_mean)[["fixed"]][2,1] + 1.96 * ran_effects, linetype = "95% HI"), size = 1.5, color = viridis(5)[2]) +
  257. geom_hline(aes(yintercept = summary(mod_1_mean)[["fixed"]][2,1] - 1.96 * ran_effects, linetype = "95% HI"), size = 1.5, color = viridis(5)[2]) +
  258. scale_linetype_manual(name = " ", values = c(2, 1, 1), labels = c(expression(CI[95]), expression(HI[95]), expression(beta[N])), guide = guide_legend(override.aes = list(color = c(viridis(5)[3], viridis(5)[2], "black")))) +
  259. scale_fill_manual(name = "Parameter", values = c(viridis(5)[2])) +
  260. ggtitle("B") +
  261. theme_bw() +
  262. theme(legend.position = "bottom", legend.key = element_blank(), legend.key.width = unit(.5, 'cm'), axis.text.y = element_blank(), axis.ticks.y = element_blank(), legend.key.size = unit(0.1, 'cm'), axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), title = element_text(size = 7), legend.title = element_text(size = 11), plot.title = element_text(size = 12, face = "bold"), legend.text = element_text(size = 10)) +
  263. coord_flip()
  264. fig2 <- fig2a | fig2b
  265. ggsave(here::here("Plots/fig2.tiff"), plot = fig2, width = 15, height = 7, units = 'cm', dpi = 300, type = 'cairo')
  266. # Proportion of people where mean pupil diameter increases
  267. mean <- summary(mod_1_mean)[["fixed"]][2,1] # Estimated n-back slope for the average person (fixed effect)
  268. sd <- summary(mod_1_mean)[["random"]][["ppid"]][2,1] # Estimated heterogeneity (in SD units) (random effect)
  269. ub <- 10
  270. lb <- 0
  271. # create an interval ranging from -4 to 4 and multiply by SD.
  272. # This gives values falling within +/- 4SD. Then add mean.
  273. x <- seq(-4,4,length=100)*sd + mean
  274. # put the above into a dataframe along with the density and lower and upper bounds
  275. # set ub (upper bound) to 0 so we can compute proportion of slope values under this number
  276. dendf <- data.frame(
  277. x = seq(-4,4,length=100)*sd + mean,
  278. hx = dnorm(x,mean,sd),
  279. ub = 10, lb = 0)
  280. # compute area under the curve with upper bound at 0 i.e. what proportion of people have reduced SGE when completing n-back
  281. area <- pnorm(ub, mean, sd) - pnorm(lb, mean, sd)
  282. signif(area, digits=2) * 100
  283. ```
  284. ## FIGURE 3
  285. # Distributions of raw sd pupil diameter by conditions
  286. ```{r}
  287. # labels
  288. lead_labs <- c("No lead vehicle", "Lead vehicle")
  289. names(lead_labs) <- c(FALSE, TRUE)
  290. fig3 <- ggplot(trial_pupil_diameter, mapping = aes(sd_pupil_diameter, fill = as.factor(n_back))) +
  291. geom_histogram(alpha = .5) +
  292. xlab("SD of pupil diameter (mm)") +
  293. ylab("Count") +
  294. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  295. scale_y_continuous(expand = c(0, 0), limits = c(0, 60)) +
  296. scale_x_continuous(limits = c(0, .6), breaks = seq(.2, .6, .2), labels = label_number(accuracy = .50)) +
  297. facet_wrap(~ lead, labeller = labeller(lead = lead_labs)) +
  298. theme_bw() +
  299. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  300. ggsave(here::here("Plots/fig3.tiff"), plot = fig3, width = 10, height = 6, units = 'cm', dpi = 300, type = 'cairo')
  301. ```
  302. ## Model 2
  303. # Standard deviation of pupil diameter
  304. ```{r}
  305. # sd pupil diameter
  306. mod_2_sd <- brm(data = trial_pupil_diameter,
  307. family = gaussian(),
  308. sd_pupil_diameter ~ n_back * lead + (n_back * lead | ppid),
  309. prior = c(prior(normal(0, 10), class = "Intercept"),
  310. prior(normal(0, 10), class = "b"),
  311. prior(cauchy(0, 2), class = sd),
  312. prior(lkj(2), class = cor)),
  313. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_2_sd"))
  314. # reload sd pupil diameter model
  315. mod_2_sd <- readRDS(here::here("Models/mod_2_sd.rds"))
  316. # model summaries for sd pupil diameter
  317. print(summary(mod_2_sd), digits = 5)
  318. describe_posterior(mod_2_sd)
  319. # calculating the marginal effect of takeover window for mean pupil diameter (i.e., the contrast)
  320. mod_2_sd %>%
  321. emmeans(~ n_back,
  322. at = list(lead = FALSE),
  323. epred = TRUE, re_formula = NA) %>%
  324. contrast(method = "revpairwise") %>%
  325. gather_emmeans_draws() %>%
  326. mean_hdi() %>%
  327. View()
  328. predicted_sd_pupils <- mod_2_sd %>%
  329. epred_draws(newdata = expand_grid(lead = c(FALSE, TRUE),
  330. n_back = c(FALSE, TRUE)),
  331. re_formula = NA)
  332. predicted_sd_pupils %>% mean_hdi() %>% View()
  333. ```
  334. ## FIGURE 4
  335. # heterogegnity of sd of pupil diameter effects
  336. ```{r}
  337. # labels
  338. lead_labs <- c("No lead vehicle", "Lead vehicle")
  339. names(lead_labs) <- c(FALSE, TRUE)
  340. fig4a <- ggplot(predicted_sd_pupils, aes(x = .epred, y = " ", fill = as.factor(n_back))) +
  341. stat_histinterval(alpha = .5) +
  342. ylab(NULL) +
  343. xlab("Predicted SD of pupil diameter (mm)") +
  344. #xlim(3.5, 4.5) +
  345. scale_x_continuous(limits = c(.15, .30), breaks = seq(.15, .25, .05)) +
  346. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  347. facet_wrap(~ lead, labeller = labeller(lead = lead_labs)) +
  348. ggtitle("A") +
  349. theme_bw() +
  350. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 12), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  351. # relative size of heterogeneity effect for N-back - more than .5 is note worthy (variance in the effect is half the size of the average effect)
  352. summary(mod_2_sd)[["random"]][["ppid"]][2,1] / summary(mod_2_sd)[["fixed"]][2,1]
  353. # CI interval for average effect
  354. bayes_credible_intervals <- as.data.frame(summary(mod_2_sd)[["fixed"]][3:4])
  355. # individual effects - random effects added to the average effect
  356. ppid_effects <- as.data.frame(ranef(mod_2_sd)) %>%
  357. dplyr::select(ppid.Estimate.n_backTRUE) %>%
  358. dplyr::mutate(ranef = ppid.Estimate.n_backTRUE + summary(mod_2_sd)[["fixed"]][2,1])
  359. ppid_effects$x <- "x"
  360. # calculate individual effects
  361. ran_effects <- summary(mod_2_sd)[["random"]][["ppid"]][2,1]
  362. # strip plot highlighting the individual effects of N-back on mean pupil diameter, alongside the average effct, the confidence intervals, and the heterogeneity intervals.
  363. fig4b <- ggplot() +
  364. geom_jitter(ppid_effects, mapping = aes(x = x, y = ranef), width = 0.01, height = 0, size = 3,
  365. shape = 21, colour = "black", alpha = .95, stroke = 1) +
  366. #scale_y_continuous(limits = c(-.35, .1), breaks = seq(-.3, .1, 0.1), labels = label_number(accuracy = 0.01)) +
  367. ylab("N-back effect on SD of pupil diameter (mm)") +
  368. xlab("") +
  369. geom_hline(aes(yintercept = bayes_credible_intervals$`l-95% CI`[2], linetype ="95% CI"), size = 1.5, color = viridis(5)[3]) +
  370. geom_hline(aes(yintercept = bayes_credible_intervals$`u-95% CI`[2], linetype="95% CI"), size = 1.5, color = viridis(5)[3]) +
  371. geom_hline(aes(yintercept = summary(mod_2_sd)[["fixed"]][2,1], linetype = "beta_1"), size = 1.5, color = "black") +
  372. geom_hline(aes(yintercept = summary(mod_2_sd)[["fixed"]][2,1] + 1.96 * ran_effects, linetype = "95% HI"), size = 1.5, color = viridis(5)[2]) +
  373. geom_hline(aes(yintercept = summary(mod_2_sd)[["fixed"]][2,1] - 1.96 * ran_effects, linetype = "95% HI"), size = 1.5, color = viridis(5)[2]) +
  374. scale_linetype_manual(name = " ", values = c(2, 1, 1), labels = c(expression(CI[95]), expression(HI[95]), expression(beta[N])), guide = guide_legend(override.aes = list(color = c(viridis(5)[3], viridis(5)[2], "black")))) +
  375. scale_fill_manual(name = "Parameter", values = c(viridis(5)[2])) +
  376. ggtitle("B") +
  377. theme_bw() +
  378. theme(legend.position = "bottom", legend.key = element_blank(), legend.key.width = unit(.5, 'cm'), axis.text.y = element_blank(), axis.ticks.y = element_blank(), legend.key.size = unit(0.1, 'cm'), axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), title = element_text(size = 7), legend.title = element_text(size = 11), plot.title = element_text(size = 12, face = "bold"), legend.text = element_text(size = 10)) +
  379. coord_flip()
  380. fig4 <- fig4a | fig4b
  381. ggsave(here::here("Plots/fig4.tiff"), plot = fig4, width = 15, height = 7, units = 'cm', dpi = 300, type = 'cairo')
  382. # Proportion of people where mean pupil diameter increases
  383. mean <- summary(mod_2_sd)[["fixed"]][2,1] # Estimated n-back slope for the average person (fixed effect)
  384. sd <- summary(mod_2_sd)[["random"]][["ppid"]][2,1] # Estimated heterogeneity (in SD units) (random effect)
  385. ub <- 10
  386. lb <- 0
  387. # create an interval ranging from -4 to 4 and multiply by SD.
  388. # This gives values falling within +/- 4SD. Then add mean.
  389. x <- seq(-4,4,length=100)*sd + mean
  390. # put the above into a dataframe along with the density and lower and upper bounds
  391. # set ub (upper bound) to 0 so we can compute proportion of slope values under this number
  392. dendf <- data.frame(
  393. x = seq(-4,4,length=100)*sd + mean,
  394. hx = dnorm(x,mean,sd),
  395. ub = 10, lb = 0)
  396. # compute area under the curve with upper bound at 0 i.e. what proportion of people have reduced SGE when completing n-back
  397. area <- pnorm(ub, mean, sd) - pnorm(lb, mean, sd)
  398. signif(area, digits=2) * 100
  399. ```
  400. ## Calulcation of reaction times and computation of pupil timecourse
  401. ```{r}
  402. # takeover times for non-critical takeovers
  403. takeover_times_non_critical <- int_data_df %>%
  404. dplyr::filter(critical == FALSE) %>%
  405. dplyr::filter(critical_or_no == "non_critical_takeover") %>%
  406. dplyr::group_by(trialid_new) %>%
  407. dplyr::mutate(frame = row_number(), time = frame / 60) %>%
  408. dplyr::filter(vehicle_state == "manual") %>%
  409. dplyr::slice(1) %>%
  410. dplyr::mutate(r_time = time) %>%
  411. dplyr::select(trialid_new, r_time) %>%
  412. dplyr::ungroup() %>%
  413. dplyr::filter(r_time >= .3) %>% # filtering out taketimes less than 300 ms
  414. dplyr::mutate(z_score = scale(r_time)) %>% # calculate z scores
  415. dplyr::filter(z_score < 3) # filter out z scores more than 3
  416. # takeover times for critical takeovers
  417. takeover_times_critical <- int_data_df %>%
  418. dplyr::filter(critical == TRUE) %>%
  419. dplyr::filter(critical_or_no == "critical_takeover") %>%
  420. dplyr::group_by(trialid_new) %>%
  421. dplyr::mutate(frame = row_number(), time = frame / 60) %>%
  422. dplyr::filter(vehicle_state == "manual") %>%
  423. dplyr::slice(1) %>%
  424. dplyr::mutate(r_time = time) %>%
  425. dplyr::select(trialid_new, r_time) %>%
  426. dplyr::ungroup() %>%
  427. dplyr::filter(r_time >= .3) %>% # filtering out taketimes less than 300 ms
  428. dplyr::mutate(z_score = scale(r_time)) %>% # calculate z scores
  429. dplyr::filter(z_score < 3) # filter out z scores more than 3
  430. # combining critical and non-critical takeover times
  431. takeover_times <- rbind(takeover_times_critical, takeover_times_non_critical)
  432. # detecting 5th and 95th percentiles for plotting
  433. lower_bound <- quantile(takeover_times$r_time, 0.05)
  434. upper_bound <- quantile(takeover_times$r_time, 0.95)
  435. # merging takeover times with timecourse data
  436. int_data_df <- merge(int_data_df, takeover_times, by = c("trialid_new"))
  437. #### computing timecourse #####
  438. # timecourse for takeover and manual drive
  439. takeover_manual <- int_data_df %>%
  440. dplyr::arrange(trialid_new, frame) %>%
  441. dplyr::filter(critical_or_no %in% c("non_critical_takeover", "critical_takeover")) %>%
  442. dplyr::group_by(trialid_new) %>%
  443. dplyr::mutate(frame_new = row_number(), time = frame_new / 60) %>%
  444. dplyr::filter(time <= 20)
  445. # timecourse for 10 s of automation before takeover
  446. automation_pre <- int_data_df %>%
  447. dplyr::arrange(trialid_new, frame) %>%
  448. dplyr::filter(critical_or_no == "automation") %>%
  449. dplyr::group_by(trialid_new) %>%
  450. dplyr::mutate(frame_new = row_number(), time = frame_new / 60) %>%
  451. dplyr::mutate(end_time = time[n()]) %>%
  452. dplyr::filter(time >= (end_time - 10))
  453. # combined timecourse 12 s = TOR, 24 s = manual drive
  454. takeover_manual_automation_pre <- rbind(takeover_manual, automation_pre) %>%
  455. dplyr::arrange(trialid_new, frame) %>%
  456. dplyr::group_by(trialid_new) %>%
  457. dplyr::mutate(frame_timecourse = row_number()) %>%
  458. dplyr::mutate(timecourse = frame_timecourse / 60)
  459. ```
  460. ## FIGURE 5
  461. # TEPR timecourse for each condition
  462. ```{r}
  463. # standard error function
  464. stderror <- function(x) sd(x)/sqrt(length(x))
  465. pupil_timecourse_plot_df <- takeover_manual_automation_pre %>%
  466. dplyr::filter(r_time > .3) %>%
  467. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  468. dplyr::filter(timecourse >= 10 - 1, timecourse <= 10 + 2.5) %>%
  469. dplyr::mutate(timecourse_onset = timecourse - 10) %>%
  470. dplyr::group_by(timecourse_onset, n_back, ttc_criticality) %>%
  471. dplyr::summarise(pupil = mean(mean_pupil_butter), pupil_sem = stderror(mean_pupil_butter))
  472. ttc_labs <- c("Non-critical", "TTC = 3 s", "TTC = 5 s")
  473. names(ttc_labs) <- c(0, 3, 5)
  474. fig5 <- ggplot(pupil_timecourse_plot_df, mapping = aes(timecourse_onset, pupil, fill = as.factor(n_back), colour = as.factor(n_back))) +
  475. geom_rect(aes(xmin = -.5, xmax = 0, ymin = -Inf, ymax = Inf), alpha = .1, fill = "grey90", colour = NA) +
  476. geom_rect(aes(xmin = .3, xmax = 1.8, ymin = -Inf, ymax = Inf), alpha = .1, fill = "grey90", colour = NA) +
  477. geom_line(size = 1) +
  478. geom_ribbon(aes(
  479. ymin = pupil - pupil_sem,
  480. ymax = pupil + pupil_sem,
  481. group = n_back),
  482. alpha = 0.25,
  483. colour = NA) +
  484. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  485. scale_colour_discrete(name = " ", labels = c("No N-back", "N-back")) +
  486. ylim(3.5, 4.5) +
  487. geom_vline(aes(xintercept = 0), linetype = "solid") +
  488. geom_vline(aes(xintercept = -.5), linetype = "dashed") +
  489. geom_vline(aes(xintercept = .3), linetype = "dashed") +
  490. geom_vline(aes(xintercept = 1.8), linetype = "dashed") +
  491. ylab("Pupil diameter (mm)") +
  492. xlab("Time (s)") +
  493. scale_x_continuous(limits = c(-1, 2), breaks = seq(-1, 1, 1), labels = label_number(accuracy = .50)) +
  494. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  495. theme_bw() +
  496. theme(legend.position = "bottom", axis.title.x = element_text(size = 11), axis.text.x = element_text(size = 11), axis.title.y = element_text(size = 11), axis.text.y = element_text(size = 11), title = element_text(size = 18), legend.title = element_text(size = 12), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  497. ggsave(here::here("Plots/fig5.tiff"), plot = fig5, width = 15, height = 8, units = 'cm', dpi = 300, type = 'cairo')
  498. ```
  499. ## Calculating correction for regression to the mean
  500. ```{r}
  501. # here we calculate baseline and change of a random point without any task relevant information. This highlights a "pseudo" change
  502. rtm_data <- takeover_manual_automation_pre %>%
  503. dplyr::filter(critical_or_no == "automation") %>%
  504. dplyr::group_by(trialid_new) %>%
  505. dplyr::summarise(
  506. baseline = mean(mean_pupil_butter[timecourse > 5 & timecourse <= 5.5], na.rm = TRUE),
  507. pseudo_change = mean(mean_pupil_butter[timecourse > 6.3 & timecourse <= 7.8], na.rm = TRUE) -
  508. baseline,
  509. .groups = "drop"
  510. )
  511. # we fit a cubic model to predict the pseudo change as a function of the baseline period.
  512. rtm_model <- lm(
  513. pseudo_change ~ poly(baseline, 3, raw = TRUE),
  514. data = rtm_data
  515. )
  516. summary(rtm_model)
  517. # we caluclate the baseline and real task-evoked pupillary dilation
  518. task_data <- takeover_manual_automation_pre %>%
  519. dplyr::filter(r_time > .3) %>%
  520. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  521. dplyr::mutate(ttc_criticality = as.factor(ttc_criticality)) %>%
  522. dplyr::group_by(trialid_new, n_back, ttc_criticality, ppid, lead, critical, r_time) %>%
  523. summarise(
  524. baseline = mean(mean_pupil_butter[timecourse >= 9.5 & timecourse <= 10], na.rm = TRUE),
  525. observed_ped =
  526. mean(mean_pupil_butter[timecourse > 10.3 & timecourse <= 11.8], na.rm = TRUE) -
  527. baseline,
  528. .groups = "drop"
  529. )
  530. # we predict the likely task-evoked dilation if baseline predicted it. Then subtracted this pseudo dilation from the real task-evoked dilation, which leaves us the corrected phasic response.
  531. task_data <- task_data %>%
  532. mutate(
  533. predicted_rtm = predict(rtm_model, newdata = task_data),
  534. ped_corrected = observed_ped - predicted_rtm
  535. )
  536. ```
  537. ## FIGURE 6
  538. # Disitribution of corrected and uncorrected TEPR
  539. ```{r}
  540. # labels
  541. ttc_labs <- c("Non-critical", "TTC = 3 s", "TTC = 5 s")
  542. names(ttc_labs) <- c(0, 3, 5)
  543. # uncorrected
  544. fig6a <- ggplot(task_data, mapping = aes(observed_ped, fill = as.factor(n_back))) +
  545. geom_histogram(alpha = .5) +
  546. xlab("TEPRs (mm)") +
  547. ylab("Count") +
  548. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  549. scale_y_continuous(expand = c(0, 0), limits = c(0, 60)) +
  550. ggtitle("A: Uncorrected TEPRs") +
  551. #scale_x_continuous(limits = c(0, .6), breaks = seq(.2, .6, .2), labels = label_number(accuracy = .50)) +
  552. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  553. theme_bw() +
  554. theme(legend.position = c(.90, .73), axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  555. # corrected
  556. fig6b <- ggplot(task_data, mapping = aes(ped_corrected, fill = as.factor(n_back))) +
  557. geom_histogram(alpha = .5) +
  558. xlab("TEPRs (mm)") +
  559. ylab("Count") +
  560. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  561. scale_y_continuous(expand = c(0, 0), limits = c(0, 60)) +
  562. ggtitle("B: Corrected TEPRs") +
  563. #scale_x_continuous(limits = c(0, .6), breaks = seq(.2, .6, .2), labels = label_number(accuracy = .50)) +
  564. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  565. theme_bw() +
  566. theme(legend.position = "none", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  567. fig6 <- fig6a / fig6b
  568. ggsave(here::here("Plots/fig6.tiff"), plot = fig6, width = 15, height = 12, units = 'cm', dpi = 300, type = 'cairo')
  569. ```
  570. ## MODEL 3
  571. # Correlation between baseline pupil diameter and TEPR - UNCORRECTED
  572. ```{r}
  573. # regularising priors (https://github.com/stan-dev/stan/wiki/prior-choice-recommendations)
  574. prior(normal(0, .5)) %>%
  575. parse_dist() %>%
  576. ggplot(aes(xdist = .dist_obj, y = prior)) +
  577. stat_halfeye(.width = c(.5, .99)) +
  578. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  579. scale_x_continuous(expression(italic(p)(beta[1]))) +
  580. theme_bw()
  581. # mean pupil diameter
  582. mod_3_phasic_uncorrected <- brm(data = task_data,
  583. family = gaussian(),
  584. observed_ped ~ baseline * n_back * ttc_criticality + (baseline | ppid),
  585. prior = c(prior(normal(0, .5), class = "Intercept"),
  586. prior(normal(0, .5), class = "b"),
  587. prior(cauchy(0, 2), class = sd),
  588. prior(lkj(2), class = cor)),
  589. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_3_phasic_uncorrected"))
  590. # model summaries for sd pupil diameter
  591. print(summary(mod_3_phasic_uncorrected), digits = 5)
  592. describe_posterior(mod_3_phasic_uncorrected)
  593. # posterior checks
  594. pp_check(mod_3_phasic_uncorrected , type = "dens_overlay_grouped", group = "ttc_criticality", ndraws = 100)
  595. pp_check(mod_3_phasic_uncorrected , type = "dens_overlay_grouped", group = "n_back", ndraws = 100)
  596. # reload uncorrected phasic model
  597. mod_3_phasic_uncorrected <- readRDS(here::here("Models/mod_3_phasic_uncorrected.rds"))
  598. # average change in TEPR for a change in baseline pupil diameter
  599. mod_3_phasic_uncorrected %>%
  600. emtrends(
  601. ~ 1,
  602. var = "baseline",
  603. epred = TRUE,
  604. re_formula = NA
  605. ) %>%
  606. gather_emmeans_draws() %>%
  607. mean_hdi() %>% View()
  608. # average change in TEPR for a change in baseline pupil diameter, within each level of n-back
  609. mod_3_phasic_uncorrected %>%
  610. emtrends(
  611. ~ n_back,
  612. var = "baseline",
  613. epred = TRUE,
  614. re_formula = NA
  615. ) %>%
  616. gather_emmeans_draws() %>%
  617. mean_hdi() %>% View()
  618. # create a new dataset based on the original dataset
  619. newdata <- task_data %>%
  620. distinct(ttc_criticality, n_back) %>%
  621. crossing(
  622. baseline = seq(
  623. min(task_data$baseline, na.rm = TRUE),
  624. max(task_data$baseline, na.rm = TRUE),
  625. length.out = 100
  626. )
  627. )
  628. # generate population level predictions from the model
  629. preds <- add_epred_draws(
  630. mod_3_phasic_uncorrected,
  631. newdata = newdata,
  632. re_formula = NA # re_formula = NA = fixed effects only
  633. )
  634. # creates 95% CIs for estimate
  635. pred_summary <- preds %>%
  636. mean_qi(.epred)
  637. ```
  638. ## MODEL 4
  639. # Correlation between baseline pupil diameter and TEPR - CORRECTED
  640. ```{r}
  641. # mean pupil diameter
  642. mod_4_phasic_corrected <- brm(data = task_data,
  643. family = gaussian(),
  644. ped_corrected ~ baseline * n_back * ttc_criticality + (baseline | ppid),
  645. prior = c(prior(normal(0, .5), class = "Intercept"),
  646. prior(normal(0, .5), class = "b"),
  647. prior(cauchy(0, 2), class = sd),
  648. prior(lkj(2), class = cor)),
  649. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_4_phasic_corrected"))
  650. # reload mean pupil diameter model
  651. mod_4_phasic_corrected <- readRDS(here::here("Models/mod_4_phasic_corrected.rds"))
  652. # model summaries for corrected phasic model
  653. print(summary(mod_4_phasic_corrected), digits = 5)
  654. describe_posterior(mod_4_phasic_corrected)
  655. # average change in TEPR for a change in baseline pupil diameter
  656. mod_4_phasic_corrected %>%
  657. emtrends(
  658. ~ 1,
  659. var = "baseline",
  660. epred = TRUE,
  661. re_formula = NA
  662. ) %>%
  663. gather_emmeans_draws() %>%
  664. mean_hdi() %>% View()
  665. # average change in TEPR for a change in baseline pupil diameter, within each level of n-back
  666. mod_4_phasic_corrected %>%
  667. emtrends(
  668. ~ n_back,
  669. var = "baseline",
  670. epred = TRUE,
  671. re_formula = NA
  672. ) %>%
  673. gather_emmeans_draws() %>%
  674. mean_hdi() %>% View()
  675. # generate population level predictions from the model
  676. preds_correct <- add_epred_draws(
  677. mod_4_phasic_corrected,
  678. newdata = newdata,
  679. re_formula = NA # re_formula = NA = fixed effects only
  680. )
  681. # creates 95% CIs for estimate
  682. pred_summary_correct <- preds_correct %>%
  683. mean_qi(.epred)
  684. ```
  685. ## FIGURE 7
  686. # Plotting corrected and uncorrected TEPR model
  687. ```{r}
  688. ttc_labs <- c("Non-critical", "TTC = 3 s", "TTC = 5 s")
  689. names(ttc_labs) <- c(0, 3, 5)
  690. fig7a <- ggplot() +
  691. geom_point(task_data, mapping = aes(x = baseline, y = observed_ped, color = as.factor(n_back)), alpha = 0.6) +
  692. geom_ribbon(data = pred_summary, mapping = aes(x = baseline, ymin = .lower, ymax = .upper, fill = as.factor(n_back)), alpha = 0.2) +
  693. geom_line(data = pred_summary, mapping = aes(x = baseline, y = .epred,color = as.factor(n_back)), linewidth = 1) +
  694. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  695. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  696. scale_colour_discrete(name = " ", labels = c("No N-back", "N-back")) +
  697. xlab("Baseline pupil diameter (mm)") +
  698. ylab("Uncorrected TEPR (mm)") +
  699. theme_bw() +
  700. ggtitle("A: Uncorrected TEPRs") +
  701. xlim(2.5, 6) +
  702. ylim(-1, 1) +
  703. theme(legend.position = c(.56, .84), axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  704. fig7b <- ggplot() +
  705. geom_point(task_data, mapping = aes(x = baseline, y = ped_corrected, color = as.factor(n_back)), alpha = 0.6) +
  706. geom_ribbon(data = pred_summary_correct, mapping = aes(x = baseline, ymin = .lower, ymax = .upper, fill = as.factor(n_back)), alpha = 0.2) +
  707. geom_line(data = pred_summary_correct, mapping = aes(x = baseline, y = .epred,color = as.factor(n_back)), linewidth = 1) +
  708. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  709. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  710. scale_colour_discrete(name = " ", labels = c("No N-back", "N-back")) +
  711. xlab("Baseline pupil diameter (mm)") +
  712. ylab("Corrected TEPR (mm)") +
  713. theme_bw() +
  714. ggtitle("B: Corrected TEPRs") +
  715. xlim(2.5, 6) +
  716. ylim(-1, 1) +
  717. theme(legend.position = "none", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  718. fig7 <- fig7a / fig7b
  719. fig7
  720. ggsave(here::here("Plots/fig7.tiff"), plot = fig7, width = 15, height = 16, units = 'cm', dpi = 300, type = 'cairo')
  721. ```
  722. ## FIGURE 8
  723. # Reaction time distribution across each condition
  724. ```{r}
  725. fig8 <- ggplot(task_data, mapping = aes(r_time, fill = as.factor(n_back))) +
  726. geom_histogram(alpha = .5) +
  727. xlab("Reaction time (s)") +
  728. ylab("Count") +
  729. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  730. scale_y_continuous(expand = c(0, 0), limits = c(0, 60)) +
  731. #scale_x_continuous(limits = c(0, .6), breaks = seq(.2, .6, .2), labels = label_number(accuracy = .50)) +
  732. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  733. theme_bw() +
  734. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  735. ggsave(here::here("Plots/fig8.tiff"), plot = fig8, width = 15, height = 6, units = 'cm', dpi = 300, type = 'cairo')
  736. ```
  737. ## MODEL 5
  738. # Relationship betweem reaction time and pupillary dilations
  739. Our previous research previously indicated that there were no interactions between n_back and ttc criticality (Goodridge et al, 2026) we opted to remove 2way and 3way interactions between these variables. The removal of these higher-order interactions provided improved interpretability of model parameters as well as more targeted assessment of the effects of interest.
  740. ```{r}
  741. # regularising priors (https://github.com/stan-dev/stan/wiki/prior-choice-recommendations)
  742. prior(normal(0, .5)) %>%
  743. parse_dist() %>%
  744. ggplot(aes(xdist = .dist_obj, y = prior)) +
  745. stat_halfeye(.width = c(.5, .99)) +
  746. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  747. scale_x_continuous(expression(italic(p)(beta[1]))) +
  748. theme_bw()
  749. # mean pupil diameter
  750. mod_5_RT <- brm(data = task_data,
  751. family = lognormal(link = "identity"),
  752. r_time ~ ped_corrected +
  753. ttc_criticality +
  754. ped_corrected:n_back +
  755. ped_corrected:ttc_criticality +
  756. ped_corrected:n_back:ttc_criticality + (ped_corrected | ppid),
  757. prior = c(prior(normal(0, .5), class = "Intercept"),
  758. prior(normal(0, .5), class = "b"),
  759. prior(cauchy(0, 2), class = sd),
  760. prior(lkj(2), class = cor)),
  761. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_5_RT"))
  762. # reload mean pupil diameter model
  763. mod_5_RT <- readRDS(here::here("Models/mod_5_RT.rds"))
  764. # posterior checks
  765. pp_check(mod_5_RT , type = "dens_overlay_grouped", group = "ttc_criticality", ndraws = 100)
  766. # model summaries for reaction time
  767. print(summary(mod_5_RT ), digits = 5)
  768. describe_posterior(mod_5_RT )
  769. # create a new dataset based on the original dataset
  770. newdata_ped_corrected <- task_data %>%
  771. distinct(ttc_criticality, n_back) %>%
  772. crossing(
  773. ped_corrected = seq(
  774. min(task_data$ped_corrected, na.rm = TRUE),
  775. max(task_data$ped_corrected, na.rm = TRUE),
  776. length.out = 100
  777. )
  778. )
  779. # generate population level predictions from the model
  780. preds_r_time <- add_epred_draws(
  781. mod_5_RT,
  782. newdata = newdata_ped_corrected,
  783. re_formula = NA # re_formula = NA = fixed effects only
  784. )
  785. # creates 95% CIs for estimate
  786. pred_summary_r_time <- preds_r_time %>%
  787. mean_qi(.epred)
  788. #
  789. mean_ped <- mean(task_data$ped_corrected, na.rm = TRUE)
  790. #
  791. mod_5_RT %>%
  792. emmeans(
  793. ~ ttc_criticality,
  794. at = list(
  795. ped_corrected = mean_ped,
  796. n_back = FALSE
  797. ),
  798. epred = TRUE,
  799. re_formula = NA
  800. )
  801. mod_5_RT %>%
  802. emmeans(
  803. ~ ttc_criticality,
  804. at = list(
  805. ped_corrected = mean_ped,
  806. n_back = FALSE
  807. ),
  808. epred = TRUE,
  809. re_formula = NA
  810. ) %>%
  811. contrast(method = "revpairwise") %>%
  812. gather_emmeans_draws() %>%
  813. mean_hdi()
  814. ```
  815. ## FIGURE 9
  816. # correlation between reaction times and TEPRs
  817. ```{r}
  818. ttc_labs <- c("Non-critical", "TTC = 3 s", "TTC = 5 s")
  819. names(ttc_labs) <- c(0, 3, 5)
  820. fig9 <- ggplot() +
  821. geom_point(task_data, mapping = aes(x = ped_corrected, y = r_time, color = as.factor(n_back)), alpha = 0.6) +
  822. geom_ribbon(data = pred_summary_r_time, mapping = aes(x = ped_corrected, ymin = .lower, ymax = .upper, fill = as.factor(n_back)), alpha = 0.2) +
  823. geom_line(data = pred_summary_r_time, mapping = aes(x = ped_corrected, y = .epred,color = as.factor(n_back)), linewidth = 1) +
  824. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  825. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  826. scale_colour_discrete(name = " ", labels = c("No N-back", "N-back")) +
  827. xlab("Corrected TEPR (mm)") +
  828. ylab("Reaction time (s)") +
  829. theme_bw() +
  830. ylim(0, 4) +
  831. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 15, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  832. ggsave(here::here("Plots/fig9.tiff"), plot = fig9, width = 15, height = 8, units = 'cm', dpi = 300, type = 'cairo')
  833. ```
  834. ## MODEL 6
  835. # Correlation between M-pui and reaction time
  836. ```{r}
  837. mpui_df <- takeover_manual_automation_pre %>%
  838. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  839. dplyr::mutate(ttc_criticality = as.factor(ttc_criticality)) %>%
  840. dplyr::filter(timecourse >= 10 - 1, timecourse <= 10) %>% # baseline window
  841. dplyr::group_by(trialid_new, n_back, ttc_criticality, critical, lead, ppid, r_time) %>%
  842. dplyr::mutate(dpupil_50 = mean_pupil_50 - lag(mean_pupil_50),
  843. dpupil_100 = mean_pupil_100 - lag(mean_pupil_100),
  844. dpupil_200 = mean_pupil_200 - lag(mean_pupil_200),
  845. dpupil_400 = mean_pupil_400 - lag(mean_pupil_400)) %>%
  846. dplyr::summarise(mpui_50 = sum(abs(dpupil_50), na.rm = TRUE) / sum(!is.na(dpupil_50)), n_samples = sum(!is.na(dpupil_50)),
  847. mpui_100 = sum(abs(dpupil_100), na.rm = TRUE) / sum(!is.na(dpupil_100)), n_samples = sum(!is.na(dpupil_100)),
  848. mpui_200 = sum(abs(dpupil_200), na.rm = TRUE) / sum(!is.na(dpupil_200)), n_samples = sum(!is.na(dpupil_200)),
  849. mpui_400 = sum(abs(dpupil_400), na.rm = TRUE) / sum(!is.na(dpupil_400)), n_samples = sum(!is.na(dpupil_400)),
  850. .groups = "drop"
  851. )
  852. # regularising priors
  853. prior(normal(0, .5)) %>%
  854. parse_dist() %>%
  855. ggplot(aes(xdist = .dist_obj, y = prior)) +
  856. stat_halfeye(.width = c(.5, .99)) +
  857. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  858. scale_x_continuous(expression(italic(p)(beta[1]))) +
  859. theme_bw()
  860. # reaction time predicted by m-pui 50
  861. mod_6_mpui50 <- brm(data = mpui_df,
  862. family = lognormal(link = "identity"),
  863. r_time ~ mpui_50 + ttc_criticality + mpui_50:n_back + mpui_50:ttc_criticality + mpui_50:n_back:ttc_criticality + (mpui_50 | ppid),
  864. prior = c(prior(normal(0, .5), class = "Intercept"),
  865. prior(normal(0, .5), class = "b"),
  866. prior(cauchy(0, 2), class = sd)),
  867. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_6_mpui50"))
  868. # reload mpui50 model
  869. mod_6_mpui50 <- readRDS(here::here("Models/mod_6_mpui50.rds"))
  870. # model summaries for sd pupil diameter
  871. print(summary(mod_6_mpui50), digits = 5)
  872. describe_posterior(mod_6_mpui50)
  873. # reaction time predicted by m-pui 100
  874. mod_6_mpui100 <- brm(data = mpui_df,
  875. family = lognormal(link = "identity"),
  876. r_time ~ mpui_100 + ttc_criticality + mpui_100:n_back + mpui_100:ttc_criticality + mpui_100:n_back:ttc_criticality + (mpui_100 | ppid),
  877. prior = c(prior(normal(0, .5), class = "Intercept"),
  878. prior(normal(0, .5), class = "b"),
  879. prior(cauchy(0, 2), class = sd)),
  880. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_6_mpui100"))
  881. # reload mpui100 model
  882. mod_6_mpui100 <- readRDS(here::here("Models/mod_6_mpui100.rds"))
  883. # model summaries for sd pupil diameter
  884. print(summary(mod_6_mpui100), digits = 5)
  885. describe_posterior(mod_6_mpui100)
  886. # reaction time predicted by m-pui 200
  887. mod_6_mpui200 <- brm(data = mpui_df,
  888. family = lognormal(link = "identity"),
  889. r_time ~ mpui_200 + ttc_criticality + mpui_200:n_back + mpui_200:ttc_criticality + mpui_200:n_back:ttc_criticality + (mpui_200 | ppid),
  890. prior = c(prior(normal(0, .5), class = "Intercept"),
  891. prior(normal(0, .5), class = "b"),
  892. prior(cauchy(0, 2), class = sd)),
  893. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_6_mpui200"))
  894. # reload mpui200 model
  895. mod_6_mpui200 <- readRDS(here::here("Models/mod_6_mpui200.rds"))
  896. # model summaries for sd pupil diameter
  897. print(summary(mod_6_mpui200), digits = 5)
  898. describe_posterior(mod_6_mpui200)
  899. # reaction time predicted by m-pui 400
  900. mod_6_mpui400 <- brm(data = mpui_df,
  901. family = lognormal(link = "identity"),
  902. r_time ~ mpui_400 + ttc_criticality + mpui_400:n_back + mpui_400:ttc_criticality + mpui_400:n_back:ttc_criticality + (mpui_400 | ppid),
  903. prior = c(prior(normal(0, .5), class = "Intercept"),
  904. prior(normal(0, .5), class = "b"),
  905. prior(cauchy(0, 2), class = sd)),
  906. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_6_mpui400"))
  907. # reload mpui400 model
  908. mod_6_mpui400 <- readRDS(here::here("Models/mod_6_mpui400.rds"))
  909. # model summaries for sd pupil diameter
  910. print(summary(mod_6_mpui400), digits = 5)
  911. describe_posterior(mod_6_mpui400)
  912. # create a new dataset based on the original dataset
  913. newdata_mpui50 <- mpui_df %>%
  914. distinct(ttc_criticality, n_back) %>%
  915. crossing(
  916. mpui_50 = seq(
  917. min(mpui_df$mpui_50, na.rm = TRUE),
  918. max(mpui_df$mpui_50, na.rm = TRUE),
  919. length.out = 100
  920. )
  921. )
  922. # generate population level predictions from the model
  923. preds_mpui50 <- add_epred_draws(
  924. mod_6_mpui50,
  925. newdata = newdata_mpui50,
  926. re_formula = NA # re_formula = NA = fixed effects only
  927. )
  928. # creates 95% CIs for estimate
  929. pred_summary_mpui50 <- preds_mpui50 %>%
  930. mean_qi(.epred)
  931. ```
  932. ## Figure 10
  933. ```{r}
  934. ttc_labs <- c("Non-critical", "TTC = 3 s", "TTC = 5 s")
  935. names(ttc_labs) <- c(0, 3, 5)
  936. fig10 <- ggplot() +
  937. geom_point(mpui_df, mapping = aes(x = mpui_50, y = r_time, col = as.factor(n_back)), alpha = 0.6) +
  938. geom_ribbon(data = pred_summary_mpui50, mapping = aes(x = mpui_50, ymin = .lower, ymax = .upper,fill = as.factor(n_back)), alpha = 0.2) +
  939. geom_line(data = pred_summary_mpui50, mapping = aes(x = mpui_50, y = .epred, col = as.factor(n_back)), linewidth = 1) +
  940. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  941. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  942. scale_colour_discrete(name = " ", labels = c("No N-back", "N-back")) +
  943. xlab("M-PUI (mm/s)") +
  944. ylab("Reaction time (s)") +
  945. theme_bw() +
  946. ylim(0, 4) +
  947. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 15, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  948. ggsave(here::here("Plots/fig10.tiff"), plot = fig10, width = 15, height = 8, units = 'cm', dpi = 300, type = 'cairo')
  949. ```
  950. ## FIGURE 11
  951. # Plotting tonic increases in pupil diameter during transitions
  952. ```{r}
  953. # baseline corrected changed in pupil diameter
  954. baseline_pupil <- task_data %>%
  955. dplyr::select(trialid_new, baseline)
  956. takeover_manual_automation_pre <- merge(takeover_manual_automation_pre, baseline_pupil, by = c("trialid_new"))
  957. # calculate maximum baseline-corrected change in pupil during transition of control
  958. transitions_pupil_change <- takeover_manual_automation_pre %>%
  959. dplyr::arrange(trialid_new, timecourse) %>%
  960. dplyr::filter(timecourse > 10, timecourse < (20 + median(takeover_times$r_time))) %>%
  961. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  962. dplyr::group_by(n_back, ttc_criticality, ppid, trialid_new, baseline, critical) %>%
  963. dplyr::summarise(mean_pupil = mean(mean_pupil_butter))
  964. # loading eye tracking data with gaze direction columns
  965. options(digits = 15)
  966. transitions_pupil_change <- fread(file = here::here("Data/updated data with gaze/transitions_pupil_change.csv"))
  967. ## Data saving mean diameter
  968. fwrite(transitions_pupil_change, file = here::here("Data/updated data with gaze/transitions_pupil_change.csv"))
  969. fig11 <- ggplot(transitions_pupil_change, mapping = aes(mean_pupil, fill = as.factor(n_back))) +
  970. geom_histogram(alpha = .5) +
  971. xlab("Mean pupil diameter (mm)") +
  972. ylab("Count") +
  973. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  974. scale_y_continuous(expand = c(0, 0), limits = c(0, 40)) +
  975. scale_x_continuous(limits = c(2.5, 5.5), breaks = seq(3, 5, 1), labels = label_number(accuracy = .50)) +
  976. facet_wrap(~ ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  977. theme_bw() +
  978. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  979. ggsave(here::here("Plots/fig11.tiff"), plot = fig11, width = 15, height = 6, units = 'cm', dpi = 300, type = 'cairo')
  980. ```
  981. ## FIGURE 12 Plotting pupil diameter during takeover windows
  982. ```{r}
  983. # create change in pupil size measure
  984. takeover_manual_automation_pre <- takeover_manual_automation_pre %>%
  985. dplyr::group_by(trialid_new) %>%
  986. dplyr::mutate(baseline = mean(mean_pupil_butter[timecourse > 0 & timecourse <= 10], na.rm = TRUE)) %>%
  987. dplyr::mutate(pupil_change = mean_pupil_butter - baseline)
  988. # raw average timecourse
  989. fig12 <- ggplot(takeover_manual_automation_pre %>%
  990. dplyr::filter(r_time > .3) %>%
  991. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  992. dplyr::group_by(n_back, ttc_criticality, timecourse) %>%
  993. dplyr::summarise(m = mean(mean_pupil_butter), pupil_sem = stderror(mean_pupil_butter))) +
  994. geom_line(mapping = aes(x = timecourse, y = m, col = n_back)) +
  995. scale_color_discrete(name = " ", labels = c("No N-back", "N-back")) +
  996. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  997. geom_vline(aes(xintercept = 10), linetype = "dashed") +
  998. geom_vline(aes(xintercept = (20 + median(takeover_times$r_time))), linetype = "dashed") +
  999. geom_ribbon(mapping = aes(x = timecourse, y = m, ymin = m - pupil_sem, ymax = m + pupil_sem, fill = n_back), alpha = 0.25, colour = NA) +
  1000. facet_wrap(~ttc_criticality, labeller = labeller(ttc_criticality = ttc_labs)) +
  1001. xlim(0, 30) +
  1002. ylim(3.5, 4.5) +
  1003. ylab("Pupil diameter (mm)") +
  1004. xlab("Time (s)") +
  1005. theme_bw() +
  1006. theme(legend.position = "bottom", axis.title.x = element_text(size = 11), axis.text.x = element_text(size = 11), axis.title.y = element_text(size = 11), axis.text.y = element_text(size = 11), title = element_text(size = 18), legend.title = element_text(size = 12), legend.text = element_text(size = 12), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 10))
  1007. ggsave(here::here("Plots/fig12.tiff"), plot = fig12, width = 15, height = 7, units = 'cm', dpi = 300, type = 'cairo')
  1008. ```
  1009. ## MODEL 7
  1010. ```{R}
  1011. # regularising priors (https://github.com/stan-dev/stan/wiki/prior-choice-recommendations)
  1012. prior(normal(0, 10)) %>%
  1013. parse_dist() %>%
  1014. ggplot(aes(xdist = .dist_obj, y = prior)) +
  1015. stat_halfeye(.width = c(.5, .99)) +
  1016. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  1017. scale_x_continuous(expression(italic(p)(beta[1]))) +
  1018. theme_bw()
  1019. # mean pupil diameter change during transition phase
  1020. mod_7_mean_transition <- brm(data = transitions_pupil_change,
  1021. family = gaussian(),
  1022. mean_pupil ~ as.factor(n_back) * as.factor(ttc_criticality) +
  1023. (as.factor(n_back) + as.factor(ttc_criticality) | ppid),
  1024. prior = c(prior(normal(0, 10), class = "Intercept"),
  1025. prior(normal(0, 10), class = "b"),
  1026. prior(cauchy(0, 2), class = sd),
  1027. prior(lkj(2), class = cor)),
  1028. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_7_mean_transition"))
  1029. # model summaries
  1030. print(summary(mod_7_mean_transition), digits = 5)
  1031. describe_posterior(mod_7_mean_transition)
  1032. # reload model
  1033. mod_7_mean_transition <- readRDS(here::here("Models/mod_7_mean_transition.rds"))
  1034. # posterior checks
  1035. pp_check(mod_7_mean_transition, type = "dens_overlay_grouped", group = "ttc_criticality", ndraws = 100)
  1036. # calculating the marginal effect of takeover window for mean pupil diameter (i.e., the contrast)
  1037. mod_7_mean_transition %>%
  1038. emmeans(~ ttc_criticality,
  1039. at = list(n_back = TRUE),
  1040. epred = TRUE, re_formula = NA) %>%
  1041. contrast(method = "revpairwise") %>%
  1042. gather_emmeans_draws() %>%
  1043. mean_hdi() %>%
  1044. View()
  1045. predicted_pupil_change <- mod_7_mean_transition %>%
  1046. epred_draws(newdata = expand_grid(ttc_criticality = c(0, 3, 5),
  1047. n_back = c(FALSE, TRUE)),
  1048. re_formula = NA)
  1049. predicted_pupil_change %>% mean_hdi() %>% View()
  1050. ```
  1051. ################# POST HOC ANALYSIS #########################
  1052. ## FIGURE 13
  1053. # plot gaze distribution histogram
  1054. ```{r}
  1055. # creating time window variable
  1056. takeover_manual_automation_pre <- takeover_manual_automation_pre %>%
  1057. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  1058. dplyr::group_by(trialid_new) %>%
  1059. dplyr::mutate(time_windows = case_when(
  1060. timecourse <= 10 ~ "pre-RTI",
  1061. timecourse > 10 & timecourse <= (20 + median(takeover_times$r_time)) ~ "transition",
  1062. timecourse > 20 ~ "post-transition"))
  1063. sd_pitch_df <- takeover_manual_automation_pre %>%
  1064. dplyr::group_by(n_back, ttc_criticality, time_windows, ppid, trialid_new) %>%
  1065. dplyr::summarise(sd_pitch = sd(pitch_angle_deg))
  1066. # labels - n-back
  1067. t.labs <- c("Pre-RTI", "Transition", "Post-transition")
  1068. names(t.labs) <- c("pre-RTI", "transition","post-transition")
  1069. # reorder time windows for plotting
  1070. sd_pitch_df$time_windows <- factor(sd_pitch_df$time_windows,
  1071. levels = c("pre-RTI", "transition", "post-transition"))
  1072. fig13 <- ggplot(sd_pitch_df, mapping = aes(sd_pitch, fill = as.factor(n_back))) +
  1073. geom_histogram(alpha = .5) +
  1074. xlab("SD of pitch angle (°)") +
  1075. ylab("Count") +
  1076. scale_fill_discrete(name = " ", labels = c("No N-back", "N-back")) +
  1077. scale_y_continuous(expand = c(0, 0), limits = c(0, 65)) +
  1078. #scale_x_continuous(limits = c(0, .6), breaks = seq(.2, .6, .2), labels = label_number(accuracy = .50)) +
  1079. facet_grid(time_windows ~ ttc_criticality, labeller = labeller(time_windows = t.labs, ttc_criticality = ttc_labs)) +
  1080. theme_bw() +
  1081. theme(legend.position = "bottom", axis.title.x = element_text(size = 10), axis.text.x = element_text(size = 10), axis.title.y = element_text(size = 10), axis.text.y = element_text(size = 10), title = element_text(size = 18), legend.title = element_text(size = 11), legend.text = element_text(size = 10), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 12, face = "bold"), strip.text = element_text(face = "bold", size = 7))
  1082. ggsave(here::here("Plots/fig13.tiff"), plot = fig13, width = 15, height = 10, units = 'cm', dpi = 300, type = 'cairo')
  1083. ```
  1084. ## MODEL 8
  1085. ```{R}
  1086. # regularising priors (https://github.com/stan-dev/stan/wiki/prior-choice-recommendations)
  1087. prior(normal(0, 5)) %>%
  1088. parse_dist() %>%
  1089. ggplot(aes(xdist = .dist_obj, y = prior)) +
  1090. stat_halfeye(.width = c(.5, .99)) +
  1091. scale_y_discrete(NULL, breaks = NULL, expand = expansion(add = 0.1)) +
  1092. scale_x_continuous(expression(italic(p)(beta[1]))) +
  1093. theme_bw()
  1094. # mean pupil diameter change during transition phase
  1095. mod_8_sd_pitch <- brm(data = sd_pitch_df,
  1096. family = gaussian(),
  1097. sd_pitch ~ as.factor(n_back) * as.factor(ttc_criticality) * as.factor(time_windows) +
  1098. (as.factor(time_windows) | ppid),
  1099. prior = c(prior(normal(0, 5), class = "Intercept"),
  1100. prior(normal(0, 5), class = "b"),
  1101. prior(cauchy(0, 2), class = sd),
  1102. prior(lkj(2), class = cor)),
  1103. iter = 5000, warmup = 2000, chains = 2, cores = 2, seed = 13, file = here::here("Models/mod_8_sd_pitch"))
  1104. # model summaries
  1105. print(summary(mod_8_sd_pitch), digits = 5)
  1106. describe_posterior(mod_8_sd_pitch)
  1107. # calculating the marginal effect of takeover window for mean pupil diameter (i.e., the contrast)
  1108. mod_8_sd_pitch %>%
  1109. emmeans(~ n_back * time_windows,
  1110. at = list(ttc_criticality = 0),
  1111. epred = TRUE, re_formula = NA) %>%
  1112. contrast(method = "revpairwise") %>%
  1113. gather_emmeans_draws() %>%
  1114. mean_hdi() %>%
  1115. View()
  1116. predicted_sd_pitch <- mod_8_sd_pitch %>%
  1117. epred_draws(newdata = expand_grid(ttc_criticality = c(0, 3, 5),
  1118. time_windows = c("pre-RTI", "transition", "post-transition"),
  1119. n_back = c(FALSE, TRUE)),
  1120. re_formula = NA)
  1121. predicted_sd_pitch %>% mean_hdi() %>% View()
  1122. ```
  1123. ## FIGURE 14
  1124. # plotting gaze dispersion for time window
  1125. ```{r}
  1126. # labels - n-back
  1127. n.labs <- c("No N-back", "N-back")
  1128. names(n.labs) <- c("FALSE", "TRUE")
  1129. # pre-RTI
  1130. fig14a <- ggplot(takeover_manual_automation_pre %>%
  1131. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  1132. dplyr::filter(timecourse <= 10), mapping = aes(x = gaze_direction_x * 180 / pi, y = gaze_direction_y * 180 / pi)) +
  1133. geom_density_2d(aes(col = ..level..), alpha = .5, binwidth = .00003) +
  1134. facet_grid(ttc_criticality ~ n_back, labeller = labeller(n_back = n.labs, ttc_criticality = ttc_labs)) +
  1135. scale_colour_viridis_c(option = "C") +
  1136. annotate( # dashboard area
  1137. 'rect',
  1138. xmin = -10,
  1139. xmax = 25,
  1140. ymin = -30,
  1141. ymax = -15,
  1142. alpha = 0,
  1143. size = .5,
  1144. col = viridis(2)[2]
  1145. ) +
  1146. xlim(-50, 50) +
  1147. ylim(-40, 30) +
  1148. ggtitle("A: Pre-RTI") +
  1149. xlab("Yaw (°)") +
  1150. ylab("Pitch (°)") +
  1151. theme_bw() +
  1152. theme(legend.position = "none", axis.title.x = element_text(size = 11), axis.text.x = element_text(size = 11), axis.title.y = element_text(size = 11), axis.text.y = element_text(size = 11), title = element_text(size = 10), legend.title = element_text(size = 12), legend.text = element_text(size = 12), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 10, face = "bold"), strip.text = element_text(face = "bold", size = 6))
  1153. # transition of control
  1154. fig14b <- ggplot(takeover_manual_automation_pre %>%
  1155. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  1156. dplyr::filter(timecourse > 10, timecourse <= (20 + median(takeover_times$r_time))), mapping = aes(x = gaze_direction_x * 180 / pi, y = gaze_direction_y * 180 / pi)) +
  1157. geom_density_2d(aes(col = ..level..), alpha = .5, binwidth = .00003) +
  1158. facet_grid(ttc_criticality ~ n_back, labeller = labeller(n_back = n.labs, ttc_criticality = ttc_labs)) +
  1159. scale_colour_viridis_c(option = "C") +
  1160. annotate( # dashboard area
  1161. 'rect',
  1162. xmin = -10,
  1163. xmax = 25,
  1164. ymin = -30,
  1165. ymax = -15,
  1166. alpha = 0,
  1167. size = .5,
  1168. col = viridis(2)[2]
  1169. ) +
  1170. xlim(-50, 50) +
  1171. ylim(-40, 30) +
  1172. ggtitle("B: Transition of control") +
  1173. xlab("Yaw (°)") +
  1174. ylab("Pitch (°)") +
  1175. theme_bw() +
  1176. theme(legend.position = "none", axis.title.x = element_text(size = 11), axis.text.x = element_text(size = 11), axis.title.y = element_text(size = 11), axis.text.y = element_text(size = 11), title = element_text(size = 10), legend.title = element_text(size = 12), legend.text = element_text(size = 12), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 10, face = "bold"), strip.text = element_text(face = "bold", size = 6))
  1177. # post-transition of control
  1178. fig14c <- ggplot(takeover_manual_automation_pre %>%
  1179. dplyr::mutate(ttc_criticality = if_else(is.na(ttc_criticality), 0, ttc_criticality)) %>%
  1180. dplyr::filter(timecourse > (20 + median(takeover_times$r_time))), mapping = aes(x = gaze_direction_x * 180 / pi, y = gaze_direction_y * 180 / pi)) +
  1181. geom_density_2d(aes(col = ..level..), alpha = .5, binwidth = .00003) +
  1182. facet_grid(ttc_criticality ~ n_back, labeller = labeller(n_back = n.labs, ttc_criticality = ttc_labs)) +
  1183. scale_colour_viridis_c(option = "C") +
  1184. annotate( # dashboard area
  1185. 'rect',
  1186. xmin = -10,
  1187. xmax = 25,
  1188. ymin = -30,
  1189. ymax = -15,
  1190. alpha = 0,
  1191. size = .5,
  1192. col = viridis(2)[2]
  1193. ) +
  1194. xlim(-50, 50) +
  1195. ylim(-40, 30) +
  1196. ggtitle("C: Post-transition") +
  1197. xlab("Yaw (°)") +
  1198. ylab("Pitch (°)") +
  1199. theme_bw() +
  1200. theme(legend.position = "none", axis.title.x = element_text(size = 11), axis.text.x = element_text(size = 11), axis.title.y = element_text(size = 11), axis.text.y = element_text(size = 11), title = element_text(size = 10), legend.title = element_text(size = 12), legend.text = element_text(size = 12), legend.key = element_blank(), legend.key.width = unit(0.3, 'cm'), legend.key.size = unit(0.1, 'cm'), plot.title = element_text(size = 10, face = "bold"), strip.text = element_text(face = "bold", size = 6))
  1201. fig14 <- fig14a / fig14b + fig14c + plot_layout(axes = "collect")
  1202. # plot saving
  1203. ggsave(here::here("Plots/fig14.tiff"), plot = fig14, width = 10, height = 22, units = 'cm', dpi = 300, type = 'cairo')
  1204. ```

2_pupils_modelling.Rmd, no license · at the source

Overview

Authors: Courtney M. Goodridge1,2, Rafael C. Gonçalves2, Ali Arabian2, Anthony Horrobin2, Albert Solernou2, Yee Thung Lee2, Audrey Bruneau3, Jonny Kuo4, Michael G. Lenné4, Gaëtan Merlhiot5, Yee Mun Lee2, Natasha Merat2
  1. School of Psychology, University of Leeds, Leeds, United Kingdom
  2. Institute for Transport Studies, University of Leeds, Leeds, United Kingdom
  3. Toyota Motor Europe, Brussels, Belgium
  4. Seeing Machines, Canberra, Australia
  5. VEDECOM Institute, Versailles, France
Institutions: University of Leeds (United Kingdom); Toyota Motor Corporation (Belgium) (Belgium); VeDeCoM Institute (France)
Journal: PloS one, volume 21, issue 8, article e0355165
Dates: received 23 October 2025; accepted 19 July 2026; published online 11 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pone.0355165 · PMID 42579679 · PMCID PMC13460587 · OpenAlex W7202206070
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Preprocessing, Physiology & signal measures
MeSH: Automobile Driving*, Cognition*, Pupil*, Adult, Arousal, Female, Humans, Male, Psychomotor Performance, Reaction Time, Young Adult (* major topic)
Journal subjects: Biology and Life Sciences, Neuroscience, Cognitive Science, Cognitive Neuroscience, Reaction Time, Psychology, Behavior, Social Sciences, Cognitive Psychology, Attention, Vigilance, Anatomy, Ocular System, Ocular Anatomy, Pupil, Medicine and Health Sciences, Engineering and Technology, Navigation, Steering, Civil Engineering, Transportation Infrastructure, Roads, Transportation, Cognition
Topic: Sleep and Work-Related Fatigue (Experimental and Cognitive Psychology, Psychology), according to OpenAlex
Citations: not cited yet (Europe PMC); 113 references in the paper

Abstract

Arousal plays a vital role in facilitating the human ability to respond flexibly in a goal-directed manner, and pupillometry offers a non-invasive window into arousal-related neural processes given the close relationship between pupil size and Locus Coeruleus-Norepinephrine (LC-NE) activity. Whilst pupillometry has been used to detect cognitive load in manual and automated driving, the dynamic relationship between pre-stimulus (baseline) pupillary state and task-evoked pupillary responses (TEPRs) has not been investigated in driving contexts. This is important because variability in baseline pupil size – itself influenced by cognitive demands such as non-driving related task engagement – may contribute to variability in TEPRs independently of how drivers respond to critical events. This driving simulator experiment aimed to establish whether pupillometry during hands-off Level 2 (L2) driving was a reliable indicator of cognitive load, and to examine whether relationships between pupillary dynamics and behaviour established in controlled laboratory paradigms generalise to applied tasks. The size and reactivity of drivers’ (N = 38) pupils were measured during hands-off L2 driving with and without a cognitive load task, followed by critical and non-critical transitions of control. Analysis revealed that mean, not standard deviation, of pupil diameter was a reliable indicator of cognitive load. Furthermore, higher baseline pupil diameter was associated with smaller TEPRs, and this relationship persisted after correcting for regression to the mean artefacts – suggesting that pre-stimulus pupillary state genuinely constrains subsequent TEPRs. Limited evidence was found that TEPRs or pre-stimulus pupillary variability predicted driver reaction times, potentially reflecting the motoric variability inherent in naturalistic driving responses. Finally, more critical events were associated with larger pupil diameter, indicating that drivers were exerting greater effort to manage the transition. These results indicate that pupillometry is a useful measure of cognitive load and that laboratory-established pupillometric relationships extend, at least partially, to applied driving contexts, with implications for the development of Driver Monitoring Systems (DMS).

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 9 matches between paragraphs and lines of code.

OSF vmxj3

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Languages: R (3)
Size: 33 files, 3 scripts
Software Heritage: not checked
Found in: “Data Availability”
Holds: 3 notebooks
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: data.table (3 files), ggplot2 (3 files), tidyverse (3 files), lme4 (2 files), lmerTest (2 files), mgcv (2 files), BayesFactor (1 file), brms (1 file), cowplot (1 file), easystats (1 file), emmeans (1 file), patchwork (1 file), Plotly (1 file), Stan (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
3 files
At the source: osf.io/vmxj3

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 3 scripts, each with its path and the digest of its content;
  • 9 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability

Data, analysis code, and models can be found in the following link: https://osf.io/vmxj3.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 12 authors, 11 MeSH terms, 1 funder, 96 references.

Cite

This paper

Goodridge, C. M., Gonçalves, R. C., Arabian, A., Horrobin, A., Solernou, A., Lee, Y. T., Bruneau, A., Kuo, J., Lenné, M. G., Merlhiot, G., Lee, Y. M., & Merat, N. (2026). Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load. PloS one, 21(8), e0355165. https://doi.org/10.1371/journal.pone.0355165

BibTeX

@article{goodridge2026pupillary,
author = {Goodridge, Courtney M. and Gonçalves, Rafael C. and Arabian, Ali and Horrobin, Anthony and Solernou, Albert and Lee, Yee Thung and Bruneau, Audrey and Kuo, Jonny and Lenné, Michael G. and Merlhiot, Gaëtan and Lee, Yee Mun and Merat, Natasha},
title = {{Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load}},
journal = {PloS one},
year = {2026},
month = aug,
volume = {21},
number = {8},
pages = {e0355165},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/journal.pone.0355165},
url = {https://doi.org/10.1371/journal.pone.0355165},
pmid = {42579679},
pmcid = {PMC13460587}
}

RIS

TY - JOUR
AU - Goodridge, Courtney M.
AU - Gonçalves, Rafael C.
AU - Arabian, Ali
AU - Horrobin, Anthony
AU - Solernou, Albert
AU - Lee, Yee Thung
AU - Bruneau, Audrey
AU - Kuo, Jonny
AU - Lenné, Michael G.
AU - Merlhiot, Gaëtan
AU - Lee, Yee Mun
AU - Merat, Natasha
TI - Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/08/11
VL - 21
IS - 8
SP - e0355165
SN - 1932-6203
PB - PLOS
DO - 10.1371/journal.pone.0355165
UR - https://doi.org/10.1371/journal.pone.0355165
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pone.0355165",
"type": "article-journal",
"title": "Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load",
"container-title": "PloS one",
"author": [
{
"family": "Goodridge",
"given": "Courtney M."
},
{
"family": "Gonçalves",
"given": "Rafael C."
},
{
"family": "Arabian",
"given": "Ali"
},
{
"family": "Horrobin",
"given": "Anthony"
},
{
"family": "Solernou",
"given": "Albert"
},
{
"family": "Lee",
"given": "Yee Thung"
},
{
"family": "Bruneau",
"given": "Audrey"
},
{
"family": "Kuo",
"given": "Jonny"
},
{
"family": "Lenné",
"given": "Michael G."
},
{
"family": "Merlhiot",
"given": "Gaëtan"
},
{
"family": "Lee",
"given": "Yee Mun"
},
{
"family": "Merat",
"given": "Natasha"
}
],
"container-title-short": "PLoS One",
"volume": "21",
"issue": "8",
"page": "e0355165",
"DOI": "10.1371/journal.pone.0355165",
"PMID": "42579679",
"PMCID": "PMC13460587",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pone.0355165",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
11
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41562-026-02533-1 [code]
Fluctuations in arousal reflect latent state transitions that facilitate behavioural optimization.
Journal: Nature human behaviour
In common: cognitive, 13 references
[2] doi:10.1038/s41467-026-73865-9 [code]
Histamine shapes the neurocomputational dynamics of human learning.
Journal: Nature communications
In common: BayesFactor, brms, easystats, 7 other tools, cognitive
[3] doi:10.1016/j.celrep.2026.117505 [code]
Impaired spatial coding and neuronal hyperactivity in the medial entorhinal cortex of aged APP knock-in mice.
Journal: Cell reports
In common: Stan, brms, mgcv, 7 other tools
[4] doi:10.1016/j.isci.2026.116747 [code]
Age and loneliness relate to reduced trust learning and alterations in amygdala function.
Journal: iScience
In common: Stan, brms, easystats, 7 other tools, cognitive
[5] doi:10.1126/sciadv.adz6495
Pupil-linked arousal heterogeneously modulates cell-type-specific sensory processing.
Journal: Science advances
In common: 10 references
[6] doi:10.1523/eneuro.0076-26.2026 [code]
Exogenously Driven Neural Reactivation of Spatially Matching Visual Working-Memory Contents.
Journal: eNeuro
In common: BayesFactor, brms, emmeans, 5 other tools, cognitive, 1 reference
[7] doi:10.1038/s41467-026-74753-y [code]
A human-specific microRNA controls the timing of excitatory synaptogenesis.
Journal: Nature communications
In common: easystats, emmeans, lmerTest, 7 other tools
[8] doi:10.1038/s44271-026-00431-w [code]
Alpha power increases spontaneously during a neurofeedback session.
Journal: Communications psychology
In common: BayesFactor, Stan, brms, 5 other tools, cognitive
[9] doi:10.1162/imag.a.1258 [code]
Non-specific increase in alpha power during a neurofeedback session targeting its downregulation.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: BayesFactor, Stan, brms, 5 other tools
[10] doi:10.1038/s41593-026-02363-4 [code]
Cortical thickness changes precede high levels of amyloid by at least 7 years.
Journal: Nature neuroscience
In common: mgcv, easystats, lmerTest, 6 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.