--- title: "Getting started with sandtablR" author: "Raban Heller, Gerhard Achatz" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with sandtablR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} resource_files: - figures/logo.svg - figures/logo-dark.svg --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4, dpi = 96 ) has <- function(...) { all(vapply(c(...), requireNamespace, logical(1), quietly = TRUE)) } tidymodels_ready <- has( "parsnip", "recipes", "workflows", "rsample", "tune", "yardstick", "dials", "hardhat", "rpart" ) explain_ready <- tidymodels_ready && has("DALEX", "DALEXtra") ``` ```{r setup} library(sandtablR) ``` `sandtablR` is a framework for mass-casualty incident tabletop exercises: generate a casualty cohort, run it across a board, capture the decisions that were made, then model and explain them. The steps below are the whole workflow. Everything the Shiny workbench does, it does by calling these functions. ## 1. A cohort A cohort is a validated tibble subclass. Every casualty carries a card: demographics, life points, whether they are ventilated, the injury region, and the true sighting category and therapy concept. ```{r cohort} co <- sdt_synthetic_cohort(n = 40, seed = 1) co ``` Marginals come from `sdt_reference()`, which separates two things that are easy to conflate: values attributed to the published evaluation, and modelling choices `sandtablR` made where the source does not pin one. ```{r reference} sdt_reference("published")$triage_mix ``` Validation is vectorised over columns rather than looping over rows, so a bad import reports everything wrong with it at once rather than one problem per attempt: ```{r validate, error = TRUE} bad <- co bad$triage <- as.character(bad$triage) bad$triage[c(2, 5)] <- "purple" bad$life_points[3] <- 99 sdt_cohort(bad) ``` ## 2. A scenario A scenario is a cohort plus the board's resource capacities. ```{r scenario} sc <- sdt_scenario(co, resources = list(or = 3, icu_imc = 6, ward = 20), name = "concert") sc summary(sc) ``` ```{r triage-mix, fig.alt = "Bar chart of casualties per sighting category", fig.cap = "Bar chart of casualties per sighting category. Synthetic demonstration."} sdt_plot_triage_mix(sc) ``` ## 3. Simulate patient flow `sdt_simulate()` maps the scenario onto a discrete-event model and routes each casualty by sighting category and therapy concept. ```{r simulate, eval = has("simmer")} run <- sdt_simulate(sc, replications = 5, seed = 1, verbose = FALSE) run ``` The engine's real output is the event log: one row per casualty per board area, with when it entered and left. ```{r events, eval = has("simmer")} head(run$events) ``` Three dispositions are terminal by design, and that is what reproduces the published end-of-game distribution: critical care is where a casualty stays, a ward bed is the end of the line, and a casualty who is worked up but not taken to theatre is still being held in the emergency department when the game is called. The quickest way to see what the engine did is to look at the board. Every casualty is a point in the area it had reached at that moment, laid out as the nine areas of the published board. Change `at` and you watch the stream move across it. ```{r board, eval = has("simmer"), fig.width = 8, fig.height = 3.4, fig.alt = "The nine board areas with casualties drawn as coloured points", fig.cap = "The nine board areas with casualties drawn as coloured points. Synthetic demonstration."} sdt_plot_board(run, at = 150) ``` ```{r end-state, eval = has("simmer")} summary(run, reference = TRUE) ``` ```{r end-plot, eval = has("simmer"), fig.alt = "Simulated end-of-game location against the published reference", fig.cap = "Simulated end-of-game location against the published reference. Synthetic demonstration."} sdt_plot_end_state(run) ``` That calibration is checkable rather than asserted: `sdt_reference_scenario()` builds the board the engine was tuned on, and the package's own test suite re-runs the comparison. ## 4. Decision paths Because the event log records every area entry, each casualty has a full ordered pathway. ```{r paths, eval = has("simmer")} head(sdt_decision_paths(run), 8) ``` `sdt_decision_consistency()` asks how repeatably the same casualty was handled across replications, as a distance between its pathways rather than agreement on a single label. ```{r consistency, eval = has("simmer")} consistency <- sdt_decision_consistency(run) head(consistency[order(consistency$consistency), ]) ``` ```{r sequences, eval = has("simmer"), fig.alt = "Share of casualties in each board area by position in the pathway", fig.cap = "Share of casualties in each board area by position in the pathway. Synthetic demonstration."} sdt_plot_sequences(run, type = "distribution") ``` With **TraMineR** and **WeightedCluster** installed, `sdt_seq_cluster()` groups pathways into a few characteristic routes through the board. ## 5. Model the decisions The card's true category follows from life points by construction, so every engine scores near 1 on it and the comparison says nothing. What is worth modelling is what a *participant* called. `sdt_add_decisions()` simulates that using the published per-category hit rates. ```{r decisions} labelled <- sdt_add_decisions(co, seed = 2) sdt_consistency_score(labelled$triage, labelled$called_triage) ``` ```{r fit, eval = tidymodels_ready} fit <- sdt_fit(labelled, target = "called_triage", engine = "tree", tune = FALSE, folds = 3, seed = 1, verbose = FALSE) fit sdt_metrics(fit) ``` `sdt_workflowset()` screens every installed engine onto one leaderboard, with glass-box and black-box models side by side. See `vignette("model-bench", package = "sandtablR")`. ## 6. Explain Explanations are model-agnostic, so the same views work on a decision tree and on a random forest. ```{r explain, eval = explain_ready, fig.alt = "Permutation variable importance", fig.cap = "Permutation variable importance. Synthetic demonstration."} sdt_importance(fit, n_permutations = 5) ``` ## The workbench `sdt_run_app()` launches an interactive workbench over all of the above: build a scenario, run the board, annotate decisions, train and compare models, explain them, and analyse decision paths. It also renders the session to a self-contained HTML report. ## Provenance The domain model (sighting categories, ETC/DCS/TASC therapy concepts, board areas, patient-card fields) follows the TDSC tabletop exercise as described in the open-access evaluation by Achatz et al. (2023), *Eur J Trauma Emerg Surg* 49(2):607--617, [doi:10.1007/s00068-022-02217-1](https://doi.org/10.1007/s00068-022-02217-1). `sandtablR` is a generic re-implementation built from published specifications, and is not affiliated with the TDSC course. ## Save and reproduce a run ```{r save-results, eval=has("simmer")} path <- tempfile(fileext = ".rds") saveRDS(list(scenario = sc, run = run, seed = 1, session = sessionInfo()), path) restored <- readRDS(path) stopifnot(identical(run$events, restored$run$events)) ``` Retain the software revision and scenario alongside every seed. A reproducible simulation is not evidence that its assumptions describe a real incident. See [the model bench](model-bench.html) for a separate synthetic prediction example. ## Software environment ```{r session-information} sessionInfo() ```