Skip to contents

mcp aims to implement Generalized Linear Models in a way that closely mimics that of brms::brm. You can set the family and link functions using the family argument.

First, let us specify a toy model with three segments:

model = list(
  y | trials(N) ~ 1,  # constant success probability
  ~ 0 + year,  # joined changing success probability
  ~ 1 + year  # disjoined changing success probability
)

Simulate data

If you already have data, you can safely skip this section.

We run mcp with sample = FALSE to get what we need to simulate data.

library(mcp)
future::plan(future::multisession, workers = 3)

df = data.frame(
  year = 1901:2020,  # evaluate for each of these
  N = sample(10:20, size = 120, replace = TRUE),  # number of trials
  y = 1
)

empty = mcp(model, data = df, family = binomial(), sample = FALSE)

Now we can simulate. First, let us see the model parameters.

mcp_pars(empty)
## # A tibble: 6 × 8
##   parameter   part      scope      role         segment dpar  group_col population_parameter
##   <chr>       <chr>     <chr>      <chr>          <int> <chr> <chr>     <chr>               
## 1 cp_1        cp        population change_point       2 NA    NA        NA                  
## 2 cp_2        cp        population change_point       3 NA    NA        NA                  
## 3 Intercept_1 predictor population coefficient        1 mu    NA        NA                  
## 4 year_2      predictor population coefficient        2 mu    NA        NA                  
## 5 Intercept_3 predictor population coefficient        3 mu    NA        NA                  
## 6 year_3      predictor population coefficient        3 mu    NA        NA
  • It takes two intercepts (Intercept_*), for segments 1 and 3.
  • It takes two slopes (year_*), for segment 2 and 3.
  • It takes two change points (cp_*) - one between each pair of adjacent segments.

empty$simulate is now a function that can predict data given these parameters (signature: empty$simulate(fit, newdata, ...)). If you are in a reasonable R editor, type empty$simulate( and press TAB to see the required arguments. I came up with some values below, including change points at year = 1925 and year = 1975. Because binomial() defaults to link = "logit", the intercept and slopes are on the logit scale, which maps success probabilities between 0 and 1 to the real line. This will be important later when we set priors.

df$y = empty$simulate(
  empty, df, 
  cp_1 = 1925, cp_2 = 1975, 
  Intercept_1 = 2, Intercept_3 = -0.5, 
  year_2 = -0.1, year_3 = 0.1)

head(df)
##   year  N  y
## 1 1901 10  8
## 2 1902 14 13
## 3 1903 10  8
## 4 1904 18 15
## 5 1905 19 18
## 6 1906 13 13

Visually:

plot(df$year, df$y)

Check parameter recovery

The next sections go into more detail, but let us quickly see if we can recover the parameters used to simulate the data.

fit = mcp(model, data = df, family = binomial(), iter = 10000)

We can use summary to see that it recovered the parameters to a pretty good precision. Again, recall that intercepts and slopes are on a logit scale.

summary(fit)
## Family: binomial
## Links: mu = logit
## Draws: 30000 (3 chains with iter = 10000)
## Segments:
##   1: y | trials(N) ~ 1
##   2: ~ 0 + year
##   3: ~ 1 + year
## 
## Change point parameters:
##    parameter      mean     sd     lower     upper rhat ess_bulk ess_tail       sim match
##  cp_1        1921.5    2.8    1915.6    1926.8    1.00     3888     6842 1925.0       OK
##  cp_2        1974.27   0.81   1972.19   1974.98   1.00     3462     1549 1975.00      OK
## 
## Population-level parameters:
##    parameter      mean     sd     lower     upper rhat ess_bulk ess_tail       sim match
##  Intercept_1    2.18   0.19      1.84      2.58   1.00     4564     7680    2.00      OK
##  year_2        -0.0928 0.0067   -0.1065   -0.0803 1.00     7275    11366   -0.1000    OK
##  Intercept_3   -0.76   0.23     -1.26     -0.36   1.00     1665     1643   -0.50      OK
##  year_3         0.110  0.010     0.091     0.131  1.01     1125     2155    0.100     OK

summary uses 95% central posterior intervals by default, but you can change it using summary(fit, width = 0.80). If you have group-level effects, use ranef(fit) to see their deviations.

Plotting the fit confirms good fit to the data, and we see the discontinuities at the two change points:

plot(fit)

These lines are just fit$simulate applied to random posterior draws. In other words, they represent the joint distribution of the parameters. You can change the number of draws (lines) using plot(fit, lines = 50).

Speaking of alternative visualizations, you can also plot this on the logit scale, where the linear trends are modeled:

plot_dpar(fit, scale = "linear")
## Not showing 17 data point(s) with infinite values on the linear scale.

Data points with zero or all successes (y = 0 or y = N) are at \pm\infty on the logit scale, so they are not shown, as the message says.

These plots work with group-level effects as well.

Model diagnostics and sampling options

Already in the default plot as used above, it will be obvious if there was poor convergence. A more direct assessment is to look at the posterior distributions and trace plots:

plot_pars(fit, npars = NULL)

Convergence looks good here as evidenced by the overlapping trace plots that look like fat caterpillars (Bayesians love fat caterpillars). Notice that the posterior distribution of change points can be quite non-normal and sometimes even bimodal. Therefore, one should be careful not to interpret the interval as if it was normal.

plot() and plot_pars() can do a lot more than this, so check out the plotting article.

Priors for binomial models

Brief overview of all default priors

mcp uses priors to achieve a lot of its functionality. See how to set priors, including how to fix values. Here, I post a few notes about the binomial-specific default priors.

All priors are stored in fit$prior (also empty$prior). We did not specify prior above, so it ran with the default priors:

cbind(fit$prior)  # Raw view
prior_summary(fit)  # Richer view. Try adding verbose = TRUE
##             [,1]                            
## cp_1        "dunif(min(year), max(year))"   
## cp_2        "dunif(min(year), max(year))"   
## Intercept_1 "dnorm(0, 1.5)"                 
## year_2      "dnorm(0, 0.043121968093205169)"
## Intercept_3 "dnorm(0, 1.5)"                 
## year_3      "dnorm(0, 0.043121968093205169)"
## # A tibble: 6 × 5
##   parameter   segment dpar  prior              bounds               
##   <chr>         <int> <chr> <chr>              <chr>                
## 1 cp_1              2 NA    dunif(1901, 2020)  [1901, 2020], ordered
## 2 cp_2              3 NA    dunif(1901, 2020)  [1901, 2020], ordered
## 3 Intercept_1       1 mu    dnorm(0, 1.5)      none                 
## 4 year_2            2 mu    dnorm(0, 0.043122) none                 
## 5 Intercept_3       3 mu    dnorm(0, 1.5)      none                 
## 6 year_3            3 mu    dnorm(0, 0.043122) none

The priors on change points are discussed extensively in the prior article. With the default logit link, intercepts use dnorm(0, 1.5), which is approximately uniform on the probability scale: its central 95% interval is approximately -2.9 to 2.9 logits, corresponding to probabilities from 0.05 to 0.95. The probit link uses dnorm(0, 1), which is exactly uniform on the probability scale. The figure compares normal priors with SD 1.5 (black, the mcp default) and 2.5 (blue, the rstanarm default), together with their correspondence to probabilities.

inverse_logit = function(x) exp(x) / (1 + exp(x))

# Start the plot
library(ggplot2)
ggplot(data.frame(logits = 0), aes(x = logits)) + 
  
  # Plot normal priors. Set SD in "args"
  stat_function(fun = dnorm, args = list(sd = 1.5), lwd = 2, col = "black") +
  stat_function(fun = dnorm, args = list(sd = 2.5), lwd = 2, col = "blue") +
  
  # Set the secondary axis
  scale_x_continuous(
    breaks = -7:7, limits = c(-7, 7), 
    sec.axis = sec_axis(
      ~ inverse_logit(.), 
      name = "Probability",
      labels = scales::percent_format(),
      breaks = round(inverse_logit(seq(-7, 7, by = 2)), 3)
    )
  )

Please keep in mind that when these priors combine through the model, the joint probability may be quite different.

Coefficients and categorical contrasts use the same scales, autoscaled as in rstanarm: dnorm(0, 1.5 / sd(column)) for logit links and dnorm(0, 1 / sd(column)) for probit links.

JAGS code

Here is the JAGS code for the model used in this article.

fit$jags_code
## model {
##   # Change points
##   cp_0 = min(year)
##   cp_1 ~ dunif(cp_0, cp_3)  # Flat Dirichlet on the segment lengths
##   cp_2 ~ dunif(cp_0, cp_3)  # Flat Dirichlet on the segment lengths
##   cp_3 = max(year)
##   cp_order_ ~ dbern(step(cp_2 - cp_1))  # Ordered change points
## 
##   # Priors for population-level effects
##   Intercept_1 ~ dnorm(0, 1/(1.5)^2)  # Link-scale intercept, approximately uniform on the probability scale
##   year_2_end_ ~ dnorm(Intercept_1, 1/(0.043121968093205169*(cp_2-cp_1))^2)  # Autoscaled link-scale coefficient (rstanarm autoscaling); sampled as the value at the segment end
##   year_2 = (year_2_end_ - (Intercept_1)) / (cp_2 - cp_1)
##   Intercept_3_end_ ~ dnorm(year_3 * (cp_3 - cp_2), 1/(1.5)^2)  # Link-scale intercept, approximately uniform on the probability scale; sampled as the value at the segment end
##   Intercept_3 = Intercept_3_end_ - (year_3 * (cp_3 - cp_2))
##   year_3 ~ dnorm(0, 1/(0.043121968093205169)^2)  # Autoscaled link-scale coefficient (rstanarm autoscaling)
## 
##   # Model and likelihood
##   for (i_ in 1:length(year)) {
##     # year measured from each segment's change point
##     x_local_1_[i_] = min(year[i_], cp_1)
##     x_local_2_[i_] = min(year[i_], cp_2) - cp_1
##     x_local_3_[i_] = min(year[i_], cp_3) - cp_2
##     
##     # Formula for mu
##     link_mu_[i_] =
##       (year[i_] >= cp_0) * (year[i_] < cp_2) * inprod(rhs_matrix_[i_, c(1)], c(Intercept_1)) * 1 + 
##       (year[i_] >= cp_1) * (year[i_] < cp_2) * inprod(rhs_matrix_[i_, c(2)], c(year_2)) * x_local_2_[i_] + 
##       (year[i_] >= cp_2) * inprod(rhs_matrix_[i_, c(3)], c(Intercept_3)) * 1 + 
##       (year[i_] >= cp_2) * inprod(rhs_matrix_[i_, c(4)], c(year_3)) * x_local_3_[i_]
## 
##     # Likelihood for family = binomial()
##     mu_[i_] = ilogit(link_mu_[i_])
##     y[i_] ~ dbin(mu_[i_], N[i_])
##   }
## }