library(simpr)
The default simpr
workflow is easy to understand but
computationally inefficient. Although simpr
prioritizes
ease of use over computational speed, two things together can
make simpr more efficient:
future
packageConsider a standard simpr
workflow:
specify(a = ~ rnorm(n),
b = ~ a + rnorm(n)) %>%
define(n = c(100, 200)) %>%
generate(10) %>%
fit(lm = ~ lm(b ~ a)) %>%
tidy_fits()
An issue with this workflow is that it involves shuttling a lot of
data around: the generate()
step adds a list-column with
simulated data to a tibble, which is then sent to fit()
,
which adds a list-column containing large model objects, and then these
are all sent to tidy_fits()
for extracting essential model
statistics.
Instead, you can simply place the call to generate()
later in the chain:
specify(a = ~ rnorm(n),
b = ~ a + rnorm(n)) %>%
define(n = c(100, 200)) %>%
fit(lm = ~ lm(b ~ a)) %>%
tidy_fits() %>%
generate(10)
This means that the data is generated, fit, and tidied all at once
once you call generate()
. This means that these steps can
occur on a single parallel worker without pushing lots of data
around.
Behind the scenes, before generate()
is called,
simpr
simply stores successive commands in the
simpr_spec
object. When generate()
is called,
these successive commands are executed in order. Data munging, including
with per_sim()
or on the final tidied data, using
dplyr
or tidyr
is also supported in this
workflow. Below, data is specified, and the commands for reshaping,
fitting, and tidying, and selecting columns from the tidied output are
all written before generate()
and are executed
together:
specify(control = ~ rnorm(n, mean = 0),
intervention_1 = ~ rnorm(n, mean = 0.2),
intervention_2 = ~ rnorm(n, mean = 2)) %>%
define(n = c(6, 12)) %>%
per_sim() %>%
pivot_longer(cols = everything(),
names_to = "group",
values_to = "response") %>%
fit(lm = ~ lm(response ~ group)) %>%
tidy_fits() %>%
select(.sim_id, n, term, estimate) %>%
generate(2)
#> # A tibble: 12 × 4
#> .sim_id n term estimate
#> <int> <dbl> <chr> <dbl>
#> 1 1 6 (Intercept) 0.216
#> 2 1 6 groupintervention_1 -0.311
#> 3 1 6 groupintervention_2 2.02
#> 4 2 12 (Intercept) -0.350
#> 5 2 12 groupintervention_1 0.550
#> 6 2 12 groupintervention_2 2.32
#> 7 3 6 (Intercept) 0.249
#> 8 3 6 groupintervention_1 0.387
#> 9 3 6 groupintervention_2 1.76
#> 10 4 12 (Intercept) 0.243
#> 11 4 12 groupintervention_1 0.348
#> 12 4 12 groupintervention_2 2.21
future
packageChanging the evaluation order makes little difference on its own, but
combined with parallel processing can produce a speedup.
simpr
uses the furrr
package, part of the futureverse suite of packages
designed around the future
package. These packages are
designed to make parallel processing transparent as easy to use.
To use parallel processing with simpr
, simply load the
future
package and declare your “plan” for code execution
with future::plan()
. The three most relevant plans are:
sequential
, the default R behavior using just one
worker.multiprocess
, a parallel processing approach.multicore
, another parallel processing approach that
can be faster than multiprocess
, but which doesn’t work
with R Studio and only works on Linux/Mac.For both multiprocess
and multicore
plans,
you must tell R how many cores you want to use. You can check how many
your computer has available with
future::availableCores()
.
The optimized version of the opening example, rewritten to both change execution order and use parallel processing:
library(future)
plan(multisession, availableCores() - 1)
specify(a = ~ rnorm(n),
b = ~ a + rnorm(n)) %>%
define(n = c(100, 200)) %>%
fit(lm = ~ lm(b ~ a)) %>%
tidy_fits() %>%
generate(10)
Results will vary. The parallel version for this small simulation actually takes longer, because there fixed costs in setting up the workers. The speed advantage will become more apparent for larger simulations with slow data-generation or data-fitting steps.