Using the simulate package in
R!
A starter vignette
Welcome to the simulate package! This R
package helps R coders to conduct statistical simulations
using their regression models.
Why simulate?
Statistical simulation refers to generating model predictions for a hypothetical observation with specific traits (usual the average case in the dataset), using repeated simulations (random draws) to account for estimation and fundamental uncertainty.
The idea was popularized by Gary King and colleagues’ Stata package
Clarify (King, Tomz, and Wittenberg
2000), and extended to R through the work of Kosuke
Imai, Olivia Lau, Gary King, Christine Choirat, and many more via the
Zelig package in R. Choirat et al. (2018).
With Zelig recently going offline, the simulate package
provides a lightweight stand-in for regular Zelig users,
supporting basic models from the lm(), glm(),
MASS::glm.nb(), and betareg:betareg()
functions.
Install Package from Github
You can install simulate from Github using the
devtools package’s install_github() function.
(Sorry, no CRAN package right now!)
# Load devtools
library(devtools)
# Install package from github!
install_github("timothyfraser/simulate",
# and get the package's dependencies
dependencies = TRUE, upgrade = TRUE)To get started, please load the simulate and
tidyverse packages! simulate is intended to be
used in tandem with tidyverse functions from
ggplot2 and dplyr; they are necessary for
running simulate functions.
library(simulate) # for tidy simulation!
library(tidyverse) # for tidy data wrangling and visualization!Workflow
Using the simulate package involves a short string of
key functions, including equate(),
calculate(), simulate(),
tabulate(), and illustrate(). See Figure
1!
Figure 1: Sample Workflow in simulate package!
Example
For example, let’s use the famous mtcars dataset to
simulate the effects of horse power (hp) and cylinders
(cyl) on the mileage (mpg) of 32 fancy
cars.
A usual simulate package query might look a bit like
this below.
# Using a sample dataset
mtcars %>%
# Make a model
mutate(cyl = factor(cyl)) %>%
lm(formula = mpg ~ hp + cyl) %>%
# Simulate estimate uncertainty
equate() %>%
# Calculate predictions from simulated equations
calculate(setx = list(hp = c(100, 150, 200))) %>%
# Simulate fundamental uncertainty
simulate() %>%
# for each setx case
group_by(case) %>%
# summarize results!
tabulate(qi = "ev", mu = 0)In the following sections, I’ll break down each of these functions with examples!
1. Make Model Object
First, we’ll use the lm() function to create a simple
ordinary least squares model, called m, with
mpg as the outcome and hp and cyl
as predictors. We’ll turn cyl into a factor(),
to demonstrate how to use factors / fixed effects here.
Note: Current limitations of the package require that you
turn predictors into factor() format before
regressing them in the model. simulate will not work
otherwise.
m <- mtcars %>%
mutate(cyl = factor(cyl)) %>%
lm(formula = mpg ~ hp + cyl)
2. equate() estimation uncertainty
Next, we’ll use equate() to generate 1000 model
equations, each very close to the observed model equation in model
m, but each just slightly different. These slightly
differing model equations represent estimation uncertainty,
the uncertainty in the true values of our beta coefficients.
Exactly like in Zelig and CLARIFY,
equate() takes random draws from a multivariate normal
distribution using the model’s coefficients and variance-covariance
matrix.
myequations <- m %>% equate()A bonus of the simulate package is that
equate() gives you more control, allowing you to work
directly with our simulated equations, in the event that you only want
to account for estimation uncertainty, or in the event that you want to
reuse the exact same multivariate normal distribution.
You can set a seed with seed = 12345, for example, to
reproduce results. To change the number of simulated equations sampled,
adjust reps from reps = 1000 (default) to, for
example, reps = 3. (Though 1000 is standard!)
# for example,
m %>% equate(reps = 3, seed = 12345)#> # A tibble: 3 × 5
#> replicate `(Intercept)` hp cyl6 cyl8
#> * <int> <dbl> <dbl> <dbl> <dbl>
#> 1 1 28.2 -0.0310 -5.26 -6.91
#> 2 2 30.1 -0.0384 -5.65 -7.01
#> 3 3 26.4 -0.00544 -4.01 -9.03
3. calculate() simulated values
Next, we’re going to use calculate() to compute our
simulated equations. By default, calculate() takes the
median of numeric variables and the mode of categorical variables, but
you can simulate at specific values, using the argument
setx = list(...) (much like in Zelig).
For example, let’s calculate the simulated mpg for an
otherwise average car as its hp increases from
100 to 150 to 200.
mycalc <- myequations %>%
calculate(setx = list(hp = c(100, 150, 200)))#> [1] "Predictor values set using contents of setx; otherwise, set to medians and modes by default."
calculate outputs a data.frame containing
replicate, the id for each simulated equation,
ysim, the predictions from each simulated equation,
sigma, the residual standard error for the model, and
case, the unique ID for the n conditions supplied
in setx. We used hp = c(100, 150, 200),
totalling 3 conditions, so case runs from 1 to
3.
mycalc %>% head(3)
4. simulate() fundamental uncertainty
Fourth, we can use simulate() to simulate fundamental
uncertainty. This takes each simulated value outputted by
calculate() and then constructs a distribution fitting the
outcome variable (normal for lm(), poisson for
glm(family = "Poisson"), etc.)
simulate() outputs a tibble containing
replicate, the unique ID for each simulated equation,
pv, the predicted value for that equation, ev,
the expected value for that equation, and case, the unique
ID for each condition specified in setx in the
calculate() function.
mysim <- mycalc %>% simulate()Check it out!
mysim %>% head()#> # A tibble: 6 × 4
#> replicate case pv ev
#> <chr> <int> <dbl> <dbl>
#> 1 1 1 21.8 20.0
#> 2 1 2 20.1 17.9
#> 3 1 3 15.5 15.7
#> 4 10 1 13.8 15.3
#> 5 10 2 16.4 14.4
#> 6 10 3 7.90 13.6
But what are ev and pv?
To make predicted values (
pv), it simulates 1 random draw from this distribution.To make expected values (
ev), it simulates 1000 random draws from this distribution, and takes the average of them.
Predicted values are encouraged when making predictions about the future, since they generate wider confidence intervals, while expected values are generally encouraged for explaining the past, as they generate more precise estimates, averaging over fundamental uncertainty.
We can do a lot with these values using ggplot2! The
most direct visualization is usually a geom_point() or
geom_jitter(), but geom_violin() can be handy
at showing distributions too!
mysim %>%
# map aesthetics
ggplot(mapping = aes(x = case, y = ev, color = factor(case), group = factor(case) )) +
# Show full 1000 expected value range
geom_jitter() +
# Show distributions
geom_violin(fill = "white", alpha = 0.5) +
# Relabel the x-axis's case variable as needed
scale_x_continuous(
breaks = c(1, 2, 3),
labels = c("100 hp", "150 hp", "200 hp")) +
# Skip legend
guides(color = "none") +
# Add labels
labs(x = "Level of Horsepower", y = "Expected Mileage (mpg)",
color = "Conditions")
5. illustrate() results
What if we want to visualize not just the overall range, but varying
confidence intervals? To get that data, we can use
illustrate() on a quantity of interest (qi).
Be sure to use group_by(case) to get confidence intervals
for each different condition from setx!
mybands <- mysim %>%
group_by(case) %>%
illustrate(qi = "ev")Check it out! Each row has a unique ID describing a band, and
together, this tidy dataframe can be used to visualize the full range of
confidence intervals in ggplot2.
mybands %>% head()#> # A tibble: 6 × 5
#> # Groups: case [1]
#> case group ci lower upper
#> <int> <int> <fct> <dbl> <dbl>
#> 1 1 1 99.9% 11.9 12.5
#> 2 1 2 99% 12.5 14.0
#> 3 1 3 95% 14.0 14.6
#> 4 1 4 90% 14.6 17.8
#> 5 1 5 90% 17.8 21.0
#> 6 1 6 95% 21.0 21.5
We can visualize mybands like so in ggplot2
(but for ideas, check out my other tutorials on RPubs!)
mybands %>%
# Map aethestics
ggplot(mapping = aes(x = case, ymin = lower, ymax = upper, group = group, fill = ci)) +
# Make a pretty ribbon for each group!
geom_ribbon(alpha = 0.5, color = "black") +
# Shade the ribbon!
scale_fill_brewer(palette = "Blues") +
# Relabel the x-axis's case variable as needed
scale_x_continuous(
breaks = c(1, 2, 3),
labels = c(100, 150, 200)) +
# Add lables
labs(x = "Level of Horsepower", y = "Expected Mileage (mpg)",
fill = "Confidence\nInterval")illustrate() can even be used on other outputs, like our
model equations from equate().
myequations %>%
illustrate(qi = "hp") %>%
head(3)#> # A tibble: 3 × 4
#> group ci lower upper
#> <int> <fct> <dbl> <dbl>
#> 1 1 99.9% -0.0709 -0.0625
#> 2 2 99% -0.0625 -0.0548
#> 3 3 95% -0.0548 -0.0507
6. tabulate() results
Finally, do you just want a nice model table? Can do! We can tabulate
these results like so, supplying the tabulate() function a
column to tabulate.
6.1 Tabulating Confidence Intervals
tabulate() will return an estimate, the
median simulated value for your quantity of interest (qi),
lower and upper simulated confidence intervals
(defaulting at 95% as shown by ci = 0.95), and
se, showing the standard deviation of simulations. We can
even pair it with group_by(case) to get estimates for each
level.
mysim %>%
group_by(case) %>%
tabulate(qi = "ev", ci = 0.95, mu = 0)#> # A tibble: 3 × 8
#> case term estimate lower upper se p_value stars
#> <int> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 1 ev 17.8 14.0 21.5 1.90 0 ***
#> 2 2 ev 16.6 14.1 18.9 1.23 0 ***
#> 3 3 ev 15.3 13.7 16.9 0.809 0 ***
6.2 Hypothesis Testing with mu
Further, you can easily test whatever hypothesis you want
using the mu parameter above. mu sets the null
hypothesis for the estimate. The p_value shows a one-tailed
test of what percentage of simulations cross mu from the
estimate. As in, if the estimate is positive and mu = 0,
what share of simulations are negative? Our p-value for the null
hypothesis mu = 0 above is 0%, but that’s not
very surprising.
Instead, tabulate()’s mu parameter becomes
handy when testing hypotheses about our simulations against real world
benchmarks. For instance, did PTSD rates after a disaster exceed the
average level in the population? As X increases from 1 to 10, how often
do we expect those rates to exceed mu percent of
residents?
Or in this case, the median mileage (mpg) in our dataset
is 19.2 miles per gallon. So a meaningful quantity of
interest might be, at what level of horsepower does a car’s mileage
really begin to exceed average levels?
mtcars$mpg %>% median()#> [1] 19.2
We can test this hypothesis by setting mu to
19.2. We see below that otherwise-average cars with a
hp of 100 (case == 1) do not tend
to have mpg that are significantly different from the
median. However, as their hp increases
(case == 2 and case == 3), their
mpg actually becomes significantly lower that
median levels!
mysim %>%
group_by(case) %>%
tabulate(qi = "ev", ci = 0.95, mu = 19.2)#> # A tibble: 3 × 8
#> case term estimate lower upper se p_value stars
#> <int> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 1 ev 17.8 14.0 21.5 1.90 0.236 " "
#> 2 2 ev 16.6 14.1 18.9 1.23 0.014 "*"
#> 3 3 ev 15.3 13.7 16.9 0.809 0 "***"
6.3 First Differences with tabulate()
mu is especially useful when computing first differences
(also known as marginal effects), meaning the change in expected or
predicted values. Let’s give it a try!
myfd <- mysim %>%
# Pivot the case ids into columns
pivot_wider(id_cols = c(replicate), names_from = case,
values_from = ev, names_prefix = "ev_") %>%
# And calculate how much GREATER ev is for case 3 than for case 1!
mutate(fd = ev_3 - ev_1)Check it out!
myfd %>% head(3)#> # A tibble: 3 × 5
#> replicate ev_1 ev_2 ev_3 fd
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 1 20.0 17.9 15.7 -4.36
#> 2 10 15.3 14.4 13.6 -1.70
#> 3 100 17.4 16.9 16.4 -1.05
Or if you feel more comfortable mixing base R and tidyverse coding,
try using brackets in summarize(), which does the same
thing:
mysim %>%
group_by(replicate) %>%
summarize(
ev_1 = ev[case == 1],
ev_3 = ev[case == 3],
fd = ev_3 - ev_1) %>%
head(3)#> # A tibble: 3 × 4
#> replicate ev_1 ev_3 fd
#> <chr> <dbl> <dbl> <dbl>
#> 1 1 20.0 15.7 -4.36
#> 2 10 15.3 13.6 -1.70
#> 3 100 17.4 16.4 -1.05
6.4 Coefficient Tables
We might also just want to make a simple coefficent table using
tabulate()! Many models today violate the assumption of
independence of observations, which is required to compute standard
errors from a variance-covariance matrix. However, we could just
simulate these error instead, by taking the standard deviation from our
estimation uncertainty estimates from equate().
When applying tabulate() to an output from
equate(), you do not need to specify a quantity of interest
(qi); tabulate() will build the coefficient
table for all variables in the model.
Here, mu = 0 by default, which is the usual null
hypothesis for beta coefficients. (However, you can change
mu to anything, allowing you to do mini-linear hypothesis
tests if you want!)
myequations %>%
tabulate(ci = 0.95, mu = 0)#> # A tibble: 4 × 7
#> term estimate lower_ci upper_ci se p_value stars
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 (Intercept) 28.7 25.4 31.8 1.62 0 ***
#> 2 hp -0.0240 -0.0548 0.00687 0.0157 0.064 .
#> 3 cyl6 -5.97 -9.06 -2.80 1.61 0.001 ***
#> 4 cyl8 -8.52 -13.2 -3.67 2.32 0 ***
(Note: All p-values from these simulations are currently one-tailed,
reflecting what share of simulations are on the opposite side of
mu from the estimate. However, this also means that we know
that the other tail does exceed mu, so
two-tailed testing isn’t strictly necessary for this method. Please
report results accordingly!)
7. Next Steps
Hope you enjoy! If you like it, share it! If you use the package, please do cite it! Have questions? Feel free to reach out on Github. Happy simulating!
# Get the citation like this!
citation("simulate")#>
#> To cite package 'simulate' in publications use:
#>
#> Fraser T (2022). _simulate: Tidy Simulation for Statistical Models_.
#> R package version 0.0.0.9000.
#>
#> A BibTeX entry for LaTeX users is
#>
#> @Manual{,
#> title = {simulate: Tidy Simulation for Statistical Models},
#> author = {Timothy Fraser},
#> year = {2022},
#> note = {R package version 0.0.0.9000},
#> }