--- title: "Study design, inference and reproducibility" author: "R. Heller" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Study design, inference and reproducibility} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} resource_files: - figures/logo.svg - figures/logo-dark.svg --- ```{r setup, include=FALSE} set.seed(20260512) knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4) library(shieldR) ``` A reproducible experiment needs a declared endpoint, a comparison design, a replication schedule and an explicit multiplicity family. This tutorial shows those decisions using synthetic logistics items and dynamic network challenges. It does not provide calibrated application-specific assumptions. ## Define the experiment before looking at outcomes The unit of analysis is an independently simulated batch. Items within a batch share queues, vehicles, observations and disruption risk history, so they are not independent statistical replicates. Use batch-level outcomes for the functions below. ```{r analysis-plan} master_seed <- 20260512L scenario <- example_scenario("constrained", N = 6L) policies <- c("P0", "P1", "P3") arm_seeds <- setNames(master_seed + seq_along(policies) - 1L, policies) n_rep <- 6L bootstrap_resamples <- 10000L endpoint <- "viable_stage2" comparison_family <- "teaching-policy-comparisons-at-Stage2" data.frame(policy = policies, seed = unname(arm_seeds), n_rep = n_rep) ``` The master seed is 20260512. Different arms receive documented, distinct seeds derived from it. Six replications keep this vignette small; choose the study replication count using a prospective Monte Carlo precision assessment. The bootstrap uses the full reporting default of 10,000 resamples. A smaller bootstrap count may be useful for an interactive preview but must be identified as such and replaced for a final analysis. The primary logistics endpoint is viable delivery, operationalized here by arrival viable at Stage 2 by the administrative horizon. This is a proportion of the original batch, not a Kaplan-Meier viability probability at an arbitrary time. A item still viable elsewhere at the horizon is an endpoint failure. Arrival is recorded before unloading and ends item-condition hazard; transport can continue to Stage 3. Downstream disruptions can still affect the Stage 3 endpoint. ## Run independent policy arms ```{r independent-arms} arms <- setNames(lapply(policies, function(policy) { simulate_shield(scenario, policy = policy, disruption_model = "static", n_rep = n_rep, seed = arm_seeds[[policy]]) }), policies) summary_P0 <- summarize_shield(arms$P0) summary_P0[summary_P0$endpoint %in% c("viable_stage2", "t_stage2_med", "util_L4", "n_pending"), ] ``` Each row in an arm is one replication; each row above describes an endpoint across those replications. The medians and quartiles do not describe the parameter distribution or uncertainty about the model's scientific validity. ```{r endpoint-plot, fig.cap="Neutral teaching run: batch-level Stage 2 arrival-viable proportions by policy.", fig.alt="Empirical distributions of arrival-viable proportions for each policy in the neutral teaching scenario."} plot(stats::ecdf(arms$P0[[endpoint]]), verticals = TRUE, do.points = FALSE, xlim = c(0, 1), ylim = c(0, 1), xlab = "Proportion arriving viable at Stage 2", ylab = "Cumulative proportion of batch replications", main = "") lines(stats::ecdf(arms$P1[[endpoint]]), verticals = TRUE, do.points = FALSE, lty = 2) lines(stats::ecdf(arms$P3[[endpoint]]), verticals = TRUE, do.points = FALSE, lty = 3) legend("bottomright", legend = policies, lty = 1:3, bty = "n") ``` The neutral scenario has zero exposure and item-condition hazards. Differences in arrival outcomes can reflect congestion or the administrative horizon. The plot is a teaching output and establishes no policy advantage in practice. ## Report nonparametric comparisons in full `compare_policies()` applies a two-sided, tie-corrected Mann-Whitney U approximation to independent arms. The test compares distributions; it is not specifically a test of medians. It also returns Cliff's delta and the difference in medians with percentile bootstrap 95% intervals. This is a primary nonparametric framework, with no preliminary normality test. ```{r independent-comparisons} comparisons <- do.call(rbind, lapply(c("P1", "P3"), function(policy) { compare_policies( arms[[policy]][[endpoint]], arms$P0[[endpoint]], label_x = policy, label_y = "P0", family = comparison_family, B = bootstrap_resamples, seed = master_seed ) })) comparisons[c("arm_x", "arm_y", "n_x", "n_y", "U", "p_raw_log10", "cliff_delta", "cliff_lwr", "cliff_upr", "med_diff", "med_diff_lwr", "med_diff_upr", "med_diff_ci_degenerate")] ``` Effects are oriented as `x` minus `y`. Positive values favor `x` for viability; for delay or resource use, whether larger values are desirable depends on the question. Do not infer direction from the policy labels alone. Cliff's delta contrasts the probabilities of a randomly drawn `x` exceeding or falling below a randomly drawn `y`, accounting for ties. Retain sample sizes, median/IQR descriptives, the U statistic, raw and adjusted p-values, effect estimates and intervals in the analysis output. The bootstrap intervals quantify Monte Carlo sampling uncertainty conditional on the model. They do not include parameter uncertainty, structural uncertainty, measurement validity or uncertainty from choosing a model after seeing the result. A bootstrap interval can collapse on this discrete endpoint. Inspect the `med_diff_ci_degenerate` flag and the interval endpoints; a point interval is not evidence of empirical certainty. ### Declare the complete FDR family The family below contains both planned policy contrasts at the same endpoint. Adjust its complete set of p-values together. If a study defines additional endpoints or scenarios in the same family, include them before adjustment. Selecting only significant or interesting contrasts changes the family. ```{r multiplicity} comparisons$p_bh_log10 <- adjust_fdr(comparisons$p_raw_log10) comparisons[c("family", "arm_x", "arm_y", "p_raw_log10", "p_bh_log10")] ``` The helpers retain base-10 log p-values so extreme tails remain distinguishable. `p_raw_log10` is authoritative when a linear p-value underflows; use log probabilities or a properly formatted bound instead of reporting a computed probability as zero. `adjust_fdr()` implements Benjamini-Hochberg adjustment for one supplied family and leaves missing entries missing. It does not infer a hypothesis grid or silently pool separate families. ### More than two arms For a prespecified omnibus comparison, use Kruskal-Wallis and retain its H statistic and degrees of freedom. Any decision to gate pairwise contrasts on that omnibus result belongs to the declared study analysis plan. ```{r omnibus} values <- unlist(lapply(arms, `[[`, endpoint), use.names = FALSE) groups <- rep(names(arms), vapply(arms, nrow, integer(1))) compare_multiple(values, groups, family = "teaching-omnibus-at-Stage2") ``` The omnibus helper reports the test statistic and p-value. Pairwise effect sizes and their intervals come from explicitly chosen contrasts; the function does not select post-hoc comparisons or manufacture a multi-arm effect size. For association questions, the study's primary nonparametric choice is Spearman; specify the variables, independent unit and any bootstrap interval separately. ## Paired disruptions-disabled intervention A paired intervention is a different design. It requires identical parameters, policy, disruption-response family, replication count and seed schedule, with disruption enablement as the only changed setting. Item draws are indexed by item and minute and use a stream separate from policy and disruption draws. ```{r paired-intervention} with_disruptions <- simulate_shield( scenario, policy = "P3", disruption_model = "adaptive", n_rep = n_rep, seed = master_seed, disruptions_enabled = TRUE ) without_disruptions <- simulate_shield( scenario, policy = "P3", disruption_model = "adaptive", n_rep = n_rep, seed = master_seed, disruptions_enabled = FALSE ) compare_intervention(with_disruptions, without_disruptions, B = bootstrap_resamples, seed = master_seed) ``` The contrast is **no disruptions minus disruptions**, estimated as a paired mean difference of batch proportions with a paired percentile bootstrap interval. Pair indices are resampled jointly. The helper checks matching identities and aligns rows by replication identifier. The neutral exposure is zero, so the intervention has no effect here. In a user-specified disruption risk experiment, this is a total modelled intervention effect, including downstream decisions and administrative-horizon effects. It is not identical to `n_disruption_loss` or a count of direct disruption losses. Matched arms must not be passed to the independent-arm Mann-Whitney U helper. ## Preserve missing outcomes and replay the experiment Arrival-time medians are conditional on arrival. If an arm contains an undefined median, `compare_policies()` rejects that nonfinite input. Decide prospectively whether a comparison of defined batch medians is meaningful, report the missing counts by arm, and keep the viability/arrival endpoint alongside it. Replacing a missing time by zero or the horizon changes the estimand. Simulation and bootstrap calls restore the caller's RNG state and kind, while recording their own reproducible configuration. Repeating a run with the same parameters, software and seeds reproduces its batch results: ```{r replay} state_before <- .Random.seed replayed <- simulate_shield(scenario, policy = "P0", disruption_model = "static", n_rep = n_rep, seed = arm_seeds[["P0"]]) stopifnot(identical(arms$P0, replayed), identical(state_before, .Random.seed)) ``` ### Replay one batch or distribute a fixed schedule Use `replication_seeds()` to construct the complete cell schedule once, then send individual seeds to `simulate_replication()` from your analysis scheduler. The latter takes a replication seed directly; it does not sample a new cell schedule or attach study identifiers. Checkpoint naming and workload allocation remain the analysis project's responsibility. ```{r individual-replication} schedule <- replication_seeds(n_rep = n_rep, seed = arm_seeds[["P0"]]) stopifnot(identical(schedule, arms$P0$replicate_seed)) first_batch <- simulate_replication( scenario, policy = "P0", disruption_model = "static", replication_seed = schedule[1] ) stopifnot(all(vapply(names(first_batch), function(column) { identical(first_batch[[column]], arms$P0[[column]][1]) }, logical(1)))) ``` Generate the declared full schedule before splitting into chunks. Restarting a master seed independently for each chunk repeats the beginning of the schedule and creates duplicate replications. For an disruptions-disabled pair, use the same schedule in both configurations. Preserve the total replication count and the mapping from each seed to its replication identifier in durable checkpoints. For analysis pipelines that already manage their own data frames, `mann_whitney_u()` exposes the tested U calculation and log tail probability, `bootstrap_statistic()` exposes named independent or paired interval estimators, and `effect_magnitude()` provides descriptive Cliff's delta labels. These primitives do not establish independence or pairing for the caller; prefer the higher-level comparison functions when using complete run objects. ### Save the complete request Save the request, full result object, analysis settings and session information. An RDS retains attributes required for pairing, whereas CSV retains only table columns. ```{r reproducibility-bundle} request <- list(parameters = scenario, policy = "P0", disruption_model = "static", n_rep = n_rep, seed = arm_seeds[["P0"]]) bundle <- list(request = request, run = arms$P0, analysis = list(endpoint = endpoint, family = comparison_family, bootstrap_resamples = bootstrap_resamples, master_seed = master_seed), session_info = sessionInfo()) bundle_file <- tempfile("shieldR-study-", fileext = ".rds") saveRDS(bundle, bundle_file) restored <- readRDS(bundle_file) stopifnot(identical(do.call(simulate_shield, restored$request), restored$run)) ``` For a real analysis, use a named output directory and retain an immutable copy of the exact source archive or package commit, the scenario input hash, seed schedule, replication counts and an environment lockfile. A version number alone cannot distinguish two uncommitted source states with the same version. Study scheduling, frozen input/output manifests and publication exports remain in the analysis project. Method changes require explicit validation before adoption by a study. ```{r session-information} sessionInfo() ```