The generate half of the engine: a model plus population values on
the link scale become a data set and the truth that produced it, so
that recover() and recover_subjects() can score a fit of that
data. Subject values are drawn on the link scale around the
population values, converted to the natural scale through
inverse_link() (a softmax term excepted, see the details), and
handed to the model's own r<model>() generator,
or to the function you pass as generator, which always takes
precedence over the built-in one.
Usage
simulate_recovery(
model,
pars,
n_subjects,
n_trials,
sds = NULL,
cors = NULL,
covariates = NULL,
tasks = NULL,
task_col = "task",
coding = c("cell", "contrast"),
contrasts = NULL,
subject_pars = NULL,
formula = NULL,
trial_design = NULL,
generator = NULL,
seed = NULL
)Arguments
- model
A
bmmodel, built with the column names the fit will use, so the generated columns match.- pars
Named numeric: population values on the link scale under bmm's parameter names, one per estimated parameter. A fixed parameter may be given too, in which case the generator uses that value. May also be a function with no arguments returning such a vector, evaluated under
seed, for random hyperparameters.- n_subjects, n_trials
Subjects, and trials per subject and per row of the generator's layout (for
sdt_yn, per stimulus class). With atrial_design,n_trialsis the number of design rows per subject, which for a count model are rows of data, not trials (see the section "Trial design").- sds
Named numeric: between-subject standard deviations on the link scale. A parameter not named does not vary.
NULLmeans no parameter varies. May be a function, aspars.- cors
A correlation matrix between subject values on the link scale, with the parameter and covariate names as dimnames; terms it does not name are uncorrelated.
NULLmeans none are correlated. May be a function, aspars.cors_from_factors()builds one from factor loadings.- covariates
Observed, error-free person variables drawn jointly with the parameters: a named list of
c(mean = , sd = ). Each becomes a subject-constant data column afterid. A name must be syntactic, contain no_, and differ from the model's parameters and columns.- tasks
NULL, or a character vector of at least two task levels (letters and digits) for a design in which every subject does every task. Each free parameter then has one value per task, under the term<parameter>_<task_col><level>(kappa_task1), brms's coefficient name for0 + task. See the details.- task_col
The name of the task column in
data,"task"by default. Used only withtasks.- coding
What the task column means to a fit.
"cell", the default, leaves it as it was: a fit of0 + taskestimates one value per task and the truth is those values."contrast"attachescontraststo the task column, so a fit of1 + taskestimates an intercept andlength(tasks) - 1contrasts, andtruthholds those instead:<parameter>for the intercept and<parameter>_<task_col><j>for the contrasts, with the SD and correlation truths transformed with them.What is drawn does not change. Subject values are drawn around the cell means exactly as under
"cell", from the same RNG stream, sosds,corsandsubject_parskeep their meaning and a simulation is comparable across codings. Only the design the fit sees, and the truth it is scored against, differ.- contrasts
The contrast matrix for
coding = "contrast":length(tasks)rows and one column fewer, or a function(n)returning one. stats::contr.treatment by default, so the intercept is the first task and each contrast a difference from it. For a study in which bmm's default prior should mean the same for every task, use an orthonormal coding such asbayestestR::contr.equalprior, whose intercept is the grand mean and whose contrasts are uncorrelated with it. Ignored undercoding = "cell".- subject_pars
A data frame
id,term,true_value(link scale) of subject values to use instead of drawing them, so that replications can share the same simulated people. Withcovariatesit must give their values too.- formula
NULL, or thebmmformulathe model is fitted with, for a built-in generator that reads it: only the custom version ofm3, whose activation formulas exist nowhere else (see the section "The m3 model"). A formula for any other model, or with agenerator, is an error: a generator you write closes over its own.recovery_grid(),recovery_component()andsbc()pass on the formula they fit with.- trial_design
NULL, or the per-trial variables of the design (set sizes, non-target locations, option counts): what a generator needs per trial and the model reads from the data but does not hold. For a count model, the number of trials (and stimulus) of each row instead; see the section "Trial design". One ofa data frame of
n_trialsrows, one trial list used for every subject (and every task);a data frame with an
idcolumn holding the subjects1ton_subjects,n_trialsrows each, one trial list per subject;a function
(n_trials)returning a data frame ofn_trialsrows, called once per subject and task underseed, for designs drawn at random, such as non-target locations.
Each generator call receives its rows as
trial_design, and the same rows are bound intodataafterid, the covariates and the task column. See the section "Trial design".- generator
A function
(pars, n_trials, model)returning one subject's rows as a data frame with the model's column names;parsis a named list on the natural scale, fixed parameters included, except a parameter whose link issoftmax, which arrives on the link scale (see the details). With atrial_designit is called as(pars, n_trials, model, trial_design)and must have that fourth argument or....NULLuses the adapter bmmtools ships for the model.- seed
A seed applied with
withr::with_seed()around the draws and the generator;NULLleaves the random number generator alone.
Value
A list of class bmmtools_simulation with data (a tibble,
id first, then any covariates, then the task column), truth (a
list of tibbles on the link scale: population with term and
true_value; subjects with id, term and true_value; sd with
term and true_value; cor with term, var1, var2 and
true_value;
covariates with id, term and true_value), the realised
pars, sds, cors (the full matrix over varying parameters then
covariates, NULL with fewer than two) and covariates,
n_subjects, n_trials, seed (NA when none), model,
generator, tasks and task_col (both NULL without tasks),
trial_design (NULL, the data frame, or a function's text),
trial_design_columns (the columns it contributed to data) and
formula (NULL, or the formula that reached the generator, each
element deparsed to text).
Details
Adapters exist for sdt_yn, sdt_mafc, sdt_ranking, ezdm (both
versions), ddm, cswald (both versions), mixture2p, sdm,
mixture3p, imm (full, bsc and abc) and m3 (ss, cs and
custom). Every other model takes a generator, among them bmm's
sdt_rating and sdt_cdp, whose thresholds bmm builds internally
from criterion and the spacing parameters. The mixture3p and imm
adapters need a trial_design, and so does sdt_ranking when its m
names a column; see the section "Trial design". The custom m3 needs
formula; see the section "The m3 model".
The four-parameter ezdm has RT summaries per boundary, so its model
names two columns in each of mean_rt and var_rt, upper boundary
first: mean_rt = c(upper, lower), var_rt = c(upper, lower). bmm's
constructor accepts one name each; the adapter refuses such a model
before any data are drawn. A boundary reached fewer than twice in a data
row leaves that boundary's mean and variance NA; bmm 1.4.1 keeps such
rows, fits that boundary through the response counts only, and warns
how many there are. For sdt_ranking with dist = "normal", pars
takes sdratio on bmm's scale, the log of the ratio of signal to noise
SDs (0 for equal variances).
The truth for the subjects and for the SDs lists only the parameters
that vary, because a parameter that does not vary has nothing
person-level to recover.
Alternative parameterizations. Where bmm offers two parameters for
one quantity and estimates only one of them (from bmm 1.4.1,
criterion and bias of sdt_yn and sdt_rating), pars names the
one to simulate and recover, and naming both is an error. The other
appears nowhere: not in truth, and not in sds or cors, which may
not name it. Without either, the error asks for bmm's default, the
first of the group. Fit with recovery_formula(sim$model, pars = sim$pars), which writes the same member.
Correlated draws. Subject values and covariates are one
multivariate normal draw, Z %*% chol(cors), over the varying
parameters in pars order and then the covariates, scaled by the SDs
afterwards. Without correlations this gives exactly the values an
uncorrelated draw gave in earlier versions, so seeded simulations
stay reproducible; adding a covariate never changes the parameter
values. A correlation pair is named <a>__<b>, the two names sorted
in the C locale.
Tasks. With tasks, pars and sds may name a parameter, which
gives every task that value, or a full term such as kappa_task2, which
overrides it for that task; the realised pars and sds are stored
with full terms. cors, subject_pars and the truth tables use full
terms only, so a correlation between tasks is, for example,
kappa_task1__kappa_task2. The generator is called once per subject and
task with that task's values under the bare parameter names, and
n_trials is per task. The task values are cell means: fit them with
recovery_formula(model, task_col = "task"). Fixed parameters are the
same in every task. Adding tasks changes the random numbers drawn
compared with a simulation without them; tasks = NULL gives exactly
the simulation of earlier versions.
Trial design
A generator learns one subject's parameters and a number of trials. A
model whose responses depend on what was shown on each trial —
mixture3p and imm need a set size and non-target locations — gets
those through trial_design. The engine hands each generator call its
rows and binds the same rows into data, so the fit sees exactly the
trials the responses were drawn from; a generator returns the response
columns only, and returning a design column is an error. A design
column must differ from the model's response columns, the covariates
and the task column.
mixture3p and imm. Their adapters read, from the columns the
model names: the set size, when set_size is a column name (a number
is every trial's set size); the non-target locations nt_features,
relative to the target, in radians; and for imm full and bsc the
distances nt_distances. A trial of set size k has its lures in the
first k - 1 non-target columns, which must not be NA; columns
beyond are ignored and are NA by bmm's convention. Each trial is one
call of bmm::rmixture3p() or bmm::rimm(), with the weights bmm's
likelihood gives that trial.
The softmax link. mixture3p's thetat and thetant are log
weights against a guessing weight of 0: on a trial with lures,
(p_mem, p_nt, p_guess) is the softmax of (thetat, thetant, 0), the
lures sharing p_nt equally, and on a set-size-1 trial there is no
lure weight. Since no inverse of one term gives its natural value, a
parameter whose link is softmax reaches the generator on the link
scale, and recover() scores it there (its scale column says so).
m3 with its numbers of options as columns. When num_options
names columns, those columns are a trial design and each of its rows
is one trial: see the section "The m3 model".
Count models. One row of sdt_yn, sdt_mafc, sdt_ranking,
ezdm or m3 with numbers of options holds many trials. Without a
design, every row holds n_trials of them. A design is optional for
these models; with one, each design row is one row of data and
carries its own number of trials, so n_trials counts the design rows.
The trial count is read from the model's n_trials column, and for
m3 and sdt_ranking, which have none, from a design column
n_trials. sdt_ranking whose m names a column reads each row's set
size from that column, so for it the design is required: a whole number
from 2 to the number of rank columns, with the ranks beyond it 0.
sdt_yn also reads its stimulus column
(0 or 1), so that 100 signal and 50 noise trials are
data.frame(stimulus = c(1, 0), n_trials = c(100, 50)) under the
model's column names. A count must be a whole number of at least 1, and
of at least 3 for ezdm, whose generator refuses fewer; a design that
breaks this is refused before anything is drawn.
Without trial_design nothing changes: the generator is called with
three arguments, as in bmmtools 0.2.0, and a seeded simulation gives
the same data, and so the same fit_cached() key. A data frame draws
no random numbers; a function draws its own, interleaved with the
generator's, once per subject and task. What a function returns can
only be checked once it has been called, so an error in it (wrong row
count, a missing column) stops the simulation after some random numbers
have been drawn; under seed that changes nothing, and without one the
random number stream has moved.
The m3 model
m3's response is a count per category, in the columns resp_cats
names. With numbers of options on the model (num_options = c(1, 4, 5)), a subject has one row of n_trials trials per task, drawn
with one bmm::rm3() call, as sdt_mafc has one row of counts. With
num_options naming columns, the options differ between trials, so
those columns come from trial_design and each row is one trial:
n_trials rows per subject, each counting one response in its
category. The probabilities are bmm's: each category's activation,
exponentiated under choice_rule = "softmax", times its number of
options, normalised. The parameters' links are elementwise, and which
links the model has depends on the choice rule, so take pars on the
link scale of the model as built: bmm::parameters() lists them for
ss and cs, and the model's links for custom.
The versions ss and cs take their activation formulas from bmm. The
custom version knows its activations only from the formula it is
fitted with, so it needs formula, holding one activation per category
(corr ~ b + a + c), and a model built with a link for each parameter
they use (m3(..., links = list(c = "log", a = "log"))); without links
bmm knows no parameter to give a value to until it fits. Every
parameter with a link must appear in an activation. Extra parameter
formulas in formula (c ~ 1 + (1 | id)) are ignored here, and a
grid records only the activations.
Examples
if (FALSE) { # \dontrun{
sim <- simulate_recovery(
bmm::mixture2p(resp_error = "y"),
pars = c(kappa = log(8), thetat = qlogis(0.75)),
n_subjects = 30, n_trials = 60,
sds = c(kappa = 0.3, thetat = 0.5), seed = 1
)
fit <- bmm::bmm(recovery_formula(sim$model), sim$data, sim$model)
recover(fit, sim$truth$population)
recover_subjects(fit, sim$truth$subjects)
# two tasks, kappa lower in the second, correlated .6 across tasks
cors <- diag(2)
dimnames(cors) <- rep(list(c("kappa_task1", "kappa_task2")), 2)
cors[1, 2] <- cors[2, 1] <- 0.6
two_tasks <- simulate_recovery(
bmm::mixture2p(resp_error = "y"),
pars = c(kappa = log(8), kappa_task2 = log(5), thetat = qlogis(0.75)),
n_subjects = 30, n_trials = 60,
sds = c(kappa_task1 = 0.3, kappa_task2 = 0.3), cors = cors,
tasks = c("1", "2"), seed = 1
)
formula <- recovery_formula(
two_tasks$model,
re_cor = "within", task_col = "task"
)
# a per-trial design: a condition the generator reads on every trial
shifted <- function(pars, n_trials, model, trial_design) {
mu <- ifelse(trial_design$cond == "a", 0, 0.5)
data.frame(y = vapply(mu, function(m) {
bmm::rmixture2p(1, mu = m, kappa = pars$kappa, p_mem = pars$thetat)
}, numeric(1)))
}
sim <- simulate_recovery(
bmm::mixture2p(resp_error = "y"),
pars = c(kappa = log(8), thetat = qlogis(0.75)),
n_subjects = 10, n_trials = 40,
trial_design = function(n_trials) {
data.frame(cond = sample(c("a", "b"), n_trials, replace = TRUE))
},
generator = shifted, seed = 1
)
# mixture3p: set size 2 to 4, non-target locations drawn per trial
lures <- function(n_trials) {
ss <- sample(2:4, n_trials, replace = TRUE)
nt <- matrix(runif(3 * n_trials, -pi, pi), n_trials)
nt[col(nt) >= ss] <- NA
data.frame(ss = ss, nt1 = nt[, 1], nt2 = nt[, 2], nt3 = nt[, 3])
}
sim <- simulate_recovery(
bmm::mixture3p(
resp_error = "y", nt_features = paste0("nt", 1:3), set_size = "ss"
),
pars = c(kappa = log(8), thetat = 1.5, thetant = 0),
n_subjects = 10, n_trials = 60, trial_design = lures, seed = 1
)
# a custom m3: the activation formulas reach the adapter through formula
model <- bmm::m3(
resp_cats = c("corr", "other", "npl"), num_options = c(1, 4, 5),
choice_rule = "simple", version = "custom",
links = list(c = "log", a = "log")
)
formula <- bmm::bmf(
corr ~ b + a + c, other ~ b + a, npl ~ b,
c ~ 1 + (1 | id), a ~ 1 + (1 | id)
)
sim <- simulate_recovery(
model,
pars = c(c = log(3), a = log(0.5)),
n_subjects = 30, n_trials = 100, sds = c(c = 0.3),
formula = formula, seed = 1
)
} # }
