Scaffold and run a reproducible Monte Carlo simulation study in R — a declared assumption regime, a parameterized DGP, an estimator grid, a seeded replication loop, and a summary of bias, RMSE, empirical SE, coverage, size/power with Monte Carlo standard errors. Use when the user says "run a Monte Carlo simulation", "simulation study", "check the bias/coverage of an estimator", "compare estimators in simulation", "size and power simulation", "Monte Carlo experiment", or wants to demonstrate an estimator's finite-sample properties. Produces a numbered R script in `scripts/R/` and saves per-replication raw results + a summary table to `scripts/R/_outputs/`.
/simulation-study — Monte Carlo Simulation StudyDesign and run a Monte Carlo experiment that characterizes an estimator's finite-sample behavior, then review it for the bugs that quietly invalidate simulation evidence.
Input: $ARGUMENTS — a description of the estimator(s) and DGP to study (e.g., "compare 2SLS vs LIML under weak instruments with heteroskedasticity"), or a pointer to an existing script/paper whose simulation you want to reproduce or extend.
.claude/rules/simulation-conventions.md — the simulation contract (DGP, truth, estimand, MCSE, assumption regime) is non-negotiable.simulation-conventions.md §2)..claude/rules/r-code-conventions.md for general R standards (header, library() at top, relative paths, numerical discipline).scripts/R/ with a numbered, descriptive name (e.g., scripts/R/sim_2sls_vs_liml.R).scripts/R/_outputs/.saveRDS() the per-replication raw results, not just the summary — re-aggregation and the review pass need them.sim-reviewer agent on the generated script before presenting results, then address Critical/High findings.Before writing any code, produce a Pre-Flight Report showing you have pinned down the experiment. This prevents the most common failure mode — a beautiful results table built on a mismatched estimand or a coverage-against-the-estimate bug.
## Pre-Flight Report — Simulation Design
**Research question:** [what finite-sample property is being demonstrated]
**Target estimand:** [ATT / ATE / coefficient θ — and how its TRUE value is computed from the DGP params]
**Maintained assumptions:** [the FULL list the estimator(s) under study require — A1 … An, every one]
**Regime:** [IN-ASSUMPTION — all hold | OUT-OF-ASSUMPTION — relaxes A[k] only, severity grid {…}, targeting pseudo-estimand …]
**Verification:** [per assumption, the checkable property of the DGP that establishes it — by construction or by an assertion]
**DGP:** [structure + the parameters that define it; what is held fixed vs. varied]
**Estimator grid:** [list each estimator + which estimand it targets + how it returns est/se/CI]
**Design grid:** [sample sizes, parameter values, scenarios to sweep]
**Replications R:** [value] → implied MCSE on coverage ≈ sqrt(0.95·0.05/R) = [value]
**Metrics:** bias, empirical SE, RMSE, coverage, size/power — each with MCSE
**Conventions read:** simulation-conventions.md, r-code-conventions.mdIf the estimand or its true value is ambiguous, stop and ask before writing code.
If an assumption cannot be verified — you cannot name the property of the DGP that establishes it — the run is not IN-ASSUMPTION, and per the firewall no within-assumption claim may rest on it. Say so in the Pre-Flight Report rather than letting the header assert what was never checked.
Write one parameterized function that returns a dataset. Compute and return (or store) the true target value from the parameters.
generate_data <- function(n, params) {
# ... generate covariates, treatment, outcome from params ...
list(data = df, truth = compute_truth(params)) # truth from params, never from an estimate
}The header's Verified lines are earned here: every assumption the regime block claims holds by a check gets that check written into the script (a large-draw assertion, a condition number, a stopifnot() on the parameter bounds), run once at setup. An assumption whose verification exists only in the comment is asserted, not verified.
Each estimator is a function data -> list(est, se, ci_lo, ci_hi, converged). State the estimand each one targets; an estimator scored against a mismatched truth is a bug, not a finding.
set.seed(YYYYMMDD) once. For parallel reps use RNGkind("L'Ecuyer-CMRG") and furrr::furrr_options(seed = TRUE).est, se, ci_lo, ci_hi, converged.R × (#estimators) rows. Track non-convergence; never silently drop.Per estimator × scenario, against truth:
mean(est) - truth (+ MCSE = sd(est)/sqrt(R))sd(est); RMSE = sqrt(mean((est - truth)^2))mean(ci_lo <= truth & truth <= ci_hi) (+ MCSE = sqrt(p(1-p)/R))Build a tidy summary table; report MCSE next to every headline metric.
Use ggplot2 with the project theme: bias / coverage vs. sample size (or scenario), with reference lines (0 bias, nominal coverage). Transparent background, explicit dimensions (per r-code-conventions.md §4).
saveRDS() the raw per-rep tibble and the summary table to scripts/R/_outputs/; also write the summary as .csv/.tex.
Run the review:
Delegate to the sim-reviewer agent:
"Review the simulation script at scripts/R/[name].R"Address Critical/High findings (coverage-vs-truth, estimand mismatch, missing MCSE, dropped reps, an unstated or unverified regime) before presenting.
Apply the firewall to the presentation itself. Every claim you are about to make must cite a run whose regime can bear it — consistency, valid analytic standard errors, nominal coverage, and shipping a default require an IN-ASSUMPTION run and nothing else (simulation-conventions.md §2). Carry the regime in every caption — and per row wherever a severity grid mixes the two.
# ============================================================
# [Title] — Monte Carlo simulation
# Author: [project context]
# Purpose: [property being demonstrated]
# Estimand: [target + how truth is computed]
# Maintained assumptions: [A1 ... An — the FULL list the estimator requires]
# Regime: [IN-ASSUMPTION | OUT-OF-ASSUMPTION: relaxes A[k] only, severity ...,
# targeting pseudo-estimand ...]
# Verified: [per assumption, the property that was actually checked, not asserted]
# Outputs: scripts/R/_outputs/[name]_raw.rds, [name]_summary.{rds,csv}
# ============================================================
# 0. Setup ----
library(tidyverse)
library(furrr) # parallel reps (optional)
plan(multisession) # enable parallel workers; omit this line to run sequentially
RNGkind("L'Ecuyer-CMRG")
set.seed(20260531) # once, YYYYMMDD (simulation-conventions.md §3)
R <- 2000L # MCSE on coverage near .95 ≈ 0.005
dir.create("scripts/R/_outputs", recursive = TRUE, showWarnings = FALSE)
# 1. DGP ----
generate_data <- function(n, params) { ... } # returns list(data, truth)
# 2. Estimators ----
estimators <- list(tsls = est_tsls, liml = est_liml) # each -> est, se, ci, converged
# 3. Run one replication ----
run_one_rep <- function(rep_id, n, params) { ... } # -> tibble rows (one per estimator)
# 4. Replicate ----
raw <- future_map_dfr(seq_len(R), run_one_rep, n = n, params = params,
.options = furrr_options(seed = TRUE))
# 5. Summarize (vs truth, with MCSE) ----
# Group by EVERY design-grid dimension you sweep (estimator, n, scenario, ...) so
# each group has a single true value. Use per-row `truth` — never `truth[1]` — so a
# truth that varies across the grid can't be silently mis-scored. Score only the
# converged reps; report failures separately.
summary_tbl <- raw |>
filter(converged) |>
group_by(estimator) |> # add n, scenario, ... as needed
summarise(
R_eff = n(),
bias = mean(est - truth),
emp_se = sd(est),
rmse = sqrt(mean((est - truth)^2)),
coverage = mean(ci_lo <= truth & truth <= ci_hi),
.groups = "drop"
) |>
mutate(
bias_mcse = emp_se / sqrt(R_eff),
cov_mcse = sqrt(coverage * (1 - coverage) / R_eff)
)
failures <- raw |> group_by(estimator) |> summarise(n_fail = sum(!converged), .groups = "drop")
# Size/power: add `power = mean(reject)` (+ `sp_mcse = sqrt(power*(1-power)/R_eff)`)
# to the summary above — each estimator must emit a per-rep `reject = p_value < alpha`
# column. Size = rejection rate under the null DGP; power = under the alternative.
# 6. Export ----
saveRDS(raw, "scripts/R/_outputs/[name]_raw.rds")
saveRDS(summary_tbl, "scripts/R/_outputs/[name]_summary.rds")
write_csv(summary_tbl, "scripts/R/_outputs/[name]_summary.csv")Large grids (many scenarios × large R) can run for many minutes. Background-launch via Bash with run_in_background: true, capture the bash_id, and use the Monitor tool to stream R stdout (e.g., a progressr milestone or process exit) instead of polling with sleep. See data-analysis/SKILL.md and the guide's Cost-Conscious Parallelism section.
9d371f0
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.