The Log-Normal Race (LNR) model is useful for modeling reaction times and
choices in decision-making tasks. Each choice option (accumulator) draws a
processing time from a LogNormal distribution; the winning accumulator (the
minimum draw) determines both the observed reaction time and the choice. The
observed RT is that decision time shifted by a non-decision time ndt, and a
fixed proportion poutlier of responses is generated by an outlier process
instead of by the race.
Functions:
rcogmod_lnr(): Simulates random draws from the LNR model.dcogmod_lnr(): Computes the density (likelihood).cogmod_lnr(): Creates abrms::custom_family()for use inbrmsmodels.cogmod_lnr_stanvars(): Generates thestanvarsto pass tobrm().p_outlier(): Per-trial posterior probability of being an outlier.
Usage
rcogmod_lnr(
n,
nuzero = 0,
nuone = 0,
sigmazero = 1,
sigmaone = 1,
ndt = 0.2,
poutlier = 0
)
dcogmod_lnr(
x,
nuzero = 0,
nuone = 0,
sigmazero = 1,
sigmaone = 1,
ndt = 0.2,
response,
poutlier = 0,
log = FALSE
)
cogmod_lnr(
link_mu = "identity",
link_nuone = "identity",
link_sigmazero = "softplus",
link_sigmaone = "softplus",
link_ndt = "log",
link_poutlier = "logit",
predict_outliers = FALSE
)
cogmod_lnr_lpdf_expose()
cogmod_lnr_stanvars()
log_lik_cogmod_lnr(i, prep)
posterior_predict_cogmod_lnr(i, prep, predict_outliers = NULL, ...)
posterior_epred_cogmod_lnr(prep)Arguments
- n
Number of simulated trials. If
length(n) > 1, the length is taken to be the number required.- nuzero, nuone
The (inverse of the) log-space mean parameter for both accumulators (choice 0 and 1). Controls the central tendency of the reaction time. Can take any real value (-Inf, Inf), with larger values leading to faster RTs. Named 'nu' (=-meanlog) for consistency with other race models.
- sigmazero, sigmaone
The log-space standard deviation for both accumulators (choice 0 and 1). Controls the variability of reaction times. Must be positive (0, Inf). Larger values increase variability.
- ndt
Non-decision time (shift parameter), in seconds. Represents the time taken for processes unrelated to the decision (e.g., encoding, motor response). Must be non-negative. Range: [0, Inf).
- poutlier
Proportion of responses generated by the outlier process rather than by the race. Range:
[0, 1]. Atpoutlier = 0the distribution reduces to the plain shifted LNR.- x
The observed reaction time (RT).
- response
The decision indicator (0 or 1). 0 for choice 0, 1 for choice 1.
- log
Logical; if TRUE, returns the log-density. Default: FALSE.
- link_mu, link_nuone
Link function for the nu parameters.
muisnuzero:brmsrequires the first distributional parameter of a custom family to be calledmu, so that is the name the formula and this argument use, andnuzerois what it means.- link_sigmazero, link_sigmaone
Link function for the sigma parameters.
- link_ndt, link_poutlier
Link functions for the non-decision time and the outlier rate.
- predict_outliers
Logical; whether
posterior_predict()should include the outlier component.FALSE(the default) fixespoutlierto zero for prediction, so predictions describe the race alone; the likelihood is always the full mixture either way. On the prediction method itself the default isNULL, which defers to the flag carried on the model - seewith_outliers()to change it after fitting. See Details.- i, prep
For brms' functions to run: index of the observation and a
brmspreparation object.- ...
Additional arguments.
Value
rcogmod_lnr() returns a data frame with n rows and two columns,
rt (the simulated reaction time, in seconds) and response (the
boundary reached, 0 or 1, matching the dec() coding used by the
brms family). dcogmod_lnr() returns the defective density at each
element of x - the log density if log = TRUE - recycled to the length
of the longest argument. cogmod_lnr() returns a brms::custom_family
object, to put on a brms::bf() formula. cogmod_lnr_stanvars() returns
a brms::stanvars object holding the family's Stan functions block, to
pass to brms::brm(), and cogmod_lnr_lpdf_expose() compiles that Stan
code and returns it as an R function, for checking the density outside of
a model. The remaining functions are brms post-processing methods,
called by brms rather than directly: log_lik_cogmod_lnr() returns a
numeric vector holding one log-likelihood value per posterior draw for
observation i, and posterior_predict_cogmod_lnr() a draws x 2 matrix
of reaction times and choices simulated for observation i.
posterior_epred_cogmod_lnr() returns nothing: the expected reaction
time of a race has no closed form, so it errors rather than report one -
summarise posterior_predict() draws instead.
Parameterization
Each accumulator k finishes at a LogNormal time with meanlog = -nu_k and
sdlog = sigma_k, so larger nu means faster. The observed reaction time
is ndt + min(T_0, T_1) and the observed choice is whichever accumulator got
there first.
ndt is expressed directly, in seconds (through a log link in the brms
family). Nothing about it is taken from the data: it is not bounded by the
fastest observed response, so a non-decision time that varies by condition or
by participant can exceed the sample minimum wherever the data support it.
Tying ndt to the fastest observed response would cap it at an order
statistic of the sample, so any condition or participant whose true ndt
exceeded that response would be inexpressible, and the misfit would surface
as spurious effects on the race parameters. Expressing it directly is what
avoids that.
The outlier component
A shifted distribution assigns exactly zero density to any response faster
than ndt, which puts a hard boundary in the likelihood at the fastest
observed RT. Mixing in a component with support over the whole positive line
removes it: every response keeps positive density whatever ndt is, so the
boundary becomes a finite cost rather than a wall and the log-density stays
smooth and differentiable. That is what makes the direct parameterization of
ndt workable without taking a bound from the data.
Because this model produces a choice as well as a time, the contaminant
has to produce both. It is a guess: the choice is uniform over the two
options, and the RT is a half Normal with scale 0.2 seconds.
$$f(t, k) = p \frac{1}{K} g(t) + (1 - p) f_k(t - ndt)$$
The 1 / K is what keeps the total summing to one over the response options;
without it it would come to 1 + poutlier. A half Normal is used for the
timing because it is flat at the origin (zero derivative), so the very
fastest responses - the ones least plausibly decisions - are not starved of
density, and because it dies away fast enough above ndt to leave the slow
tail to the decision process. Plot it with curve(2 * dnorm(x, 0, 0.2), 0, 3).
poutlier is a rate, not a classification: the model never labels
individual trials, it estimates what share of them came from elsewhere. Use
p_outlier() for per-trial posterior probabilities.
Reaction times must be in seconds
The outlier component's scale is a constant in seconds, and so are the
priors cogmod_priors() supplies. There is no argument for changing the
unit. Millisecond data fails silently rather than loudly - the outlier
component contributes nothing and the min-RT boundary comes back. See the
corresponding section of cogmod_lognormal() for the full account, which
applies unchanged here.
Fitting
f <- brms::bf(RT | dec(Error) ~ Condition, nuone ~ Condition,
sigmazero ~ 1, sigmaone ~ 1, ndt ~ 1, poutlier ~ 1,
family = cogmod_lnr())
brms::brm(f, data = df,
prior = cogmod_priors(f, df),
init = cogmod_inits(f, df),
stanvars = cogmod_stanvars(f))Use cogmod_inits() rather than init = 0. brms initialises on the
unconstrained scale, so init = 0 puts ndt at exp(0) = 1 second - above
nearly every sub-second RT, which leaves every response attributed to the
outlier component and the race parameters with no gradient at all.
cogmod_priors() is not a convenience here either. Beyond ndt and
poutlier, a race has a flat direction of its own: push an accumulator's
rate far enough down and it stops finishing first ever, so the density
depends on it only through the loser's survival term, which has already
saturated at 1. Past about nuone = -6 the log-likelihood is exactly
constant, and that accumulator's sigma is unidentified along with it -
nothing is left for it to act on. On the identity link nuone uses, that is
an unbounded flat region under a flat prior: an improper posterior, the same
failure as poutlier running to 1.
The outlier component makes this reachable rather than hypothetical. Without
it, an accumulator that never wins would still have to explain the trials on
which the other one lost, and the likelihood would object. With it, those
trials are floored by the contaminant instead, so the plateau is there even
when both responses are well represented. cogmod_priors() fences off
nuone, sigmazero and sigmaone for this reason.
mu is nuzero and has the mirror-image plateau, but it is the response's
own intercept, so brms already gives it a proper student_t default and
cogmod_priors() leaves it alone. If one option is chosen only rarely, the
accumulator that loses is the one at risk, and it is worth putting the same
prior on both by hand:
priors <- c(cogmod_priors(f, df),
brms::prior(normal(0.7, 1.5), class = "Intercept"),
replace = TRUE)Predictions exclude the outlier component
posterior_predict() describes the race alone by default, as if
poutlier were zero, because the outlier component is a fixed regularizer
rather than a claim about how guesses are distributed. Use with_outliers()
for the fitted mixture - chiefly for brms::pp_check() - and
without_outliers() to go back. log_lik() is always the full mixture.
posterior_epred() is not provided: for a race model the expectation needs
numerical integration per draw and per observation, and users are better off
summarising posterior_predict() draws.
References
Rouder, J. N., Province, J. M., Morey, R. D., Gomez, P., & Heathcote, A. (2015). The lognormal race: A cognitive-process model of choice and latency with desirable psychometric properties. Psychometrika, 80(2), 491-513. doi:10.1007/s11336-013-9396-3
Examples
# Simulate data, with 2% of trials from the outlier process
data <- rcogmod_lnr(1000,
nuzero = 1, nuone = 0.5, sigmazero = 1, sigmaone = 0.8,
ndt = 0.2, poutlier = 0.02
)
head(data)
#> rt response
#> 1 0.5930482 0
#> 2 0.2978002 0
#> 3 0.4627420 0
#> 4 0.2675939 0
#> 5 0.3660357 1
#> 6 0.7338803 1
# Responses faster than ndt keep positive density, unlike the unmixed model
dcogmod_lnr(0.1, ndt = 0.2, response = 0, poutlier = 0.02)
#> [1] 0.03520653
dcogmod_lnr(0.1, ndt = 0.2, response = 0, poutlier = 0)
#> [1] 0
if (FALSE) { # \dontrun{
# Needs cmdstanr and a CmdStan toolchain, which live outside CRAN - see the
# package website to install them. Not run under R CMD check, which executes
# every example in one R session: once brms has fitted a model there (the
# cogmod_inits() and p_outlier() examples do), rstan is live in the process
# and loading an exposed Stan function next to it segfaults on Linux.
lpdf <- cogmod_lnr_lpdf_expose()
lpdf(
Y = 0.5, mu = 0.5, nuone = 0.2, sigmazero = 1.0, sigmaone = 0.8,
ndt = 0.2, poutlier = 0.02, dec = 0
)
} # }