Skip to contents

The Data

For this chapter, we will be using the data from Wagenmakers et al., (2008) - Experiment 1 also reanalyzed by Heathcote & Love (2012), that contains responses and response times for several participants in two conditions (where instructions emphasized either speed or accuracy). Using the same procedure as the authors, we excluded all trials with uninterpretable response time, i.e., responses that are too fast (<200 ms instead of <180 ms) or too slow (>2 sec instead of >3 sec).

set.seed(123)  # For reproducibility

df <- read.csv("https://raw.githubusercontent.com/DominiqueMakowski/CognitiveModels/main/data/wagenmakers2008.csv")
df <- df[df$RT > 0.2 & df$Participant %in% c(1, 2, 3), ]

# Show 10 first rows
head(df, 10)
#>    Participant Condition    RT Error Frequency
#> 1            1     Speed 0.700 FALSE       Low
#> 2            1     Speed 0.392  TRUE  Very Low
#> 3            1     Speed 0.460 FALSE  Very Low
#> 4            1     Speed 0.455 FALSE  Very Low
#> 5            1     Speed 0.505  TRUE       Low
#> 6            1     Speed 0.773 FALSE      High
#> 7            1     Speed 0.390 FALSE      High
#> 8            1     Speed 0.587  TRUE       Low
#> 9            1     Speed 0.603 FALSE       Low
#> 10           1     Speed 0.435 FALSE      High

We are going to first take interest in the response times (RT) of Correct answers only (as we can assume that errors are underpinned by a different generative process).

df <- df[df$Error == 0, ]
ggplot(df, aes(x = RT, fill = Condition)) +
  geom_histogram(bins = 120, alpha = 0.8, position = "identity") +
  scale_fill_manual(values = c("darkgreen", "darkred")) +
  theme_minimal()

Models

Normal

A basic linear model.

Code
f <- bf(
  RT ~ Condition
)

m_normal <- brm(f,
  data = df, 
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_normal <- brms::add_criterion(m_normal, "loo") 

saveRDS(m_normal, file = "models/m_normal.rds")

ExGaussian

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  beta ~ Condition,
  family = exgaussian()
)

m_exgauss <- brm(f,
  data = df, 
  family = exgaussian(), 
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_exgauss <- brms::add_criterion(m_exgauss, "loo") 

saveRDS(m_exgauss, file = "models/m_exgauss.rds")

Shifted LogNormal

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_lognormal()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_lognormal <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_lognormal_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_lognormal <- brms::add_criterion(m_lognormal, "loo") 

saveRDS(m_lognormal, file = "models/m_lognormal.rds")

Inverse Gaussian (Shifted Wald)

Code
f <- bf(
  RT ~ Condition,
  bs ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_invgaussian()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_wald <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_invgaussian_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_wald <- brms::add_criterion(m_wald, "loo") 

saveRDS(m_wald, file = "models/m_wald.rds")

Weibull

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_weibull()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_weibull <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_weibull_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_weibull <- brms::add_criterion(m_weibull, "loo") 

saveRDS(m_weibull, file = "models/m_weibull.rds")

LogWeibull (Shifted Gumbel)

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_logweibull()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_logweibull <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_logweibull_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_logweibull <- brms::add_criterion(m_logweibull, "loo") 

saveRDS(m_logweibull, file = "models/m_logweibull.rds")

Inverse Weibull (Shifted Fréchet)

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_invweibull()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_invweibull <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_invweibull_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_invweibull <- brms::add_criterion(m_invweibull, "loo") 

saveRDS(m_invweibull, file = "models/m_invweibull.rds")

Gamma

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_gamma()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_gamma <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_gamma_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_gamma <- brms::add_criterion(m_gamma, "loo") 

saveRDS(m_gamma, file = "models/m_gamma.rds")

Inverse Gamma

Code
f <- bf(
  RT ~ Condition,
  sigma ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_invgamma()
)

priors <- brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau") |>
  brms::validate_prior(f, data = df)

m_invgamma <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_invgamma_stanvars(),
  init = 0,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_invgamma <- brms::add_criterion(m_invgamma, "loo") 

saveRDS(m_invgamma, file = "models/m_invgamma.rds")

Linear Ballistic Accumulator

Code
f <- bf(
  RT ~ Condition,
  sigma = 1,
  sigmabias ~ Condition,
  bs ~ Condition,
  tau ~ Condition,
  minrt = min(df$RT),
  family = rt_lba()
)

priors <- c(
  brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "mu"),
  brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "sigmabias"),
  brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "bs"),
  brms::set_prior("normal(0, 1)", class = "Intercept", dpar = "tau")
  )|>
  brms::validate_prior(f, data = df)

m_lba <- brm(
  f,
  prior = priors,
  data = df, 
  stanvars = rt_lba_stanvars(),
  init = 0.5,
  chains = 4, iter = 500, backend = "cmdstanr"
)

m_lba <- brms::add_criterion(m_lba, "loo") 

saveRDS(m_lba, file = "models/m_lba.rds")

Model Comparison

Model Fit

We can compare these models together using the loo package, which shows that CHOCO provides a significantly better fit than the other models.

loo::loo_compare(m_normal, m_exgauss, m_lognormal, m_wald,
                 m_weibull, m_logweibull, m_invweibull,
                 m_gamma, m_invgamma, m_lba) |>
  parameters(include_ENP = TRUE)
#> # Fixed Effects
#> 
#> Name         |    LOOIC |   ENP |    ELPD | Difference | Difference_SE |      p
#> -------------------------------------------------------------------------------
#> m_lba        | -4845.13 |  7.29 | 2422.56 |       0.00 |          0.00 |       
#> m_invweibull | -4840.82 |  4.72 | 2420.41 |      -2.15 |          5.95 | 0.717 
#> m_logweibull | -4840.37 |  5.03 | 2420.18 |      -2.38 |          6.00 | 0.691 
#> m_invgamma   | -4831.71 |  8.31 | 2415.85 |      -6.71 |          6.22 | 0.281 
#> m_exgauss    | -4792.89 |  6.62 | 2396.45 |     -26.12 |          9.53 | 0.006 
#> m_lognormal  | -4763.00 |  8.80 | 2381.50 |     -41.06 |         11.08 | < .001
#> m_wald       | -4728.73 | 10.84 | 2364.36 |     -58.20 |         15.07 | < .001
#> m_gamma      | -4393.23 |  7.53 | 2196.61 |    -225.95 |         24.80 | < .001
#> m_weibull    | -3680.39 |  9.33 | 1840.19 |    -582.37 |         38.96 | < .001
#> m_normal     | -1904.18 |  7.82 |  952.09 |   -1470.47 |         73.27 | < .001

Sampling Duration

rbind(
  data_modify(attributes(m_normal$fit)$metadata$time$chain, Model="Gaussian"),
  data_modify(attributes(m_exgauss$fit)$metadata$time$chain, Model="ExGaussian"),
  data_modify(attributes(m_lognormal$fit)$metadata$time$chain, Model="LogNormal"),
  data_modify(attributes(m_wald$fit)$metadata$time$chain, Model="Wald"),
  data_modify(attributes(m_weibull$fit)$metadata$time$chain, Model="Weibull"),
  data_modify(attributes(m_logweibull$fit)$metadata$time$chain, Model="LogWeibull"),
  data_modify(attributes(m_invweibull$fit)$metadata$time$chain, Model="InvWeibull"),
  data_modify(attributes(m_gamma$fit)$metadata$time$chain, Model="Gamma"),
  data_modify(attributes(m_invgamma$fit)$metadata$time$chain, Model="InvGamma"),
  data_modify(attributes(m_lba$fit)$metadata$time$chain, Model="LBA")
) |> 
  ggplot(aes(x = Model, y = total, fill = Model)) +
  geom_boxplot() +
  labs(y = "Sampling Duration (s)") +
  scale_fill_material_d(guide = "none") +
  # scale_y_log10() +
  theme_minimal() 

Posterior Predictive Check

iterations controls the actual number of iterations used (e.g., for the point-estimate) and keep_iterations the number included.

Code

pred <- rbind(
  # estimate_prediction(m_normal, keep_iterations = 50, iterations = 50) |>
  #   reshape_iterations() |>
  #   data_modify(Model = "Normal"),
  estimate_prediction(m_exgauss, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "ExGaussian"),
  estimate_prediction(m_lognormal, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "LogNormal"),
  estimate_prediction(m_wald, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "InvGaussian"),
  estimate_prediction(m_weibull, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "Weibull"),
  estimate_prediction(m_logweibull, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "LogWeibull"),
  estimate_prediction(m_invweibull, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "InvWeibull"),
  estimate_prediction(m_gamma, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "Gamma"),
  estimate_prediction(m_invgamma, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "InvGamma"),
  estimate_prediction(m_lba, keep_iterations = 50, iterations = 50) |>
    reshape_iterations() |>
    data_modify(Model = "LBA") |> 
    data_filter(iter_value < 2)
)
#> Loading required namespace: rstan

p <- pred |> 
  ggplot(aes(x=iter_value)) +
  geom_histogram(data = df, aes(x=RT, y = after_stat(density), fill = Condition), 
                 position = "identity", bins=120, alpha = 0.8) +
  geom_line(aes(color=Model, group=interaction(Condition, iter_group)), stat="density", alpha=0.2) +
  theme_minimal() +
  facet_wrap(~Model) +
  coord_cartesian(xlim = c(0, 2)) +
  scale_fill_manual(values = c("darkgreen", "darkred")) +
  scale_color_material_d(guide = "none") +
  labs(x = "RT (s)")
p