names(results)
#> [1] "results" "summary" "truth" "config" "scenarios"
#> [6] "method_name" "timestamp"medsim_run() returns a medsim_results object. This article covers what that object holds, which analysis function answers which question, how to compare methods fairly, and how to turn the results into tables and figures.
The examples continue the Sobel study from Tutorial: Your First Coverage Study: two scenarios (“Medium”, ab = .1521; “Null (a = 0)”, ab = 0), 500 replications each.
| Question | Function | Needs from the method |
|---|---|---|
| Is the estimator biased or noisy? | medsim_analyze() |
<param> + truth |
| Bias, empirical SE and model SE, with MCSEs | medsim_analyze_performance() |
<param>, <param>_se + truth |
| Do the intervals cover? | medsim_analyze_coverage() |
<param>_ci_lower, <param>_ci_upper + truth |
| How often does the test reject? | medsim_analyze_power() |
<param>_p |
| Which method is better? | medsim_compare_methods() |
two or more runs on the same scenarios |
| How many replications? | medsim_nsim_for_mcse() |
nothing (planning) |
names(results)
#> [1] "results" "summary" "truth" "config" "scenarios"
#> [6] "method_name" "timestamp"results$results holds one row per scenario × replication: the method’s outputs, plus scenario, replication, elapsed (seconds) and error (NA when the replication succeeded).results$truth holds one row per scenario: the values returned by compute_truth.results$config and results$scenarios record how the run was made, so a saved results file documents itself.Failed replications are kept, not dropped. Count them before you analyze:
tapply(!is.na(results$results$error), results$results$scenario, sum)The coverage table reports these as n_failed and failure_rate. A method that fails selectively (for example, on hard data sets) can look better than it is if the failures are ignored.
medsim_analyze()an <- medsim_analyze(results)
an$by_scenario[, c("scenario", "parameter", "bias", "rmse", "relative_bias")]
#> scenario parameter bias rmse relative_bias
#> 1 Medium indirect 0.0013953010 0.03928207 0.9173576
#> 2 Null (a = 0) indirect -0.0008496464 0.02809685 NArelative_bias is in percent of the true value. It is NA where the truth is zero, so report absolute bias for null scenarios.
medsim_analyze() scores every output column that has a matching truth value. It skips SE, interval and p-value columns (indirect_se, indirect_ci_lower, indirect_ci_upper, indirect_p) silently, because they belong to a scored estimate. Any other output column without a truth is named in a single warning, so a value you forgot to return from compute_truth is still flagged.
Every number in a simulation table is itself an estimate. Morris, White and Crowther (2019) recommend reporting each with its Monte Carlo SE (MCSE). medsim_analyze_performance() does that for bias, and compares the method’s own SE with the empirical SD of its estimates:
perf <- medsim_analyze_performance(results)
perf
#> scenario parameter bias bias_mcse empirical_se model_se
#> 1 Medium indirect 0.0013953010 0.001757397 0.03929660 0.03969967
#> 2 Null (a = 0) indirect -0.0008496464 0.001257212 0.02811212 0.02846886
#> rmse n_converged n_failed
#> 1 0.03928207 500 0
#> 2 0.02809685 500 0Read bias against bias_mcse. Here both biases are under one MCSE, so there is no detectable bias. A model_se well below empirical_se means the method’s standard errors are too small, which usually shows up as under-coverage.
cov <- medsim_analyze_coverage(results, ci_levels = 0.95)
pw <- medsim_analyze_power(results, alpha = 0.05)Coverage reports its own MCSE (coverage_mcse). For a rejection rate p estimated from R replications, the MCSE is $\sqrt{p(1-p)/R}$. At the nominal .05 and R = 500 that is about .0097, so a type I error between roughly .03 and .07 is consistent with a correct test.
Invert the MCSE formula when you plan a study. To estimate 95% coverage with an MCSE of .005:
medsim_nsim_for_mcse(target_mcse = 0.005, p = 0.95)
#> [1] 1900Plan for the worst case you need to resolve. A rejection rate near .5 has the largest MCSE, so power studies need more replications than coverage studies at the same precision.
Run each method on the same scenario list and pass the results to medsim_compare_methods():
mc <- function(data, params) {
fm <- summary(lm(M ~ X, data = data))$coefficients
fy <- summary(lm(Y ~ X + M, data = data))$coefficients
ab <- rnorm(5000, fm["X", 1], fm["X", 2]) * rnorm(5000, fy["M", 1], fy["M", 2])
q <- quantile(ab, c(0.025, 0.975), names = FALSE)
list(indirect = fm["X", 1] * fy["M", 1],
indirect_ci_lower = q[1], indirect_ci_upper = q[2])
}
results_mc <- medsim_run(mc, scenarios, config, compute_truth = truth)
medsim_compare_methods(sobel = results, mc = results_mc)
#> Accuracy Comparison:
#> method parameter mae rmse bias
#> sobel indirect 0.02630142 0.0341505 0.0002728273
#> mc indirect 0.02630142 0.0341505 0.0002728273
#> ...
#> Coverage Comparison:
#> method parameter coverage n_valid
#> sobel indirect 0.952 1000
#> mc indirect 0.945 1000The two accuracy rows are identical, and that is by design. Both methods report the same point estimate âb̂, and medsim seeds each replication from the scenario name and replication number, so both methods saw exactly the same 1,000 data sets. This pairing (common random numbers) means differences between methods are not diluted by data-set noise. The comparison above isolates the interval: the Monte Carlo CI covers .945 against Sobel’s .952 on identical data.
Keep scenario names fixed
Pairing holds only when the scenario names and replication numbers match. Renaming a scenario between runs breaks it silently.
Each medsim_table_*() function takes an analysis object and returns LaTeX. medsim_write_table() writes it to disk, with the format inferred from the file extension:
tab <- medsim_table_coverage(cov, expected = 0.95,
caption = "Sobel CI coverage")
medsim_write_table(tab, "tables/coverage.tex")
medsim_table_power(pw)
medsim_table_accuracy(an, metrics = c("bias", "rmse"))medsim_tables(results, output_dir = "tables") writes the standard set (accuracy, coverage, power) in one call.
The plotting functions return ggplot2 objects, so you can restyle them:
medsim_plot_coverage(cov, expected_coverage = 0.95, tolerance = 0.02)
medsim_plot_error_boxplot(results, parameter = "indirect")medsim_figures(results, output_dir = "figures") writes the standard set as PDFs.
medsim_workflow() runs the analysis and writes all standard tables and figures:
medsim_workflow(results, output_dir = "results")
list.files("results", recursive = TRUE)
#> [1] "figures/coverage.pdf" "figures/error_boxplot.pdf"
#> [3] "figures/timing.pdf" "tables/table_accuracy.tex"
#> [5] "tables/table_coverage.tex" "tables/table_power.tex"Use it for a first look. For a manuscript, call the individual functions so you control captions, labels and which parameters appear.
Morris, T. P., White, I. R., & Crowther, M. J. (2019). Using simulation studies to evaluate statistical methods. Statistics in Medicine, 38(11), 2074–2102. https://doi.org/10.1002/sim.8086