Skip to contents

Draws 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_model from sdt_fit_behaviour().

n_groups

Number of simulated groups per replicate.

cards

Cards to deal to each simulated group. NULL uses 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. If FALSE, reuse the fitted ones.

nsim

Number of replicates. Replicates are stacked and carry a .replicate column; sim_group is 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. NULL simulates marginally, exactly as a model fitted without covariates does. Requires a model fitted with block_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

p = plogis(card effect + team effect + session effect)

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