Simulate new groups from a fitted behaviour model
Source:R/behaviour-model.R
sdt_simulate_groups.RdDraws teams that were never in the data. For each simulated group and each
modelled outcome the team's own random effect is redrawn from
N(0, sd_group), a session effect is redrawn from N(0, sd_block), and the
outcome is then drawn per card from the fitted card effects:
Usage
sdt_simulate_groups(
model,
n_groups = 20L,
cards = NULL,
seed = NULL,
new_teams = TRUE,
nsim = 1L,
composition = NULL
)Arguments
- model
An
sdt_behaviour_modelfromsdt_fit_behaviour().- n_groups
Number of simulated groups per replicate.
- cards
Cards to deal to each simulated group.
NULLuses the fitted frame's cards; anything passed must be a subset of them.- seed
Optional integer seed, applied with
withr::local_seed().- new_teams
If
TRUE, redraw the team and session effects. IfFALSE, reuse the fitted ones.- nsim
Number of replicates. Replicates are stacked and carry a
.replicatecolumn;sim_groupis unique across the whole result, so grouping by it never merges two different simulated teams.- composition
Optional block-level covariate values to condition on: a data frame with one row per course (a slice of
sdt_course_composition()will do), or a named list or vector for a single course.NULLsimulates marginally, exactly as a model fitted without covariates does. Requires a model fitted withblock_covariates.
Value
An S3 object of class sdt_simulated_groups, a
tibble::tibble() with .replicate, sim_group, card and one logical
column per modelled outcome. Carries the fitted model, the call, and a
draws attribute holding the random effects used for each simulated
group.
Details
This is deliberately not stats::simulate() on the fitted model with the
fitted grouping structure, which would put the training teams back on the
table. Every simulated group here is a new team playing in a new session.
Which random effects are redrawn
With new_teams = TRUE (the default) both the group and the block effect
are drawn fresh and independently for every simulated group, so a simulated
group is "a new team in a new session" and its total random effect has
variance sd_group^2 + sd_block^2 – the same marginal spread a real,
previously unseen group has.
With new_teams = FALSE the fitted conditional modes are reused: simulated
groups are the observed teams, with their own session effects, playing the
same cards again. On a model fitted with block covariates their own
course's composition goes back into the linear predictor as well, because
the fitted session modes are residuals with that part already removed.
That is a posterior-predictive check of the fit, not a generator of new
teams. It takes the first n_groups fitted teams in frame
order rather than a random sample, so it is reproducible without a seed;
pass n_groups = model$meta$n_groups to use them all, which is what a
like-for-like check against the observed frame wants. The draws attribute
records which observed team each simulated one came from.
Simulating a course of a given composition
composition takes one row – or several – of block-level covariate
values on their natural scale, so a caller can ask for "a course whose
participants look like this". Each row is a course: n_groups teams are
drawn inside each of them, and they share one drawn session effect,
because groups in one course played in one session. The result carries a
.composition column naming the row each simulated team belongs to.
Conditioning changes two things. The covariates' fixed contribution is
added to the linear predictor, and the session effect is drawn from the
residual block SD – what is left after the covariates – rather than
from the total. With composition = NULL neither happens: the covariates
sit at their fitted mean and the session effect is drawn from the total
block SD, which is the marginal spread of an unseen course and exactly
what a model fitted without covariates produces from the same seed.
A composition value outside the range the fitted courses covered is warned about rather than refused. It is extrapolation from a between-course slope estimated on as many points as there were courses, and it does not travel.
composition cannot be combined with new_teams = FALSE: reusing the
fitted teams reuses the courses they actually played in, so asking for a
hypothetical composition at the same time asks for two contradictory
things.
Each row is labelled by the block key when the caller passes one, so a
slice of sdt_course_composition() keeps its file name and the reader can
see which real course a hypothetical one was copied from. That label is a
course identifier, and sdt_export_simulation() will refuse to write it
into a pseudonymised bundle – rename the key column to something neutral
before simulating if the result is going to be exported.
What is not reproduced
Each outcome was fitted separately, so a simulated team's tendencies are drawn independently across outcomes. Real teams that image more also operate more; that correlation is absent here. Card-to-card dependence within a casualty (an eFAST leading to a CT) is likewise absent.
Every simulated group is also dealt every card, while an observed group can
have missing outcome values – a casualty its team never triaged. Those
groups differ in which cards contribute to their rate, and that is spread
the simulation has no way to produce. On the validation corpus red is
missing in 105 of 3410 cells and its dispersion ratio reads 1.20 over all
110 groups but 0.97 over the 64 groups with a complete card set.
model$meta$sds$n_missing counts those cells, so a fidelity comparison can
restrict itself to the groups that have none.
Examples
set.seed(1)
d <- expand.grid(
card = sprintf("c%02d", seq_len(8)),
group_id = sprintf("g%02d", seq_len(12)),
stringsAsFactors = FALSE
)
d$session_id <- ifelse(d$group_id < "g07", "s1", "s2")
b <- stats::rnorm(12, sd = 1.2)
p <- stats::plogis(-0.5 + b[match(d$group_id, unique(d$group_id))])
d$did_efast <- stats::runif(nrow(d)) < p
if (requireNamespace("lme4", quietly = TRUE)) {
fit <- sdt_fit_behaviour(d, outcomes = "efast", verbose = FALSE)
sdt_simulate_groups(fit, n_groups = 5, seed = 1)
}
#> <sdt_simulated_groups> 5 new teams x 8 cards in 1 replicate
#> 1 outcome: efast
#> # A tibble: 40 × 4
#> .replicate sim_group card efast
#> * <int> <chr> <chr> <lgl>
#> 1 1 g0001 c01 FALSE
#> 2 1 g0001 c02 FALSE
#> 3 1 g0001 c03 TRUE
#> 4 1 g0001 c04 FALSE
#> 5 1 g0001 c05 FALSE
#> 6 1 g0001 c06 FALSE
#> 7 1 g0001 c07 FALSE
#> 8 1 g0001 c08 TRUE
#> 9 1 g0002 c01 TRUE
#> 10 1 g0002 c02 FALSE
#> # ℹ 30 more rows