In this version of the code, we've added set.seed()
, which helps make the exact results reproducible. If you're new to setting seeds, play around with different values.
set.seed(103) num_weeks <- 1e5 positions <- rep(0, num_weeks) current <- 10 for (i in 1:num_weeks) { # record current position positions[i] <- current # flip coin to generate proposal proposal <- current + sample(c(-1, 1), size = 1) # now make sure he loops around the archipelago if (proposal < 1) proposal <- 10 if (proposal > 10) proposal <- 1 # move? prob_move <- proposal/current current <- ifelse(runif(1) < prob_move, proposal, current) }
In this document, we'll borrow a theme, theme_ipsum()
, from the hrbrthemes package.
# install.packages("hrbrthemes", dependencies = T) library(hrbrthemes)
Figure 8.2.a.
library(tidyverse) tibble(week = 1:1e5, island = positions) %>% filter(week < 101) %>% ggplot(aes(x = week, y = island)) + geom_point(shape = 1) + scale_x_continuous(breaks = seq(from = 0, to = 100, by = 20)) + scale_y_continuous(breaks = seq(from = 0, to = 10, by = 2)) + labs(title = "Behold: The Metropolis algorithm in action!", subtitle = "The dots show the king's path over the first 100 weeks.") + theme_ipsum()
Figure 8.2.b.
tibble(week = 1:1e5, island = positions) %>% mutate(island = factor(island)) %>% ggplot(aes(x = island)) + geom_bar() + labs(title = "Old Metropolis shines in the long run.", subtitle = "Sure enough, the time the king spent on each island was\nproportional to its population size.") + theme_ipsum()
brm()
Here we load the rugged
data.
library(rethinking) data(rugged) d <- rugged
Switching from rethinking to brms.
detach(package:rethinking) library(brms) rm(rugged)
It takes just a sec to do a little data manipulation.
d <- d %>% mutate(log_gdp = log(rgdppc_2000)) dd <- d %>% filter(complete.cases(rgdppc_2000))
In the context of this chapter, it doesn't make sense to translate McElreath's m8.1 map()
code to brm()
code. Below, we'll just go directly to the brm()
variant of his m8.1stan
.
You don't need to do the data processing McElreath does on pages 248 and 249. If you wanted to, however, here's how you might do it within the tidyverse.
dd.trim <- dd %>% select(log_gdp, rugged, cont_africa) str(dd.trim)
Finally, we get to work that sweet HMC.
b8.1 <- brm(data = dd, family = gaussian, log_gdp ~ 1 + rugged + cont_africa + rugged:cont_africa, prior = c(set_prior("normal(0, 100)", class = "Intercept"), set_prior("normal(0, 10)", class = "b"), set_prior("cauchy(0, 2)", class = "sigma")))
w_b8.1 <- waic(b8.1) l_b8.1 <- loo(b8.1) l_b8.1_save_psis <- loo(b8.1, save_psis = T) w_b8.1 l_b8.1
w_b8.1$ pointwise %>% str l_b8.1$pointwise w_b8.1$pointwise %>% as_tibble() %>% arrange(p_waic) %>% mutate(i = 1:n()) %>% ggplot(aes(x = i, y = p_waic)) + geom_point() l_b8.1$pointwise %>% as_tibble() %>% arrange(p_loo) %>% mutate(i = 1:n()) %>% ggplot(aes(x = i, y = p_loo)) + geom_point()
plot(l_b8.1)
yrep <- posterior_predict(b8.1) # requires bayesplot version >= 1.5.0 library(bayesplot) ppc_loo_pit_overlay( y = dd$log_gdp, yrep = yrep, lw = weights(l_b8.1_save_psis$psis_object) )
The posterior:
print(b8.1)
Here we add chains = 4, cores = 4
.
b8.1_4chains_4cores <- brm(data = dd, family = gaussian, log_gdp ~ 1 + rugged + cont_africa + rugged:cont_africa, prior = c(set_prior("normal(0, 100)", class = "Intercept"), set_prior("normal(0, 10)", class = "b"), set_prior("cauchy(0, 2)", class = "sigma")), chains = 4, cores = 4)
This model sampled so fast that it really didn't matter if we sampled in parallel or not. It will for others.
print(b8.1_4chains_4cores)
Unlike the way rethinking's extract.samples()
yields a list, brms's posterior_samples()
returns a data frame.
post <- posterior_samples(b8.1) str(post)
As with McElreath's rethinking, brms allows users to put the post
data frame or the brmsfit object directly in pairs()
.
pairs(b8.1)
Another nice way to customize your pairs plot is with the GGally package.
# install.packages("GGally", dependencies = T) library(GGally)
post %>% select(b_Intercept:sigma) %>% ggpairs()
The above code returned an error: "variables: "x" have non standard format: "b_rugged:cont_africa". Please rename the columns or make a new column." GGally didn't like the way brms named one of our parameters, b_rugged:cont_africa
. That's easy enough to fix.
my_pairs_plot <- post %>% rename(b_rugged_x_cont_africa = `b_rugged:cont_africa`) %>% select(b_Intercept:sigma) %>% ggpairs() my_pairs_plot
Since GGally returns a ggplot object, you can customize it as you please.
my_pairs_plot + labs(subtitle = "My custom pairs plot") + theme_ipsum()
For more ideas on customizing a GGally pairs plot, go here.
If you want information criteria as a part of your brms model summary, just add loo = T
and/or waic = T
in the summary()
function.
summary(b8.1, loo = T, waic = T)
But beware: the information criteria haven’t taken long to estimate in the models we’ve fit thus far in the text. As your models become more complex and as your data get larger, the time their information criteria will take to compute will increase. So, if you have a complex multilevel model with a large data set, you might be better off computing the information criteria separately from the model summary.
Using plot()
for a brm()
fit returns both density and trace lots for the parameters.
plot(b8.1)
The bayesplot package allows a little more control. Here, we use bayesplot's mcmc_trace()
to show only trace plots with our custom theme. Note that mcmc_trace()
works with data frames, not brmfit objects. There's a further complication. Recall how we made post
(i.e., post <- posterior_samples(b8.1)
). Our post
data frame carries no information on chains. To retain that information, we'll need to add an add_chain = T
argument to our posterior_samples()
function.
library(bayesplot) post <- posterior_samples(b8.1, add_chain = T) mcmc_trace(post[, c(1:5, 7)], # We need to include column 7 because that contains the chain info facet_args = list(ncol = 3), size = .15) + labs(title = "My custom trace plots") + scale_color_ipsum() + theme_ipsum() + theme(legend.position = c(.95, .2))
The bayesplot package offers a variety of diagnostic plots. Here we make autocorrelation plots for all model parameters, one for each HMC chain.
mcmc_acf(post, pars = c("b_Intercept", "b_rugged", "b_cont_africa", "b_rugged:cont_africa", "sigma"), lags = 5) + scale_color_ipsum() + theme_ipsum()
That's just what we like to see--nice L-shaped autocorrelation plots. Those are the kinds of shapes you'd expect when you have reasonably large effective samples. Anyway...
The stancode()
function works in brms much like it does in rethinking.
stancode(b8.1)
You can also get that information with b8.1$model
or b8.1$fit@stanmodel
.
As with rethinking, brms can take data in the form of a list. Recall however, that in order to specify starting values, you need to specify a list of lists with an inits
argument, rather than with start
, as in rethinking.
b8.2 <- brm(data = list(y = c(-1, 1)), family = gaussian, y ~ 1, prior = c(set_prior("uniform(-1e10, 1e10)", class = "Intercept"), set_prior("uniform(0, 1e10)", class = "sigma")), inits = list(list(Intercept = 0, sigma = 1), list(Intercept = 0, sigma = 1)), chains = 2, iter = 4000, warmup = 1000)
Those were some silly flat priors. Here's the damage.
post <- posterior_samples(b8.2, add_chain = T) mcmc_trace(post[, c(1:2, 4)], size = .25) + labs(title = "My version of Figure 8.5.a.", subtitle = "These trace plots do not look like the fuzzy caterpillars we usually hope for.") + scale_color_ipsum() + theme_ipsum() + theme(legend.position = c(.85, 1.5), legend.direction = "horizontal")
Let's peek at the summary.
print(b8.2)
Holy smokes, those parameters are a mess! Plus we got a nasty warning message, too. Watch our reasonable priors save the day.
b8.3 <- brm(data = list(y = c(-1, 1)), family = gaussian, y ~ 1, prior = c(set_prior("normal(0, 10)", class = "Intercept"), set_prior("cauchy(0, 1)", class = "sigma")), inits = list(list(Intercept = 0, sigma = 1), list(Intercept = 0, sigma = 1)), chains = 2, iter = 4000, warmup = 1000)
print(b8.3)
As in the text, no more warning signs and no more silly estimates. The trace plots look great, too.
post <- posterior_samples(b8.3, add_chain = T) mcmc_trace(post[, c(1:2, 4)], size = .25) + labs(title = "My version of Figure 8.5.b", subtitle = "Oh man. This looks so much better.") + scale_color_ipsum() + theme_ipsum() + theme(legend.position = c(.85, 1.5), legend.direction = "horizontal")
Our version of Figure 8.6.a.
post %>% select(b_Intercept) %>% ggplot(aes(x = b_Intercept)) + stat_density(geom = "line") + geom_line(data = data.frame(x = seq(from = min(post$b_Intercept), to = max(post$b_Intercept), length.out = 50)), aes(x = x, y = dnorm(x = x, mean = 0, sd = 10)), color = ipsum_pal()(1), linetype = 2) + theme_ipsum()
And our version of Figure 8.6.b.
post %>% select(sigma) %>% ggplot(aes(x = sigma)) + stat_density(geom = "line") + geom_line(data = data.frame(x = seq(from = 0, to = max(post$sigma), length.out = 50)), aes(x = x, y = dcauchy(x = x, location = 0, scale = 1)*2), color = ipsum_pal()(2)[2], linetype = 2) + coord_cartesian(xlim = c(0, 10)) + theme_ipsum()
It appears that the only way to get a brms version of McElreath's m8.4
and m8.5
is to augment the data. In addition to the Gaussian y
vector, we'll add two constants to the data, intercept1 = 1
and intercept2 = 1
.
set.seed(8.4) y <- rnorm(100, mean = 0, sd = 1)
b8.4 <- brm(data = list(y = y, intercept1 = 1, intercept2 = 1), family = gaussian, y ~ 0 + intercept1 + intercept2, prior = c(set_prior("uniform(-1e10, 1e10)", class = "b"), set_prior("cauchy(0, 1)", class = "sigma")), inits = list(list(intercept1 = 0, intercept2 = 0, sigma = 1), list(intercept1 = 0, intercept2 = 0, sigma = 1)), chains = 2, iter = 4000, warmup = 1000, seed = 8.4)
Our model results don't perfectly mirror McElreath's, but they're identical in spirit.
print(b8.4)
Note the frightening warning message.
b8.5 <- brm(data = list(y = y, intercept1 = 1, intercept2 = 1), family = gaussian, y ~ 0 + intercept1 + intercept2, prior = c(set_prior("normal(0, 10)", class = "b"), set_prior("cauchy(0, 1)", class = "sigma")), inits = list(list(intercept1 = 0, intercept2 = 0, sigma = 1), list(intercept1 = 0, intercept2 = 0, sigma = 1)), chains = 2, iter = 4000, warmup = 1000)
print(b8.5)
Now we'll do the preparatory work for Figure 8.7. Instead of showing the plots, here, we'll save them as objects, left_column
and right_column
, in order to combine them below.
post <- posterior_samples(b8.4, add_chain = T) left_column <- mcmc_trace(post[, c(1:3, 5)], size = .25, facet_args = c(ncol = 1)) + scale_color_ipsum() + theme_ipsum() + theme(legend.position = c(.85, 1.5), legend.direction = "horizontal") post <- posterior_samples(b8.5, add_chain = T) right_column <- mcmc_trace(post[, c(1:3, 5)], size = .25, facet_args = c(ncol = 1)) + scale_color_ipsum() + theme_ipsum() + theme(legend.position = c(.85, 1.5), legend.direction = "horizontal")
With a little help of the multiplot()
function we can slap our two plot objects together.
Behold the code for the multiplot()
function:
multiplot <- function(..., plotlist=NULL, file, cols=1, layout=NULL) { library(grid) # Make a list from the ... arguments and plotlist plots <- c(list(...), plotlist) numPlots = length(plots) # If layout is NULL, then use 'cols' to determine layout if (is.null(layout)) { # Make the panel # ncol: Number of columns of plots # nrow: Number of rows needed, calculated from # of cols layout <- matrix(seq(1, cols * ceiling(numPlots/cols)), ncol = cols, nrow = ceiling(numPlots/cols)) } if (numPlots==1) { print(plots[[1]]) } else { # Set up the page grid.newpage() pushViewport(viewport(layout = grid.layout(nrow(layout), ncol(layout)))) # Make each plot, in the correct location for (i in 1:numPlots) { # Get the i,j matrix positions of the regions that contain this subplot matchidx <- as.data.frame(which(layout == i, arr.ind = TRUE)) print(plots[[i]], vp = viewport(layout.pos.row = matchidx$row, layout.pos.col = matchidx$col)) } } }
We're finally ready to use multiplot()
to make our version of Figure 8.7.
multiplot(left_column, right_column, cols = 2)
The central message in the text, default to weakly-regularizing priors, holds for brms just as it does in rethinking. For more on the topic, see the recommendations from the Stan team. If you want to dive deeper, check out Dan Simpson's post on Gelman's blog and their corresponding paper with Michael Betancourt.
# rm(i, num_weeks, positions, current, proposal, prob_move, d, dd, b8.1, b8.1_4chains_4cores, post, my_pairs_plot, b8.2, b8.3, b8.4, b8.5, multiplot, left_column, right_column)
Note. The analyses in this document were done with:
McElreath, R. (2016). Statistical rethinking: A Bayesian course with examples in R and Stan. Chapman & Hall/CRC Press.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.