• Home
  • Work
  • Posts
  • Personal

On this page

  • Prepare data
  • Part 1 – No-pooling model
    • Statistical Model
    • Fit model
    • Diagnostics
    • Visualize estimates
  • Part 2 – Partial-pooling model
    • Statistical Model
    • Fit Model
    • Diagnostics
    • Visualize estimates

Statistical Rethinking 2026, B01

R
Bayes
causal inference
shrinkage
pooling
My solution to exercise B01 from Richard McElreath’s Statistical Rethinking course.
Author

Paulina Sell

Published

Jun 2026

Link to full course on GitHub
Link to online lecture

TipExercise

These problems use data from a 2019 publication that reports date and time of submission of papers to the medical journal The BMJ (doi:10.1136/bmj.l6460). There are 6 columns and 49,464 rows. Each row is an article submission. The columns are:

T: The local time of the submission
Y: The year of the submission
country_name: Country of corresponding author
L: ID value for country (an index variable from 1 to 38)
W: 0-1 indicator variable for weekend submission
H: 0-1 indicator variable for holiday submission

  1. For each country in the sample, estimate the probability that a corresponding author submits on a weekend. In this problem, use a no-pooling (no shrinkage) model. This means the model does not have hierarchical priors. Which countries have the highest probability of submitting on the weekend? Can you visualize these estimates in a way that also shows their uncertainties?

  2. Now use a partial-pooling model. This means that the model has a hierarchical prior for each country’s estimate. Hint: Think of each country like a tadpole tank. How are the partial-pooling estimates different from the no-pooling estimates? Can you figure out why they differ?

First, we need to load some packages and the BMJ data.

library(rethinking)
library(tidyplots)
library(tibble)
library(dplyr)
library(forcats)

data_orig <- read.csv("https://raw.githubusercontent.com/rmcelreath/stat_rethinking_2026/refs/heads/main/homework/BMJSubmissions.csv")
str(data_orig)
'data.frame':   49464 obs. of  6 variables:
 $ T           : num  11.8 10.5 13.5 19.5 14.4 ...
 $ Y           : int  2012 2012 2012 2012 2012 2012 2012 2012 2012 2012 ...
 $ country_name: chr  "Canada" "Italy" "Netherlands" "United Kingdom" ...
 $ L           : int  7 18 21 37 37 38 1 9 12 12 ...
 $ W           : int  0 0 0 0 0 0 0 0 0 0 ...
 $ H           : int  0 0 0 0 0 1 0 0 0 0 ...

Prepare data

Below, we create a new variable N that contains the total number of submissions in each country L. We also add W_count as a variable, containing the number of weekend submissions per country. We then calculate the observed proportions of weekend submissions per country by dividing W_count by N. Finally, we create a list that is required in rethinking.

Notice that we’ve created a summary dataframe that no longer has 49,464 rows (one row for each submission) but only 38 rows, each containing data for a unique country.

data <- data_orig |>
  group_by(L) |>
  summarise(
    N = n(),
    W_count = sum(W),
    country_name = unique(country_name)
  ) |>
  mutate(
    propweekend = W_count / N
  ) |>
  ungroup()

data <- list(
    country_name = data$country_name,
    propweekend = data$propweekend,
    L = data$L, # country index ( 1 to 38)
    N = data$N, # number of submissions per country (rows per country index)
    W_count = data$W_count # weekend submission count per country
)

str(data)
List of 5
 $ country_name: chr [1:38] "Australia" "Austria" "Bangladesh" "Belgium" ...
 $ propweekend : num [1:38] 0.115 0.158 0.255 0.107 0.157 ...
 $ L           : int [1:38] 1 2 3 4 5 6 7 8 9 10 ...
 $ N           : int [1:38] 3263 184 106 328 600 101 2821 6183 1455 228 ...
 $ W_count     : int [1:38] 374 29 27 35 94 27 336 1394 158 40 ...

Part 1 – No-pooling model

Statistical Model

First, we want to estimate the probability for each country that an author submits an article to BMJ on the weekend. We use a non-hierarchical model. Since we want to estimate the probability of a submission on the weekend, we use a Binomial distribution. I’ve tried using a Bernoulli distribution and it also works, but just takes super long to run.

\[ \text{W}_i \sim \text{Binomial}(N_\text{submissions[L]}, p_\text{L}) \] \[ \text{logit}(p_\text{L}) = a_\text{L[i]} \] \[ a_\text{L} \sim \text{Normal}(0, 1) \]

\(a_\text{L[i]}\) is the probability to submit on the weekend for author \(i\) in country \(\text{L}\).

Fit model

Let’s try and fit the model from above using ulam.

mWE_noml <- ulam(
  alist(
    W_count ~ dbinom(N, p),
    logit(p) <- a[L],
    a[L] ~ dnorm(0, 1)
  ),
  data = data,
  chains = 4,
  iter = 2000,
  log_lik = TRUE
)
show(mWE_noml)
Hamiltonian Monte Carlo approximation
4000 samples from 4 chains

Sampling durations (seconds):
  chain_id warmup sampling total
1        1   0.11     0.11  0.22
2        2   0.11     0.08  0.19
3        3   0.11     0.10  0.21
4        4   0.11     0.09  0.20

Formula:
W_count ~ dbinom(N, p)
logit(p) <- a[L]
a[L] ~ dnorm(0, 1)

Diagnostics

Let’s see some quick diagnostics:

dashboard(mWE_noml)

I’ve used the undocumented dashboard() function that McElreath mentioned in his lecture.
Top left: The Rhat values are all near or at 1.00, meaning that all four chains converged to the same distribution and they explored the same landscape. The effective sample sizes are high, ranging from 8,000 to 12,000, so we have lots of good-quality draws to work with.
Top right: The HMC energy plot shows that the two distributions overlap well, meaning the sampler moved through the posterior efficiently without getting stuck in any region.
Bottom left: There are 0 divergent transitions. Divergences would signal posterior geometry problems like funnel shapes or strong correlations, that HMC can’t handle well.
Bottom right: The log-probability trankplot (a natural scalar summary of where in the posterior the sampler currently is) shows all four chains moving freely over the same range, which means good mixing.

NoteParameter estimate information & trankplots

Here you can see the precis() output and all the trankplots.

precis(mWE_noml, depth = 2)
            mean         sd      5.5%      94.5%      rhat ess_bulk
a[1]  -2.0395643 0.05596513 -2.128728 -1.9513467 1.0013036 6242.047
a[2]  -1.6252669 0.18825972 -1.929843 -1.3282776 0.9999471 6211.321
a[3]  -1.0342572 0.22131590 -1.386284 -0.6978819 1.0055189 7582.040
a[4]  -2.0709445 0.17232416 -2.344571 -1.8048372 1.0018769 7737.139
a[5]  -1.6652852 0.10917209 -1.843045 -1.4929220 1.0008096 8222.782
a[6]  -0.9665822 0.21280819 -1.321655 -0.6331661 1.0018789 7843.107
a[7]  -1.9959387 0.05771253 -2.088617 -1.9050973 1.0011837 7728.764
a[8]  -1.2333670 0.03077213 -1.283215 -1.1843374 1.0018560 7970.212
a[9]  -2.0940402 0.08441923 -2.229120 -1.9612160 1.0021398 7691.047
a[10] -1.5119485 0.16788140 -1.784377 -1.2514335 1.0007121 8855.145
a[11] -1.8576891 0.12918364 -2.070923 -1.6559798 1.0006128 6163.319
a[12] -1.9845692 0.08399404 -2.116270 -1.8524929 0.9997404 7579.841
a[13] -1.9869779 0.08449475 -2.122158 -1.8508171 1.0003681 7640.337
a[14] -2.3194188 0.13072921 -2.535589 -2.1162523 0.9997666 6925.518
a[15] -1.9274970 0.14012400 -2.158931 -1.7079577 1.0013178 8719.327
a[16] -1.8455882 0.13355100 -2.062231 -1.6362775 1.0003532 6867.620
a[17] -1.5237600 0.19096777 -1.837233 -1.2197103 1.0023969 8204.902
a[18] -1.8202975 0.09301672 -1.969644 -1.6692408 1.0010253 7240.314
a[19] -1.6129974 0.06745050 -1.721365 -1.5059544 1.0022999 7161.826
a[20] -1.8891864 0.18379497 -2.187272 -1.6045716 1.0030093 5961.345
a[21] -2.0048625 0.06114496 -2.102639 -1.9076953 1.0013959 7449.357
a[22] -2.0686634 0.15290291 -2.323887 -1.8288554 1.0000568 7318.014
a[23] -2.1247134 0.11527162 -2.312969 -1.9419937 0.9992737 6812.747
a[24] -1.3610795 0.18727865 -1.668678 -1.0637716 1.0001245 7086.540
a[25] -1.3896508 0.20734215 -1.727209 -1.0678349 1.0004817 6835.311
a[26] -1.4441824 0.21694429 -1.803177 -1.1019916 1.0014460 6927.386
a[27] -1.2300393 0.17172276 -1.503511 -0.9611483 1.0028507 8268.693
a[28] -1.7786803 0.18301728 -2.079170 -1.4915760 1.0002015 6547.875
a[29] -1.7326612 0.15648419 -1.988353 -1.4874537 1.0018926 8563.170
a[30] -1.6751064 0.08619447 -1.810338 -1.5365980 1.0007565 8748.391
a[31] -1.7814530 0.09230770 -1.932546 -1.6383398 1.0003032 7385.056
a[32] -1.9404150 0.07669595 -2.061967 -1.8190425 1.0002850 7143.350
a[33] -1.7621453 0.10887353 -1.940410 -1.5911283 1.0008124 6761.275
a[34] -1.4221094 0.07653852 -1.542241 -1.2994819 1.0000605 7665.293
a[35] -1.2215675 0.23107769 -1.598119 -0.8616259 1.0026594 6833.457
a[36] -1.0627140 0.20192857 -1.388622 -0.7428927 0.9996175 8486.014
a[37] -2.0648693 0.02910255 -2.111139 -2.0182224 1.0028476 8207.589
a[38] -1.9141140 0.04220190 -1.982377 -1.8485725 1.0015849 6328.718
trankplot(mWE_noml)

Waiting to draw page 2 of 6

Waiting to draw page 3 of 6

Waiting to draw page 4 of 6

Waiting to draw page 5 of 6

Waiting to draw page 6 of 6

Looks good!

Visualize estimates

To actually understand the results, let’s visualize them. First, we extract and transform the estimates to probability scale so that we can understand them more intuitively.

post <- extract.samples(mWE_noml)
data$propweekend_est <- logistic(apply(post$a, 2, mean))

Now we’ll display the raw proportions of weekend submissions in each country (blue dots) and then add the posterior mean (black circles) and 95% intervals.

Code
plot(
  data$propweekend,
  ylim = c(0, 0.4),
  pch = 16,
  xaxt = "n",
  xlab = "",
  ylab = "proportion weekend submissions",
  col = "cornflowerblue"
)
points(data$propweekend_est)
axis(1, at = c(1:38), labels = data$country_name, las = 2, cex.axis = 0.7)

# 95% interval per country from posterior samples
ci_mat <- apply(post$a, 2, quantile, probs = c(0.025, 0.975))
ci_prob <- logistic(ci_mat)  # transform to probability scale

segments(
  x0 = 1:38,
  y0 = ci_prob[1, ],
  x1 = 1:38,
  y1 = ci_prob[2, ],
  col = rgb(0, 0, 0, 0.3),
  lwd = 1.5
)

# Global mean posterior estimate
abline(
  h = mean(data$propweekend_est),
  col = rgb(0, 0, 0, 0.3),
  lty = 2
)

Code
country_max <- data$L[data$propweekend_est == max(data$propweekend_est)]
country_name_max <- unique(data_orig$country_name[data_orig$L == country_max])

It looks like the posterior mean estimates are similar for each country compared to the raw average. Cameroon has the hightest posterior mean proportion of weekend submissions with 27.6 %.

Part 2 – Partial-pooling model

Statistical Model

Now it’s time for the second part. We’ll use a partial-pooling model to estimate the probability for weekend submissions. This means that the model has a hierarchichal prior for each countries estimate which makes it learn from the other countries and lowers the risk for overfitting the model to our specific dataset (which might contain weird data).

\[ \text{W}_i \sim \text{Binomial}(N_\text{submissions[L]}, p_\text{L}) \] \[ \text{logit}(p_\text{L}) = a_\text{L[i]} \] \[ a_\text{L} \sim \text{Normal}(\bar{a}, \sigma) \] \[ \bar{a} \sim \text{Normal}(0, 1.5) \] \[ \sigma \sim \text{Exponential}(1) \]

We use the same model as above and just add a prior for \(\sigma\), the heterogeneity of the countries, so that the model figures this out itself instead of just giving this a fixed value. We use \(\text{Exponential}(1)\) as a prior for sigma, because it needs to be positive and just often works like this. Let’s see how our model does now!

Fit Model

mWE_ml <- ulam(
  alist(
    W_count ~ dbinom(N, p),
    logit(p) <- a[L],
    a[L] ~ dnorm(a_bar, sigma),
    a_bar ~ dnorm(0, 1.5),
    sigma ~ dexp(1)
  ),
  data = data,
  chains = 4,
  iter = 2000,
  log_lik = TRUE
)
show(mWE_ml)
Hamiltonian Monte Carlo approximation
4000 samples from 4 chains

Sampling durations (seconds):
  chain_id warmup sampling total
1        1   0.14     0.11  0.25
2        2   0.12     0.11  0.24
3        3   0.12     0.11  0.23
4        4   0.12     0.11  0.23

Formula:
W_count ~ dbinom(N, p)
logit(p) <- a[L]
a[L] ~ dnorm(a_bar, sigma)
a_bar ~ dnorm(0, 1.5)
sigma ~ dexp(1)

Diagnostics

Some quick diagnostics. You can find a short explanation of what this dashboard shows in the previous diagnostics part.

dashboard(mWE_noml)

NoteParameter estimate information & trankplots

Here you can see the precis() output and all the trankplots.

precis(mWE_ml, depth = 2)
            mean         sd       5.5%     94.5%      rhat  ess_bulk
a[1]  -2.0353296 0.05471957 -2.1224759 -1.949745 1.0015085 11137.507
a[2]  -1.7118147 0.16027017 -1.9664317 -1.459049 1.0014914  8002.633
a[3]  -1.3577500 0.19092452 -1.6592635 -1.047591 1.0016127  6084.969
a[4]  -2.0323622 0.14709663 -2.2695465 -1.798087 0.9999152  7991.489
a[5]  -1.6979137 0.10533859 -1.8710055 -1.533505 1.0007879  9608.255
a[6]  -1.3214515 0.19329729 -1.6252659 -1.014511 1.0014634  8391.813
a[7]  -1.9925841 0.05657678 -2.0822989 -1.902119 1.0005647  9235.826
a[8]  -1.2398053 0.02943347 -1.2877293 -1.194058 0.9996066  8115.109
a[9]  -2.0806473 0.08033530 -2.2117976 -1.952297 1.0008541  9932.700
a[10] -1.6163271 0.15316364 -1.8604815 -1.374721 1.0004411 10102.911
a[11] -1.8668296 0.12103833 -2.0650702 -1.677639 1.0027653 10200.880
a[12] -1.9799755 0.08133122 -2.1112722 -1.851123 1.0012261  9230.825
a[13] -1.9812202 0.08170183 -2.1113315 -1.850799 1.0038400  8946.729
a[14] -2.2588588 0.12028086 -2.4520514 -2.072949 1.0005549  8649.628
a[15] -1.9250353 0.12296404 -2.1270394 -1.729441 1.0004140  8444.949
a[16] -1.8577332 0.12526192 -2.0591091 -1.659622 1.0021016  9995.498
a[17] -1.6381032 0.16853926 -1.9095709 -1.377923 1.0006418  9202.441
a[18] -1.8295078 0.08582838 -1.9671543 -1.695396 1.0014950  9141.960
a[19] -1.6291567 0.06804433 -1.7373064 -1.522724 1.0022109  9074.743
a[20] -1.8944015 0.15844760 -2.1486531 -1.646801 1.0005704  9396.503
a[21] -1.9999146 0.05825094 -2.0933247 -1.907735 1.0010412  8065.423
a[22] -2.0387415 0.13500406 -2.2569183 -1.827974 1.0000930  8638.716
a[23] -2.1001161 0.10801250 -2.2779718 -1.925314 1.0009559 11040.477
a[24] -1.5250683 0.17010885 -1.7957448 -1.255719 1.0012843  9451.993
a[25] -1.5626880 0.17441684 -1.8436424 -1.285690 1.0006541  7776.681
a[26] -1.6184898 0.17555075 -1.9060454 -1.342120 1.0015004  8370.716
a[27] -1.4145232 0.16353910 -1.6805754 -1.149854 1.0003179  8484.259
a[28] -1.8173224 0.15713905 -2.0700592 -1.569737 1.0005983  7284.220
a[29] -1.7765321 0.14939574 -2.0221034 -1.540885 1.0009304  9265.367
a[30] -1.6948025 0.08714031 -1.8333487 -1.555636 1.0036040 10051.612
a[31] -1.7950218 0.09245141 -1.9468747 -1.651596 1.0014695  9145.634
a[32] -1.9365178 0.07522182 -2.0590644 -1.816251 1.0026882  9796.077
a[33] -1.7823195 0.10357517 -1.9465220 -1.616037 0.9999343 10420.150
a[34] -1.4562259 0.07732643 -1.5808568 -1.331170 1.0021525  9073.230
a[35] -1.4943146 0.19370081 -1.8127979 -1.184689 1.0024914  7488.467
a[36] -1.3469589 0.18194873 -1.6411375 -1.055692 0.9997490  7615.766
a[37] -2.0629382 0.02963912 -2.1111915 -2.015696 0.9995055  9244.818
a[38] -1.9138169 0.04285013 -1.9823831 -1.845162 1.0001517  8224.930
a_bar -1.7686479 0.05280701 -1.8515473 -1.681888 1.0029003  6126.377
sigma  0.2910326 0.04511974  0.2256897  0.369855 1.0008641  3942.927
trankplot(mWE_ml)

Waiting to draw page 2 of 6

Waiting to draw page 3 of 6

Waiting to draw page 4 of 6

Waiting to draw page 5 of 6

Waiting to draw page 6 of 6

Visualize estimates

Now we’ll extract and transform the estimates to probability again so we can understand them more intuitively.

post_mWE_ml <- extract.samples(mWE_ml)
data$propweekend_est_mWE_ml <- logistic(apply(post_mWE_ml$a, 2, mean)) # convert to probability & average over all samples

Now we’ll display the raw proportions of weekend submissions and posterior the posterior means for each country again, this time from out partial pooling model.

Code
plot(
  data$propweekend,
  ylim = c(0, 0.4),
  pch = 16,
  xaxt = "n",
  xlab = "",
  ylab = "proportion weekend submissions",
  col = "cornflowerblue"
)

points(data$propweekend_est_mWE_ml)
axis(1, at = c(1:38), labels = data$country_name, las = 2, cex.axis = 0.7)

# 95% interval per country from posterior samples
ci_mat <- apply(post_mWE_ml$a, 2, quantile, probs = c(0.025, 0.975))
ci_prob <- logistic(ci_mat)  # transform to probability scale

segments(
  x0 = 1:38,
  y0 = ci_prob[1, ],
  x1 = 1:38,
  y1 = ci_prob[2, ],
  col = rgb(0, 0, 0, 0.3),
  lwd = 1.5
)

# Global posterior mean estimate
abline(
  h = mean(data$propweekend_est_mWE_ml),
  col = rgb(0, 0, 0, 0.3),
  lty = 2
)

Code
country_max_mWE_ml <- data$L[data$propweekend_est_mWE_ml == max(data$propweekend_est_mWE_ml)]
country_name_max_mWE_ml <- unique(data_orig$country_name[data_orig$L == country_max_mWE_ml])

Now, China has the hightest posterior mean proportion of weekend submissions with 22.4 %.

Why not Cameroon? It seems that some of the posterior mean estimates shrunk towards a global mean, while some others kind of stay where they were before. Why is that?
The width of the 95% intervals gives us a hint: While for Bangladesh, Cameroon, Thailand and some other countries they are quite wide; they are very narrow for Australia, China, the UK and the US. Let’s look at the sample size (number of total submissions) we have for each country.

Code
data <- as_tibble(data)
data$country_name <- fct_reorder(data$country_name, data$N)

data |>
  tidyplot(x = country_name, y = N) |>
  add_sum_bar() |>
  add_sum_value(accuracy = 1) |>
  adjust_colors(new_colors = "cornflowerblue") |>
  adjust_x_axis(rotate_labels = 90) |>
  adjust_y_axis_title("number of total submissions", fontsize = 13) |>
  remove_x_axis_title() |>
  adjust_size(220, 90) |>
  adjust_font(9) |>
  adjust_theme_details(
    panel.border = ggplot2::element_rect(colour = "black", fill = NA, linewidth = 0.5),
    panel.background = ggplot2::element_rect(fill = NA),
    plot.background = ggplot2::element_rect(fill = NA, colour = NA)
  ) |>
  adjust_padding(top = 0.05, bottom = 0.01)

Okay, that makes sense! The range of total submissions is huge: from 101 in Cameroon to 11,448 in the UK.

Our model is a lot more confident about estimates for countries with many submissions and shrinks estimates toward the global mean if there are relatively little submissions from that country. This is cool, because data with a small N could totally be biased by outliers, like a couple of researchers who for whatever reason submit their papers exclusively on weekends. Pooling is great!