In this project we will analyze the data from the first experiment of Ratcliff and Rouder (1998).
First, let’s clear our workspace and load a few packages:
rm(list = ls())
library(rtdists)
library(tidyverse)
library(dfoptim)
In this experiment, participants made brightness discriminations (high vs. low) of different pixel arrays. The proportion of dark to white pixels (or the brightness level) was sampled in each trial from one of two different overlapping distributions, with different mean and equal SD (dark vs light) and was then discretized in steps of 33 (so that there were only 33 possible brightness levels in the experiment).
The correct response was to match the distribution from which the brightness level was sampled from (labeled “source” in the dataset). Therefore, the noise in participants’ responses could come both from:
There were 2 main manipulations:
We can find the data already in the rtdists
package:
data(rr98) # load data
rr98 <- rr98[!rr98$outlier,] # remove outliers, as in original paper
rr98 <- rr98[rr98$block <= 8,] # keep only the first 8 sessions
rr98[,'accuracy'] = 0 # add a new column labelled as accuracy, where incorrect responses are coded as 0
rr98[rr98$correct, 'accuracy'] = 1 # and correct responses are coded as 1
head(rr98)
## id session block trial instruction source strength response response_num
## 1 jf 2 1 21 accuracy dark 8 dark 1
## 2 jf 2 1 22 accuracy dark 7 dark 1
## 3 jf 2 1 23 accuracy light 19 light 2
## 4 jf 2 1 24 accuracy dark 21 light 2
## 5 jf 2 1 25 accuracy light 19 dark 1
## 6 jf 2 1 26 accuracy dark 10 dark 1
## correct rt outlier accuracy
## 1 TRUE 0.801 FALSE 1
## 2 TRUE 0.680 FALSE 1
## 3 TRUE 0.694 FALSE 1
## 4 FALSE 0.582 FALSE 0
## 5 FALSE 0.925 FALSE 0
## 6 TRUE 0.605 FALSE 1
In the dataset there are 3 participants, who completed ~ 3800 trials in each condition (speed/accuracy), separated in 8 sessions.
Possible answer:
summarise(group_by(rr98,
id,
instruction),
n = n(),
mean_rt = mean(rt),
mean_accuracy = mean(accuracy))
## # A tibble: 6 x 5
## # Groups: id [3]
## id instruction n mean_rt mean_accuracy
## <fct> <fct> <int> <dbl> <dbl>
## 1 jf speed 3909 0.325 0.688
## 2 jf accuracy 3826 0.738 0.727
## 3 kr speed 3796 0.311 0.692
## 4 kr accuracy 3785 0.758 0.724
## 5 nh speed 3941 0.365 0.719
## 6 nh accuracy 3785 0.613 0.734
Possible answer:
ggplot(data = rr98, mapping = aes(x = factor(strength), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id))
ggplot(data = rr98, mapping = aes(x = factor(strength), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id))
Possible answer:
ggplot(data = rr98, mapping = aes(x = factor(block), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id))
ggplot(data = rr98, mapping = aes(x = factor(block), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id))
strength_binned
, in which the brightness levels are grouped into bins of equal lenght (similarly to the original paper). The idea is to have bins of brightness level with similar performance, to reduce noise in the data and to later be able to fit separate drift-rates per brightness level (33 would be a lot of drift-rates to fit).While the average is a good summary statistic for accuracy (since it’s a binary variable), when we inspect response times the average is not enough. Infact, typical response times distributions are not Gaussian, but more similar to an Inverse Gaussian. Therefore, we typically inspect quantiles rather than just the average. Moreover, it can be that the response times distribution differ for different options. Therefore, we typically want to inspect RT distributions separately per choice (in this case, light vs. dark).
Possible answer:
rr98$strength_binned <- cut(rr98$strength,
breaks = seq(min(rr98$strength), max(rr98$strength), length.out=6),
include.lowest = TRUE)
Possible answer:
ggplot(data = rr98, mapping = aes(x = rt, fill = instruction)) +
geom_histogram(binwidth=.1, alpha = .3, position="identity") +
facet_grid(rows=vars(id), cols=vars(strength_binned))
Possible answer:
ggplot(data = rr98[rr98$instruction=="speed",], mapping = aes(x = strength_binned, y=rt)) +
stat_summary(aes(group=1), fun=median, geom="line", size=.8, color="coral4") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .1), geom="line", size=.2, color="coral") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .3), geom="line", size=.5, color="coral3") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .7), geom="line", size=.5, color="coral3") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .9), geom="line", size=.2, color="coral") +
facet_grid(rows=vars(id), cols=vars(correct)) +
labs(title="Speed emphasized") +
theme(plot.title = element_text(hjust = 0.5))
ggplot(data = rr98[rr98$instruction=="accuracy",], mapping = aes(x = strength_binned, y=rt)) +
stat_summary(aes(group=1), fun=median, geom="line", size=.8, color="coral4") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .1), geom="line", size=.2, color="coral") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .3), geom="line", size=.5, color="coral3") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .7), geom="line", size=.5, color="coral3") +
stat_summary(aes(group=1), fun=function(x) quantile(x, .9), geom="line", size=.2, color="coral") +
facet_grid(rows=vars(id), cols=vars(correct)) +
labs(title="Accuracy emphasized") +
theme(plot.title = element_text(hjust = 0.5))
When fitting this data set, it is necessary to vary some of the DDM parameters across the manipulations. First of all, the brightness manipulation is most likely affecting the rate of evidence accumulation: the lower the brightness, the more “dark” decisions are made and the higher the brightness level, the more “light” decisions are made. The drift should be around 0 around the point in which the 2 underlying distributions mostly overlap, causing the performance to become worse (slower/less accurate).
Secondly, the instruction is most likely affecting the threshold: when speed is stressed, decisions are faster and less accurate (lower threshold) and when accuracy is stressed, decisions are slower and more accurate (higher threshold).
Therefore, we would like to set up a model with:
Regarding varying the drift-rates we have multiple options:
strength_binned
(as many parameters as the bins)strenght
to the drift-rate, such as drift = A + strength*B
(2 free parameters)I suggest the second one, since it is closer to what we would consider a process model, because it maps stimulus properties directly to cognitive processes, represented by the model’s parameters.
We can start from simulating a “fake” dataset, that resembles the one used in the original experiment:
library(truncnorm) # install first if not already: install.packages("truncnorm")
# simulate the stimuli
create_stimuli <- function(n_trials, mean_1=12.375, mean_2=20.625, sd=6.1875) {
dist1 <- as.integer(rtruncnorm(n_trials, a=1, b=34, mean=mean_1, sd=sd)) # dark distribution
dist2 <- as.integer(rtruncnorm(n_trials, a=1, b=34, mean=mean_2, sd=sd)) # light distribution
source <- sample(c("light", "dark"), n_trials, replace = TRUE) # sample from one of them
stimuli <- data.frame(dist1=dist1, dist2=dist2, source=source)
# add strength corresponding to the source distribution
stimuli$strength <- dist1
stimuli[stimuli$source == "light", "strength"] <- stimuli[stimuli$source == "light", "dist2"]
# add speed/accuracy manipulation
stimuli$instruction = rep(c("speed", "accuracy"), each=n_trials/2)
# separate strength in bins (based on what you did in Day 1)
stimuli$strength_binned <- cut(stimuli$strength,
breaks = seq(min(stimuli$strength), max(stimuli$strength), length.out=6),
include.lowest = TRUE)
# transform into factors
stimuli$instruction = as.factor(stimuli$instruction)
stimuli$strength = as.factor(stimuli$strength)
stimuli$source = as.factor(stimuli$source)
return(stimuli)
}
stimuli <- create_stimuli(1000) # simulate 1000 trials (for example)
summary(stimuli)
## dist1 dist2 source strength instruction
## Min. : 1.00 Min. : 2.00 dark :494 16 : 64 accuracy:500
## 1st Qu.: 8.00 1st Qu.:16.00 light:506 13 : 63 speed :500
## Median :12.00 Median :20.00 15 : 60
## Mean :12.53 Mean :19.62 19 : 60
## 3rd Qu.:16.00 3rd Qu.:24.00 18 : 52
## Max. :32.00 Max. :33.00 23 : 51
## (Other):650
## strength_binned
## [1,7.4] :123
## (7.4,13.8] :231
## (13.8,20.2]:376
## (20.2,26.6]:220
## (26.6,33] : 50
##
##
head(stimuli)
## dist1 dist2 source strength instruction strength_binned
## 1 4 16 dark 4 speed [1,7.4]
## 2 9 18 light 18 speed (13.8,20.2]
## 3 11 18 light 18 speed (13.8,20.2]
## 4 16 21 light 21 speed (20.2,26.6]
## 5 11 23 light 23 speed (20.2,26.6]
## 6 7 28 dark 7 speed [1,7.4]
Also, we can first define the DM for 1 trial, where the lower boundary corresponds to “dark” and the upper boundary corresponds to “light”:
# simulate data from simple DM
random_dm <- function(drift, threshold, ndt=.15, rel_sp=.5, noise_constant=1, dt=0.001, max_rt=10) {
choice <- NA
rt <- NA
max_tsteps <- max_rt/dt
# initialize the diffusion process
tstep <- 0
x <- rel_sp*threshold # accumulated evidence at t=tstep
# start accumulating
while (x > 0 & x < threshold & tstep < max_tsteps) {
x <- x + rnorm(mean=drift*dt, sd=noise_constant*sqrt(dt), n=1)
tstep <- tstep + 1
}
if (x <= 0) {choice = "dark"} else if (x >=threshold) {choice = "light"}
rt = dt*tstep + ndt
return (c(choice, rt))
}
Note that there are a bunch of default parameters, which we are not going to bother about at the moment. We are mostly interested in varying the drift-rate and threshold based on the stimuli
data.frame. In particular, in varying the drift-rate based on stimuli$strength
and the threshold based on stimuli$instruction
.
random_dm_vd
random_dm_vt
random_dm_vd_vt
The functions should have the following structures:
random_dm_vt <- function (stimuli, drift, threshold_speed, threshold_accuracy, ndt) {
stimuli$drift = ...
stimuli$threshold = ...
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = ...
stimuli$accuracy = ...
return(stimuli)
}
random_dm_vd <- function (stimuli, drift_int, drift_coeff, threshold, ndt) {
stimuli$drift = ...
stimuli$threshold = ...
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = ...
stimuli$accuracy = ...
return(stimuli)
}
random_dm_vd_vt <- function (stimuli, drift_int, drift_coeff, threshold_speed, threshold_accuracy, ndt) {
stimuli$drift = ...
stimuli$threshold = ...
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = ...
stimuli$accuracy = ...
return(stimuli)
}
Possible answer:
random_dm_vt <- function(stimuli, drift, threshold_speed, threshold_accuracy, ndt) {
stimuli$drift = drift
stimuli$threshold = threshold_speed
stimuli[stimuli$instruction == "accuracy", "threshold"] = threshold_accuracy
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = t(apply(stimuli[,c('drift','threshold','ndt')], 1, function(x) random_dm(x[1],x[2],x[3])))
stimuli$accuracy <- NA
stimuli[!is.na(stimuli$choice) & (stimuli$source == stimuli$choice), "accuracy"] = 1
stimuli[!is.na(stimuli$choice) & (stimuli$source != stimuli$choice), "accuracy"] = 0
stimuli$choice = as.factor(stimuli$choice)
stimuli$rt = as.numeric(stimuli$rt)
return(stimuli)
}
random_dm_vd <- function(stimuli, drift_int, drift_coeff, threshold, ndt) {
stimuli$drift = drift_int + drift_coeff*as.numeric(stimuli$strength)
stimuli$threshold = threshold
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = t(apply(stimuli[,c('drift','threshold','ndt')], 1, function(x) random_dm(x[1],x[2],x[3])))
stimuli$accuracy <- NA
stimuli[!is.na(stimuli$choice) & (stimuli$source == stimuli$choice), "accuracy"] = 1
stimuli[!is.na(stimuli$choice) & (stimuli$source != stimuli$choice), "accuracy"] = 0
stimuli$choice = as.factor(stimuli$choice)
stimuli$rt = as.numeric(stimuli$rt)
return(stimuli)
}
random_dm_vd_vt <- function(stimuli, drift_int, drift_coeff, threshold_speed, threshold_accuracy, ndt) {
stimuli$drift = drift_int + drift_coeff*as.numeric(stimuli$strength)
stimuli$threshold = threshold_speed
stimuli[stimuli$instruction == "accuracy", "threshold"] = threshold_accuracy
stimuli$ndt = ndt
stimuli[,c("choice", "rt")] = t(apply(stimuli[,c('drift','threshold','ndt')], 1, function(x) random_dm(x[1],x[2],x[3])))
stimuli$accuracy <- NA
stimuli[!is.na(stimuli$choice) & (stimuli$source == stimuli$choice), "accuracy"] = 1
stimuli[!is.na(stimuli$choice) & (stimuli$source != stimuli$choice), "accuracy"] = 0
stimuli$choice = as.factor(stimuli$choice)
stimuli$rt = as.numeric(stimuli$rt)
return(stimuli)
}
Possible answer:
### Varying only the threshold
sim_data_vt <- random_dm_vt(stimuli, drift = 0, threshold_speed = 1, threshold_accuracy = 2.5, ndt = .15)
summary(sim_data_vt)
## dist1 dist2 source strength instruction
## Min. : 1.00 Min. : 2.00 dark :494 16 : 64 accuracy:500
## 1st Qu.: 8.00 1st Qu.:16.00 light:506 13 : 63 speed :500
## Median :12.00 Median :20.00 15 : 60
## Mean :12.53 Mean :19.62 19 : 60
## 3rd Qu.:16.00 3rd Qu.:24.00 18 : 52
## Max. :32.00 Max. :33.00 23 : 51
## (Other):650
## strength_binned drift threshold ndt choice
## [1,7.4] :123 Min. :0 Min. :1.00 Min. :0.15 dark :501
## (7.4,13.8] :231 1st Qu.:0 1st Qu.:1.00 1st Qu.:0.15 light:499
## (13.8,20.2]:376 Median :0 Median :1.75 Median :0.15
## (20.2,26.6]:220 Mean :0 Mean :1.75 Mean :0.15
## (26.6,33] : 50 3rd Qu.:0 3rd Qu.:2.50 3rd Qu.:0.15
## Max. :0 Max. :2.50 Max. :0.15
##
## rt accuracy
## Min. :0.1700 Min. :0.000
## 1st Qu.:0.3640 1st Qu.:0.000
## Median :0.6445 Median :1.000
## Mean :1.1173 Mean :0.527
## 3rd Qu.:1.4033 3rd Qu.:1.000
## Max. :7.1840 Max. :1.000
##
# note that the drift-rate here is 0, because it does not depend in any way from the evidence...
ggplot(data = sim_data_vt, mapping = aes(x = factor(strength_binned), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
ggplot(data = sim_data_vt, mapping = aes(x = factor(strength_binned), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
### Varying only the drift
sim_data_vd <- random_dm_vd(stimuli, drift_int = -1.75, drift_coeff = .1, threshold = 2, ndt = .15)
head(sim_data_vd, 10)
## dist1 dist2 source strength instruction strength_binned drift threshold ndt
## 1 4 16 dark 4 speed [1,7.4] -1.35 2 0.15
## 2 9 18 light 18 speed (13.8,20.2] 0.05 2 0.15
## 3 11 18 light 18 speed (13.8,20.2] 0.05 2 0.15
## 4 16 21 light 21 speed (20.2,26.6] 0.35 2 0.15
## 5 11 23 light 23 speed (20.2,26.6] 0.55 2 0.15
## 6 7 28 dark 7 speed [1,7.4] -1.05 2 0.15
## 7 20 26 light 26 speed (20.2,26.6] 0.85 2 0.15
## 8 16 19 dark 16 speed (13.8,20.2] -0.15 2 0.15
## 9 19 30 light 30 speed (26.6,33] 1.25 2 0.15
## 10 18 20 dark 18 speed (13.8,20.2] 0.05 2 0.15
## choice rt accuracy
## 1 dark 1.620 1
## 2 light 0.703 1
## 3 light 1.599 1
## 4 light 2.314 1
## 5 light 1.937 1
## 6 dark 1.123 1
## 7 light 1.276 1
## 8 dark 1.357 1
## 9 light 0.695 1
## 10 light 1.283 0
# note that there is no difference between speed and accuracy conditions because we did not vary the threshold...
ggplot(data = sim_data_vd, mapping = aes(x = factor(strength_binned), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
ggplot(data = sim_data_vd, mapping = aes(x = factor(strength_binned), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
### Varying both
sim_data_vd_vt <- random_dm_vd_vt(stimuli, drift_int = -1.75, drift_coeff = .1,
threshold_speed = .8, threshold_accuracy = 2.5, ndt = .15)
head(sim_data_vd_vt, 10)
## dist1 dist2 source strength instruction strength_binned drift threshold ndt
## 1 4 16 dark 4 speed [1,7.4] -1.35 0.8 0.15
## 2 9 18 light 18 speed (13.8,20.2] 0.05 0.8 0.15
## 3 11 18 light 18 speed (13.8,20.2] 0.05 0.8 0.15
## 4 16 21 light 21 speed (20.2,26.6] 0.35 0.8 0.15
## 5 11 23 light 23 speed (20.2,26.6] 0.55 0.8 0.15
## 6 7 28 dark 7 speed [1,7.4] -1.05 0.8 0.15
## 7 20 26 light 26 speed (20.2,26.6] 0.85 0.8 0.15
## 8 16 19 dark 16 speed (13.8,20.2] -0.15 0.8 0.15
## 9 19 30 light 30 speed (26.6,33] 1.25 0.8 0.15
## 10 18 20 dark 18 speed (13.8,20.2] 0.05 0.8 0.15
## choice rt accuracy
## 1 dark 0.366 1
## 2 light 0.260 1
## 3 light 0.308 1
## 4 light 0.280 1
## 5 dark 0.240 0
## 6 dark 0.189 1
## 7 light 0.338 1
## 8 light 0.326 0
## 9 light 0.366 1
## 10 dark 0.265 1
ggplot(data = sim_data_vd_vt, mapping = aes(x = factor(strength_binned), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
ggplot(data = sim_data_vd_vt, mapping = aes(x = factor(strength_binned), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9)
ddiffusion
function from the rtdists
package. The likelihood functions should have the following structure:# Likelihood of the model with only varying drift
log_likelihood_vd <- function(par, data, ll_threshold=1e-10) {
# par order: drift_int, drift_coeff, threshold, ndt
drift = ...
threshold = ...
ndt = ...
density <- ddiffusion(...)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
# Likelihood of the model with only varying threhsold
log_likelihood_vt <- function(par, data, ll_threshold=1e-10) {
# par order: drift, threshold_speed, threshold_accuracy, ndt
drift = ...
threshold = ...
ndt = ...
density <- ddiffusion(...)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
log_likelihood_vd_vt <- function(par, data, ll_threshold=1e-10) {
# par order: drift_int, drift_coeff, threshold_speed, threshold_accuracy, ndt
drift = ...
threshold = ...
ndt = ...
density <- ddiffusion(...)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
Possible answer:
# Likelihood of the model with only varying drift
log_likelihood_vd <- function(par, data, ll_threshold=1e-10) {
# par order: drift_int, drift_coeff, threshold, ndt
drift = par[1] + par[2]*as.numeric(data$strength)
threshold = rep(par[3], dim(data)[1])
ndt = rep(par[4], dim(data)[1])
density <- ddiffusion(rt=data$rt, response=data$response, a=threshold, v=drift, t0=ndt)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
# Likelihood of the model with only varying threhsold
log_likelihood_vt <- function(par, data, ll_threshold=1e-10) {
# par order: drift, threshold_speed, threshold_accuracy, ndt
drift = rep(par[1], dim(data)[1])
threshold = rep(par[2], dim(data)[1])
threshold[data$instruction == "accuracy"] = par[3]
ndt = rep(par[4], dim(data)[1])
density <- ddiffusion(rt=data$rt, response=data$response, a=threshold, v=drift, t0=ndt)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
# Likelihood of the full model
log_likelihood_vd_vt <- function(par, data, ll_threshold=1e-10) {
# par order: drift_int, drift_coeff, threshold_speed, threshold_accuracy, ndt
drift = par[1] + par[2]*as.numeric(data$strength)
threshold = rep(par[3], dim(data)[1])
threshold[data$instruction == "accuracy"] = par[4]
ndt = rep(par[5], dim(data)[1])
density <- ddiffusion(rt=data$rt, response=data$response, a=threshold, v=drift, t0=ndt)
density[density <= ll_threshold] = ll_threshold # put a threhsold on very low likelihoods for computability
return(sum(log(density)))
}
Possible answer:
sim_data_vd_vt$response = "lower"
sim_data_vd_vt[sim_data_vd_vt$choice == "light", "response"] = "upper"
head(sim_data_vd_vt)
## dist1 dist2 source strength instruction strength_binned drift threshold ndt
## 1 4 16 dark 4 speed [1,7.4] -1.35 0.8 0.15
## 2 9 18 light 18 speed (13.8,20.2] 0.05 0.8 0.15
## 3 11 18 light 18 speed (13.8,20.2] 0.05 0.8 0.15
## 4 16 21 light 21 speed (20.2,26.6] 0.35 0.8 0.15
## 5 11 23 light 23 speed (20.2,26.6] 0.55 0.8 0.15
## 6 7 28 dark 7 speed [1,7.4] -1.05 0.8 0.15
## choice rt accuracy response
## 1 dark 0.366 1 lower
## 2 light 0.260 1 upper
## 3 light 0.308 1 upper
## 4 light 0.280 1 upper
## 5 dark 0.240 0 lower
## 6 dark 0.189 1 lower
starting_values = c(-2, .5, .5, 1, .2) # set some starting values
print(log_likelihood_vd_vt(starting_values, data=sim_data_vd_vt)) # check that starting values are plausible
## [1] -11383.71
fit <- nmkb(par = starting_values,
fn = function (x) log_likelihood_vd_vt(x, data=sim_data_vd_vt),
lower = c(-10, -10, 0, 0, 0),
upper = c(10, 10, 10, 10, 10),
control = list(maximize = TRUE))
print(fit$par) # print estimated parameters
## [1] -1.8701236 0.1063202 0.8537675 2.5692527 0.1477967
fit$value
, which we will need for quantitative model comparison in Day 4.Possible answer:
# Recoding for the likelihood
rr98$choice = rr98$response
rr98$response = as.character(rr98$response)
rr98[rr98$choice == "light", "response"] = "upper"
rr98[rr98$choice == "dark", "response"] = "lower"
head(rr98)
## id session block trial instruction source strength response response_num
## 1 jf 2 1 21 accuracy dark 8 lower 1
## 2 jf 2 1 22 accuracy dark 7 lower 1
## 3 jf 2 1 23 accuracy light 19 upper 2
## 4 jf 2 1 24 accuracy dark 21 upper 2
## 5 jf 2 1 25 accuracy light 19 lower 1
## 6 jf 2 1 26 accuracy dark 10 lower 1
## correct rt outlier accuracy strength_binned choice
## 1 TRUE 0.801 FALSE 1 (6.4,12.8] dark
## 2 TRUE 0.680 FALSE 1 (6.4,12.8] dark
## 3 TRUE 0.694 FALSE 1 (12.8,19.2] light
## 4 FALSE 0.582 FALSE 0 (19.2,25.6] light
## 5 FALSE 0.925 FALSE 0 (12.8,19.2] dark
## 6 TRUE 0.605 FALSE 1 (6.4,12.8] dark
# Fitting the model with only varying drift
starting_values = c(-1, 1, 1, .1) # set some starting values
for (pp in unique(rr98$id)) {
print(pp)
data_pp = rr98[rr98$id == pp,]
print(log_likelihood_vd(starting_values, data=data_pp)) # check that starting values are plausible
fit <- nmkb(par = starting_values,
fn = function (x) log_likelihood_vd(x, data=data_pp),
lower = c(-10, -10, 0, 0),
upper = c(10, 10, 10, 3),
control = list(maximize = TRUE))
print(fit$par) # print estimated parameters
rr98[rr98$id == pp, "vd_estim_drift_int"] = fit$par[1]
rr98[rr98$id == pp, "vd_estim_drift_coeff"] = fit$par[2]
rr98[rr98$id == pp, "vd_estim_threshold"] = fit$par[3]
rr98[rr98$id == pp, "vd_estim_ndt"] = fit$par[4]
rr98[rr98$id == pp, "vd_ll"] = fit$value
rr98[rr98$id == pp, "vd_BIC"] = -2*fit$value + log(dim(data_pp)[1])*4
}
## [1] "jf"
## [1] -148335.8
## [1] -3.3915215 0.2141582 1.3449426 0.1689229
## [1] "kr"
## [1] -141107.6
## [1] -4.0354657 0.2709920 1.3951626 0.1612836
## [1] "nh"
## [1] -150090.3
## [1] -4.9853900 0.3181676 1.3646289 0.1904508
# Fitting the model with only varying thresholds
starting_values = c(0, 1, 1, .1) # set some starting values
for (pp in unique(rr98$id)) {
print(pp)
data_pp = rr98[rr98$id == pp,]
print(log_likelihood_vt(starting_values, data=data_pp)) # check that starting values are plausible
fit <- nmkb(par = starting_values,
fn = function (x) log_likelihood_vt(x, data=data_pp),
lower = c(-10, 0, 0, 0),
upper = c(10, 10, 10, 3),
control = list(maximize = TRUE))
print(fit$par) # print estimated parameters
rr98[rr98$id == pp, "vt_estim_drift"] = fit$par[1]
rr98[rr98$id == pp, "vt_estim_threshold_speed"] = fit$par[2]
rr98[rr98$id == pp, "vt_estim_threshold_accuracy"] = fit$par[3]
rr98[rr98$id == pp, "vt_estim_ndt"] = fit$par[4]
rr98[rr98$id == pp, "vt_ll"] = fit$value
rr98[rr98$id == pp, "vt_BIC"] = -2*fit$value + log(dim(data_pp)[1])*4
}
## [1] "jf"
## [1] -7555.47
## [1] 0.05318701 0.74796190 1.56374596 0.19576029
## [1] "kr"
## [1] -7588.837
## [1] 0.1628137 0.7049823 1.5438007 0.1956649
## [1] "nh"
## [1] -5886.646
## [1] 0.0514629 0.8121707 1.3172762 0.2148553
# Fitting the full model
starting_values = c(-1, 1, .5, 1, .1) # set some starting values
for (pp in unique(rr98$id)) {
print(pp)
data_pp = rr98[rr98$id == pp,]
print(log_likelihood_vd_vt(starting_values, data=data_pp)) # check that starting values are plausible
fit <- nmkb(par = starting_values,
fn = function (x) log_likelihood_vd_vt(x, data=data_pp),
lower = c(-10, -10, 0, 0, 0),
upper = c(10, 10, 10, 10, 3),
control = list(maximize = TRUE))
print(fit$par) # print estimated parameters
rr98[rr98$id == pp, "vd_vt_estim_drift_int"] = fit$par[1]
rr98[rr98$id == pp, "vd_vt_estim_drift_coeff"] = fit$par[2]
rr98[rr98$id == pp, "vd_vt_estim_threshold_speed"] = fit$par[3]
rr98[rr98$id == pp, "vd_vt_estim_threshold_accuracy"] = fit$par[4]
rr98[rr98$id == pp, "vd_vt_estim_ndt"] = fit$par[5]
rr98[rr98$id == pp, "vd_vt_ll"] = fit$value
rr98[rr98$id == pp, "vd_vt_BIC"] = -2*fit$value + log(dim(data_pp)[1])*5
}
## [1] "jf"
## [1] -150529.1
## [1] -3.9853991 0.2526818 0.8184609 1.9951662 0.1945865
## [1] "kr"
## [1] -144242.3
## [1] -4.9192273 0.3288586 0.7884596 2.0337870 0.1942775
## [1] "nh"
## [1] -151784.3
## [1] -5.4778267 0.3496392 0.9716096 1.7451990 0.2115727
fit$value
that you should have saved in Day 3. Based on the BIC, which model was best?Possible answer:
# Compare the BICs
BICs = distinct(select(rr98, id, vd_BIC, vt_BIC, vd_vt_BIC))
BICs
## id vd_BIC vt_BIC vd_vt_BIC
## 1 jf 3393.060 2689.774 -2755.939
## 2 kr 2531.450 2380.451 -4466.037
## 3 nh -2497.415 2053.907 -5244.654
# parameter estimates full (winning) model
par_estim = distinct(select(rr98,
id,
vd_vt_estim_drift_int, vd_vt_estim_drift_coeff,
vd_vt_estim_threshold_speed, vd_vt_estim_threshold_accuracy,
vd_vt_estim_ndt))
par_estim
## id vd_vt_estim_drift_int vd_vt_estim_drift_coeff vd_vt_estim_threshold_speed
## 1 jf -3.985399 0.2526818 0.8184609
## 2 kr -4.919227 0.3288586 0.7884596
## 3 nh -5.477827 0.3496392 0.9716096
## vd_vt_estim_threshold_accuracy vd_vt_estim_ndt
## 1 1.995166 0.1945865
## 2 2.033787 0.1942775
## 3 1.745199 0.2115727
random_dm_vd_vt
, you generate trials based on the parameters that we estimated for each subject. Now, plot side by side the model’s predictions with the real data, across instruction levels and strength bins. What do you observe? How would you improve this model?Possible answer:
sim_data = data.frame()
for (pp in unique(rr98$id)) {
print(pp)
data_pp = rr98[rr98$id == pp,]
estim_par = distinct(select(data_pp,
vd_vt_estim_drift_int,
vd_vt_estim_drift_coeff,
vd_vt_estim_threshold_speed,
vd_vt_estim_threshold_accuracy,
vd_vt_estim_ndt))
sim_pp = random_dm_vd_vt(data_pp, estim_par[,1], estim_par[,2], estim_par[,3], estim_par[,4], estim_par[,5])
sim_pp = sim_pp[,c("id", "instruction", "strength", "choice", "strength_binned", "rt", "accuracy")]
sim_data = rbind(sim_data, sim_pp)
}
## [1] "jf"
## [1] "kr"
## [1] "nh"
sim_data$data = "simulated"
rr98$data = "real"
compare = rbind(sim_data, rr98[,c("id", "instruction", "strength", "choice", "strength_binned", "rt", "accuracy", "data")])
ggplot(data = compare, mapping = aes(x = factor(strength_binned), y = rt, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id), cols = vars(data))
ggplot(data = compare, mapping = aes(x = factor(strength_binned), y = accuracy, color=instruction)) +
stat_summary(fun = "mean", geom="point") +
stat_summary(fun.data = mean_cl_normal, geom = "errorbar", size=.2, width=.9) +
facet_grid(rows=vars(id), cols = vars(data))