Setting priors for Bayesian analyses of reaction times in psycholinguistic experiments

Companion code

Author
Affiliation

Scott James Perry

Universitat Pompeu Fabra

This is companion code to my talk at the EUTOPIA Methodological Workshop that is taking place as a satellite event to Giornate Italiane di Linguistica Sperimentale. My talk is meant to be more theoretical and conceptual in nature, but for those of you who are interested in how to conduct a prior predictive check in brms, you will find the basic code needed here along with brief explanations.

Setting up environment and loading data

library(brms) # Software for fitting Bayesian models
library(rstan)
library(ggplot2) # These two packages are for plotting
library(ggthemes)

# Loading in the data for all examples
load("data/MALD_subset.Rda")

# Setting global plotting theme
theme_set(
  theme_bw(base_size = 15) +
    theme(panel.grid.minor = element_blank(),
          panel.grid.major = element_line(color = "grey50",
                                          linetype = 2),
          legend.position = "none")
)

# Setting the seed for replicability
set.seed(101)

Setting priors

Creating some convenience functions

Here we create two simple functions to save ourselves some typing later. The first one, prior_plot(), plots a number of simulated datasets based on our prior model. The second one, prior_samples(), gets a vector of the actual predicted reaction times from our prior model.

# This one plots priors
prior_plot <- function(model_fit, ...) {
  p <- pp_check(model_fit, ...)
  p$data <- p$data[p$data$is_y == FALSE,]
  p 
}

# This one gets prior samples
prior_samples <- function(model_fit, ...) {
  p <- pp_check(model_fit, ...)
  p$data <- p$data[p$data$is_y == FALSE,]
  return(p$data$value)
}

Our first attempt at setting priors

For our first model, we talked a little bit about different possible priors and saw that they all made sense (more or less) in isolation. The first two lines show the model formula, and the last three specify the priors.

\(rt \sim Normal(\mu,\sigma)\)
\(\mu = \beta_0 + \beta_1 Frequency\)

\(\beta_0 \sim Normal(1000, 250)\)
\(\beta_1 \sim Normal(0, 200)\)
\(\sigma \sim Normal_+(0, 500)\)

Fitting our first set of priors

Here we save the priors above as a vector to use them in our model.

# Setting up a first set of priors
first_priors <- c(
  prior(normal(1000, 250), class = "Intercept"),
  prior(normal(0, 200), class = "b", coef = "sl_Freq"),
  prior(normal(0, 500), class = "sigma")
)

Now that we’ve saved our priors, we “fit” our model. The argument sample_prior = "only" means that the model will ignore our data, and just use our priors to create a the model object. In running this code for yourself, it doesn’t really matter what your data looks like, as long as you have a dataframe called dRT and the variables RT and sl_Freq this should run.

model_one <- brm(
  RT ~ sl_Freq, # model formula
  data = dRT, # pass the dataframe
  prior = first_priors, # pass our priors
  sample_prior = "only", # fit model using only priors
  cores = 4,
  control = list(adapt_delta = 0.95),
  file = "models/model_one.rds"
)

Visuzalizing twenty simulated datasets from our first priors

Here we use the prior_plot() function we created to simulate twenty different datasets based only on our priors. Changing the number of draws (ndraws) to a different number of simulated datasets.

# Making the plot
p1 <- prior_plot(model_one, ndraws = 20)

# Printing the plot with some minor changes
p1 +
  xlim(-2000, 5000) +
  labs(
    y = "density",
    x = "Reaction times (ms)"
  )

Getting quantiles of the reaction times

We can also use a combination of our prior_samples() function together with the quantile() function to look at the distribution of our data. Here we look at the median (0.50) along with the 1%, 5%, 95%, and 99% quantiles of our data.

quantile(prior_samples(model_one, ndraws = 100), probs = c(0.01, 0.05, 0.50, 0.95, 0.99))
         1%          5%         50%         95%         99% 
-804.756002    1.135078  945.684071 1929.392243 2650.346405 

A second attept: narrowing our priors

After seeing that our priors had too many fast (and impossible) reaction times, we then fit a model where we kept the measure of central tendency but changed the scale parameters (standard deviations) to be more narrow in order to make it so we don’t have as many negative values.

\(rt \sim Normal(\mu,\sigma)\)
\(\mu = \beta_0 + \beta_1 Frequency\)

\(\beta_0 \sim Normal(1000, 200)\)
\(\beta_1 \sim Normal(0, 100)\)
\(\sigma \sim Normal_+(0, 250)\)

second_priors <- c(
  prior(normal(1000, 200), class = "Intercept"),
  prior(normal(0, 100), class = "b", coef = "sl_Freq"),
  prior(normal(0, 250), class = "sigma")
)

Now that we have the new priors, we fit the model again.

model_two <- brm(
  RT ~ sl_Freq,
  data = dRT,
  prior = second_priors,
  sample_prior = "only",
  cores = 4,
  control = list(adapt_delta = 0.95),
  file = "models/model_two.rds"
)

We can plot the prior check again.

# Creating the plot
p2 <- prior_plot(model_two, ndraws = 20)

# Printing it with some minor changes
p2 +
  xlim(-2000, 5000) +
  labs(
    y = "density",
    x = "Reaction times (ms)"
  )

Getting quantiles of the reaction times

And we show the quantiles again.

quantile(prior_samples(model_two, ndraws = 100), probs = c(0.01, 0.05, 0.50, 0.95, 0.99))
       1%        5%       50%       95%       99% 
 108.1953  447.0236 1024.1208 1552.1931 1902.8835 

A lognormal model of reaction times

We discussed in the presentation that a lognormal model of reaction times makes more sense given reaction times. Now we have a new model of reaction times to create priors for, but the model parameters we need priors for is the same. They are now on the log scale.

# Creating vector of priors in the log space
third_priors <- c(
  prior(normal(6.9, 0.25), class = "Intercept"),
  prior(normal(0, 0.1), class = "b", coef = "sl_Freq"),
  prior(normal(0, 0.5), class = "sigma")
)

Fitting the the lognormal model with only our priors. Notice that we now have a new argument, family = lognormal() which changes the likelihood function of our model. This actually changes a number of assumptions in our model which we didn’t have time to go into, but it is fairly well-suited to modelling reaction times.

model_three <- brm(
  RT ~ sl_Freq,
  data = dRT,
  family = lognormal(),
  prior = third_priors,
  sample_prior = "only",
  cores = 4,
  control = list(adapt_delta = 0.99),
  file = "models/model_three.rds"
)

Creating the prior plot

Again, we simulate twenty datasets from our priors to check our assumptions.

p3 <- prior_plot(model_three, ndraws = 20)

p3 +
  xlim(0, 5000) +
  labs(
    y = "density",
    x = "Reaction times (ms)"
  )

Getting quantiles of the reaction times

Checking the quantiles, we see that they make a lot more sense for our assumptions about the data.

quantile(prior_samples(model_three, ndraws = 100), probs = c(0.01, 0.05, 0.50, 0.95, 0.99))
       1%        5%       50%       95%       99% 
 220.5228  421.8860  983.1498 2418.3574 4344.8047