Applied Informatics and Computational Economics Lab
5 July 2026
Required Packages
library(tidyverse) # data wrangling and ggplot2library(brms) # Bayesian multilevel models via Stan (NUTS)library(rstan) # the Stan engine brms compiles tolibrary(bayesplot) # trace plots, rank plots, posterior predictive checkslibrary(loo) # LOO-CV / WAIC model comparisonlibrary(coda) # classical MCMC diagnosticslibrary(lme4) # lmer() — frequentist multilevel benchmarklibrary(plm) # within / random-effects panel estimators
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport pymc as pm # NUTS sampler, PyMC >= 5import arviz as az # posterior diagnostics and plotsimport statsmodels.api as smfrom linearmodels import PanelOLS, RandomEffects
* Everything used here is built-in Stata (16+):* bayes: mixed Bayesian multilevel / hierarchical models* bayesstats summary posterior summaries* bayesgraph trace / density / autocorrelation diagnostics* mixed, xtreg frequentist benchmarks* No ssc installs required forthis deck.
About This Deck
Part I: Foundations — why panel heterogeneity is a Bayesian problem; the pooling spectrum and shrinkage
Part II: A known-truth laboratory — a simulated hierarchical panel where every parameter is known
Part III: Estimation — the same random-intercept, random-slope model in brms, PyMC and bayes: mixed; recovering the truth; shrinkage in action
Part IV: Priors, convergence & model checking — variance-component priors, R-hat/ESS, posterior predictive checks, LOO/WAIC
Part V: Real data I — random-coefficient investment equations on the Grunfeld firm panel
Part VI: Real data II — the union wage premium in the Vella–Verbeek panel; Bayesian vs frequentist FE/RE
Part VII: Bayesian testing — posterior probabilities, Savage–Dickey ratios, ROPE
All computational slides run in , and ; R generates every shared dataset (bayesian-hp-data.R)
Swamy, P. A. V. B. (1970). Efficient inference in a random coefficient regression model. Econometrica, 38(2), 311–323. DOI: 10.2307/1913012
Mundlak, Y. (1978). On the pooling of time series and cross section data. Econometrica, 46(1), 69–85. DOI: 10.2307/1913646
Gelman, A., & Hill, J. (2007). Data Analysis Using Regression and Multilevel/Hierarchical Models. Cambridge University Press. DOI: 10.1017/CBO9780511790942
Gelman, A. (2006). Prior distributions for variance parameters in hierarchical models. Bayesian Analysis, 1(3), 515–534. DOI: 10.1214/06-BA117A
Rossi, P. E., Allenby, G. M., & McCulloch, R. (2005). Bayesian Statistics and Marketing. Wiley. DOI: 10.1002/0470863692
Bürkner, P.-C. (2017). brms: An R package for Bayesian multilevel models using Stan. Journal of Statistical Software, 80(1), 1–28. DOI: 10.18637/jss.v080.i01
Salvatier, J., Wiecki, T. V., & Fonnesbeck, C. (2016). Probabilistic programming in Python using PyMC3. PeerJ Computer Science, 2, e55. DOI: 10.7717/peerj-cs.55
Vehtari, A., Gelman, A., & Gabry, J. (2017). Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC. Statistics and Computing, 27, 1413–1432. DOI: 10.1007/s11222-016-9696-4
Betancourt, M., & Girolami, M. (2015). Hamiltonian Monte Carlo for hierarchical models. arXiv: 1312.0906
A panel follows \(N\) units (firms, workers, countries) over \(T\) periods:
\[
y_{it} = \alpha_i + \beta_i\, x_{it} + \varepsilon_{it}, \qquad i = 1, \dots, N, \quad t = 1, \dots, T
\]
Units are heterogeneous: each firm has its own baseline \(\alpha_i\) and possibly its own response \(\beta_i\)
\(T\) is usually small (5–20 periods) — each unit gives only a handful of observations
The core question of panel econometrics: how much should unit \(i\) borrow from the other units?
Classical panel econometrics forces a corner solution:
Pooled OLS — pretend \(\alpha_i = \alpha\) for all \(i\): biased if heterogeneity is real
Fixed effects — estimate every \(\alpha_i\) separately: unbiased, but each \(\hat\alpha_i\) uses only \(T\) observations, so it is noisy; time-invariant covariates drop out
Random effects — assume \(\alpha_i \perp x_{it}\): efficient, but inconsistent when the assumption fails (Hausman test)
Unit-by-unit OLS for \(\beta_i\) with \(T = 10\)? Standard errors so wide the estimates are nearly useless
Each choice is all-or-nothing: pool completely, or not at all.
Treat the unit-specific parameters as draws from a population distribution:
The data decide how much to pool: if units look alike (\(\tau\) small), estimates are pulled together; if they differ (\(\tau\) large), each unit keeps its own estimate
Partial pooling is a continuum between pooled OLS and fixed effects — chosen by the likelihood, not by the researcher
Full posterior distributions for every quantity: unit effects, hyperparameters, predictions
Works even when some units have very few observations — they simply borrow more strength
The posterior is a full distribution, not a point estimate — credible intervals, tail probabilities and predictions all come from it directly
Modern samplers (Hamiltonian Monte Carlo / NUTS) draw from \(p(\theta \mid y)\) even when it has thousands of dimensions — exactly what a panel with \(N\) unit effects requires
A hierarchical (multilevel) model stacks priors in levels:
When is the hierarchy justified? When units are exchangeable: before seeing the data, no unit is special — the joint distribution of \((\alpha_1, \dots, \alpha_N)\) is invariant to relabelling.
By de Finetti’s theorem, exchangeable effects behave as if drawn iid from some common distribution — precisely the Level-2 assumption
Exchangeability fails when a unit is known a priori to differ (e.g. one country in a currency union, one giant firm) — then add covariates at Level 2 or group units
Note the parallel with random effects: RE assumes \(\alpha_i \perp x_{it}\); the Bayesian hierarchy makes the same assumption unless we model the correlation (Mundlak device — see Variations)
“Bayesian random effects” does not fix endogeneity
The hierarchical prior \(\alpha_i \sim \mathcal{N}(\mu_\alpha, \tau_\alpha^2)\) plays the same structural role as the frequentist random-effects assumption. If the unit effects are correlated with the regressors, the posterior for \(\mu_\beta\) is centred on the wrong value, no matter how well the chains mix. Bayesians handle this the same way Mundlak (1978) did: include the unit means \(\bar{x}_i\) as a Level-2 covariate, turning the model into a correlated-random-effects specification.
\(\tau_\alpha^2\) large (units genuinely differ) \(\Rightarrow\)\(w_i \to 1\): trust the unit’s own data — approaches fixed effects
\(\tau_\alpha^2\) small (units alike) \(\Rightarrow\)\(w_i \to 0\): pool towards \(\mu_\alpha\) — approaches pooled OLS
\(T_i\) large \(\Rightarrow\)\(w_i \to 1\): with enough own data, no borrowing is needed
Crucially, \(\tau_\alpha\) and \(\sigma\) are estimated from the data — the data choose the position on the spectrum
Efficiency: partial pooling dominates no pooling in mean-squared error whenever the hierarchy holds — the multilevel generalisation of the James–Stein result
Small groups: units with tiny \(T_i\) get stabilised instead of producing wild estimates
Prediction for new units: the population distribution \(\mathcal{N}(\mu_\alpha, \tau_\alpha^2)\)is the forecast for a unit never seen before — fixed effects has no answer here
Shrinkage trades a little bias for a lot of variance
Yes, the partial-pooling estimate of any single \(\alpha_i\) is biased towards \(\mu_\alpha\). But its variance is so much smaller than the no-pooling estimate’s that the mean squared error across all units falls — often dramatically when \(T\) is small. In the known-truth laboratory of Part III you will see this: the shrunken estimates sit closer to the true \(\beta_i\) than the unit-by-unit OLS estimates they were shrunk from.
All three engines use MCMC; R and Python use Hamiltonian Monte Carlo / NUTS, which follows the posterior’s gradient — the method of choice for hierarchies with hundreds of parameters
Stata’s bayes: mixed uses adaptive Metropolis–Hastings with Gibbs updates for variance components
We run 2 chains with modest draw counts so slides render quickly; for research use 4 chains and 2,000+ post-warmup draws each
Every fit is checked with \(\widehat{R}\), effective sample size and posterior predictive checks (Part IV)
Part II: A Known-Truth Laboratory
A simulated hierarchical panel where every parameter is known
Because we generate the data, we know every quantity real data hides: the population means \((\mu_\alpha, \mu_\beta) = (2.0, 1.5)\), the heterogeneity scales \((\tau_\alpha, \tau_\beta) = (0.8, 0.5)\), the noise \(\sigma = 1\), and each unit’s individual \((\alpha_i, \beta_i)\).
Parameter
True value
Role
\(\mu_\alpha\)
2.0
population mean intercept
\(\mu_\beta\)
1.5
population mean slope
\(\tau_\alpha\)
0.8
between-unit SD of intercepts
\(\tau_\beta\)
0.5
between-unit SD of slopes
\(\sigma\)
1.0
idiosyncratic noise SD
With only \(T = 10\) observations per unit, unit-by-unit OLS slopes have \(\text{SE} \approx 1/\sqrt{10} \approx 0.32\) — noisier than the true heterogeneity\(\tau_\beta = 0.5\) they try to measure
This is exactly the regime where partial pooling pays
Real panels never reveal whether shrinkage helped — a simulated panel with stored true \((\alpha_i, \beta_i)\) does
We can compare no pooling vs partial pooling against the truth, unit by unit
Same discipline as any Monte Carlo study: verify the tools on known ground before touching real data (Parts V–VI)
DGP — Code Implementation
Code
# Full script: bayesian-hp-data.R (creates all shared datasets for this deck)set.seed(14159)N <-40T_len <-10alpha_i <-rnorm(N, mean =2.0, sd =0.8)beta_i <-rnorm(N, mean =1.5, sd =0.5)sim <-data.frame()for (i in1:N) { x <-rnorm(T_len) y <- alpha_i[i] + beta_i[i] * x +rnorm(T_len, sd =1) sim <-rbind(sim, data.frame(id = i, t =1:T_len, x = x, y = y))}write.csv(sim, "../data/bayes-hp-sim.csv", row.names =FALSE)truth <-data.frame(id =1:N, alpha_true = alpha_i, beta_true = beta_i)write.csv(truth, "../data/bayes-hp-sim-truth.csv", row.names =FALSE)
Wrote ../data/bayes-hp-sim.csv : 400 rows
Wrote ../data/bayes-hp-sim-truth.csv : 40 rows
Sample moments of the true unit effects:
mean(alpha_i) = 2.024 sd(alpha_i) = 0.764
mean(beta_i) = 1.554 sd(beta_i) = 0.433
id t x y
1 1 1 -0.3772990 1.9302405
2 1 2 -0.3999774 2.8976870
3 1 3 -0.9804264 -1.3598365
4 1 4 0.8865424 2.9882610
5 1 5 -0.9528133 0.6517793
print(f"true effects stored for {len(truth)} units")
true effects stored for 40 units
Code
import delimited "../data/bayes-hp-sim.csv", clearquietlydestring_all, replacextset id txtsumy x
(encoding automatically selected: ISO-8859-1)
(4 vars, 400 obs)
Panel variable: id (strongly balanced)
Time variable: t, 1 to 10
Delta: 1 unit
Variable | Mean Std. dev. Min Max | Observations
-----------------+--------------------------------------------+----------------
y overall | 1.752178 1.987413 -3.813023 7.160384 | N = 400
between | .9107732 -.2584071 3.808481 | n = 40
within | 1.771727 -2.611013 7.37585 | T = 10
| |
x overall | -.0914653 1.017296 -3.112968 2.972849 | N = 400
between | .3382568 -.8498171 1.009444 | n = 40
within | .9607573 -3.097251 2.539685 | T = 10
DGP Diagnostics — Seeing the Heterogeneity
Code
sim <-read.csv("../data/bayes-hp-sim.csv")# left: every unit's own regression line; right: unit-by-unit OLS slopesp1 <-ggplot(sim, aes(x = x, y = y, group = id)) +geom_point(color ="#185FA5", alpha =0.25, size =1) +geom_smooth(method ="lm", se =FALSE, color ="#D85A30",linewidth =0.4, alpha =0.6) +labs(x ="x", y ="y", title ="40 units, 40 different lines",subtitle ="Each red line = OLS fit on one unit's 10 observations") + theme_lectureslopes <-numeric(40)for (i in1:40) slopes[i] <-coef(lm(y ~ x, data = sim[sim$id == i, ]))[2]p2 <-ggplot(data.frame(slope = slopes), aes(x = slope)) +geom_histogram(bins =15, fill ="#185FA5", color ="white") +geom_vline(xintercept =1.5, color ="#D85A30", linewidth =1.2) +labs(x ="Unit-by-unit OLS slope", y ="Count",title ="No-pooling slope estimates",subtitle ="Red line = true population mean 1.5") + theme_lecturep1 + p2
Code
import pandas as pdimport numpy as npimport matplotlib.pyplot as pltsim = pd.read_csv("../data/bayes-hp-sim.csv")fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))for i, g in sim.groupby("id"): b, a = np.polyfit(g["x"], g["y"], 1) xs = np.linspace(g["x"].min(), g["x"].max(), 2) axes[0].plot(xs, a + b * xs, color="#D85A30", lw=0.6, alpha=0.6)axes[0].scatter(sim["x"], sim["y"], color="#185FA5", s=6, alpha=0.25)axes[0].set_title("40 units, 40 different lines")axes[0].set_xlabel("x"); axes[0].set_ylabel("y")slopes = [np.polyfit(g["x"], g["y"], 1)[0] for _, g in sim.groupby("id")]axes[1].hist(slopes, bins=15, color="#185FA5", edgecolor="white")axes[1].axvline(1.5, color="#D85A30", lw=2.5)axes[1].set_title("No-pooling slopes (red = true mean 1.5)")axes[1].set_xlabel("Unit-by-unit OLS slope")plt.tight_layout(); plt.show()
Code
import delimited "../data/bayes-hp-sim.csv", clearquietlydestring_all, replace* between vs within variation, then a spaghetti plot of 8 unitsxtset id txtsumytwoway (liney t if id <= 8, connect(ascending) lcolor(navy%70)), ///by(id, rows(2) note("")) xtitle("t") ytitle("y") ///title("Eight units from the simulated panel")
(encoding automatically selected: ISO-8859-1)
(4 vars, 400 obs)
Panel variable: id (strongly balanced)
Time variable: t, 1 to 10
Delta: 1 unit
Variable | Mean Std. dev. Min Max | Observations
-----------------+--------------------------------------------+----------------
y overall | 1.752178 1.987413 -3.813023 7.160384 | N = 400
between | .9107732 -.2584071 3.808481 | n = 40
within | 1.771727 -2.611013 7.37585 | T = 10
Part III: Estimation
Three engines, one posterior — and shrinkage you can see
import delimited "../data/bayes-hp-sim.csv", clearquietlydestring_all, replace* Bayesian multilevel model: random intercept and random slope by id.* Default priors: N(0, 10000) on coefficients, InvGamma(0.01, 0.01) on variances.bayes, rseed(14159) mcmcsize(2500) burnin(2500): /// mixed y x || id: x, covariance(independent)
(encoding automatically selected: ISO-8859-1)
(4 vars, 400 obs)
note: Gibbs sampling is used for regression coefficients and variance
components.
Burn-in 2500 aaaaaaaaa1000aaaaaaaaa2000aaaaa done
Simulation 2500 .........1000.........2000..... done
Multilevel structure
------------------------------------------------------------------------------
id
{U0}: random intercepts
{U1}: random coefficients for x
------------------------------------------------------------------------------
Model summary
------------------------------------------------------------------------------
Likelihood:
y ~ normal(xb_y,{e.y:sigma2})
Priors:
{y:x _cons} ~ normal(0,10000) (1)
{U0} ~ normal(0,{U0:sigma2}) (1)
{U1} ~ normal(0,{U1:sigma2}) (1)
{e.y:sigma2} ~ igamma(.01,.01)
Hyperpriors:
{U0:sigma2} ~ igamma(.01,.01)
{U1:sigma2} ~ igamma(.01,.01)
------------------------------------------------------------------------------
(1) Parameters are elements of the linear form xb_y.
Bayesian multilevel regression MCMC iterations = 5,000
Metropolis–Hastings and Gibbs sampling Burn-in = 2,500
MCMC sample size = 2,500
Group variable: id Number of groups = 40
Obs per group:
min = 10
avg = 10.0
max = 10
Number of obs = 400
Acceptance rate = .751
Efficiency: min = .03683
avg = .1406
Log marginal-likelihood max = .3888
------------------------------------------------------------------------------
| Equal-tailed
| Mean Std. dev. MCSE Median [95% cred. interval]
-------------+----------------------------------------------------------------
y |
x | 1.493348 .0919236 .009482 1.496154 1.307406 1.664654
_cons | 1.868538 .1268979 .013225 1.863426 1.634415 2.124479
-------------+----------------------------------------------------------------
id |
U0:sigma2 | .5237633 .1519542 .007177 .5050353 .2872438 .8889867
U1:sigma2 | .1868057 .0746167 .006068 .1744262 .0765228 .3536002
-------------+----------------------------------------------------------------
e.y |
sigma2 | 1.05824 .0832196 .002669 1.054196 .9028264 1.230749
------------------------------------------------------------------------------
Note: Default priors are used for model parameters.
Every 95% credible interval covers the true value, and the REML point estimates agree closely — with \(N = 40\) units the hyperprior’s influence is minimal.
For every unit we now have two slope estimates and — uniquely, because this is simulated data — the true\(\beta_i\):
No pooling: OLS on each unit’s 10 observations
Partial pooling: the hierarchical posterior mean, shrunk towards \(\mu_\beta\)
If shrinkage works, the partial-pooling estimates should sit closer to the 45° line against the truth, and their RMSE across the 40 units should be smaller.
Code
truth <-read.csv("../data/bayes-hp-sim-truth.csv")# no-pooling slopes (one OLS per unit) vs hierarchical posterior meansslopes_np <-numeric(40)for (i in1:40) slopes_np[i] <-coef(lm(y ~ x, data = sim[sim$id == i, ]))[2]slopes_pp <-coef(fit_rs)$id[, "Estimate", "x"]df_shr <-data.frame(truth =rep(truth$beta_true, 2),est =c(slopes_np, slopes_pp),method =rep(c("No pooling (unit OLS)", "Partial pooling (posterior mean)"),each =40))rmse_np <-sqrt(mean((slopes_np - truth$beta_true)^2))rmse_pp <-sqrt(mean((slopes_pp - truth$beta_true)^2))ggplot(df_shr, aes(x = truth, y = est, color = method)) +geom_abline(slope =1, intercept =0, linetype ="dashed", color ="grey55") +geom_hline(yintercept =1.5, linetype ="dotted", color ="grey55") +geom_point(size =2.6, alpha =0.85) +scale_color_manual(values =c("#D85A30", "#1D9E75"), name =NULL) +labs(x ="True beta_i", y ="Estimated beta_i",title ="Partial pooling pulls noisy unit slopes towards the truth",subtitle =sprintf("RMSE across 40 units: no pooling = %.3f partial pooling = %.3f", rmse_np, rmse_pp)) + theme_lecture
Code
import numpy as npimport pandas as pdimport matplotlib.pyplot as plttruth = pd.read_csv("../data/bayes-hp-sim-truth.csv")b_pp = tr_rs.posterior["b"].mean(dim=["chain", "draw"]).valuesb_np = np.array([np.polyfit(g["x"], g["y"], 1)[0] for _, g in sim.groupby("id")])bt = truth["beta_true"].valuesfig, ax = plt.subplots(figsize=(9, 4.6))ax.axline((1, 1), slope=1, ls="--", color="grey")ax.axhline(1.5, ls=":", color="grey")ax.scatter(bt, b_np, color="#D85A30", s=35, alpha=0.85, label=f"No pooling (RMSE = {np.sqrt(np.mean((b_np - bt)**2)):.3f})")ax.scatter(bt, b_pp, color="#1D9E75", s=35, alpha=0.85, label=f"Partial pooling (RMSE = {np.sqrt(np.mean((b_pp - bt)**2)):.3f})")ax.set_xlabel("True beta_i"); ax.set_ylabel("Estimated beta_i")ax.set_title("Partial pooling pulls noisy unit slopes towards the truth")ax.legend()plt.tight_layout(); plt.show()
Part IV: Priors, Convergence & Model Checking
Trust nothing until the chains, the priors and the predictions check out
The variance components \(\tau_\alpha, \tau_\beta\) are the most prior-sensitive parameters in the model — they sit at the top of the hierarchy with only \(N\) “observations” (the unit effects) informing them.
Bad: \(\text{InvGamma}(\epsilon, \epsilon)\) on \(\tau^2\) — spikes near zero, forces over-pooling when \(N\) is small, and is not non-informative despite its reputation
Good: half-normal or half-\(t\) directly on the SD \(\tau\) — flat near zero, gentle decay (Gelman 2006)
Correlations: \(\text{LKJ}(\eta)\) with \(\eta = 2\) mildly shrinks \(|\rho|\) away from 1 — with 10–40 units the correlation is weakly identified and needs this regularisation
Rule of thumb: simulate from your priors (prior predictive) and check the implied data are merely plausible, not absurd
Code
prior_summary(fit_rs)
prior class coef group resp dpar nlpar lb ub tag
normal(0, 5) b
normal(0, 5) b x
normal(0, 5) Intercept
lkj_corr_cholesky(2) L
lkj_corr_cholesky(2) L id
normal(0, 2) sd 0
normal(0, 2) sd id 0
normal(0, 2) sd Intercept id 0
normal(0, 2) sd x id 0
normal(0, 2) sigma 0
source
user
(vectorized)
user
user
(vectorized)
user
(vectorized)
(vectorized)
(vectorized)
user
* bayes: mixed default priors (used on the previous slides):* {y:x y:_cons} ~ normal(0, 10000) regression coefficients* {var(U0) var(U1)} ~ igamma(0.01, 0.01) random-effect variances* {var(e.y)} ~ igamma(0.01, 0.01) residual variance* Custom priors are set with the prior() option, e.g.:bayes, prior({y:x}, normal(0, 25)) /// prior({var(U0)}, igamma(2, 1)): /// mixed y x || id: x, covariance(independent)
Stata keeps inverse-gamma variance priors for conjugacy (Gibbs updates). With \(N = 40\) units this is harmless here — but re-run Part V’s 10-firm model with igamma(2, 1) and watch the firm-level variances move: small panels are prior-sensitive.
\(\widehat{R}\) (potential scale reduction): compares between-chain and within-chain variance; require \(\widehat{R} < 1.01\) for every parameter
Effective sample size: MCMC draws are autocorrelated; ESS is the equivalent number of independent draws — worry below ~400 (bulk) for means, and check tail ESS for intervals
Trace plots: chains should look like “fat hairy caterpillars” overlapping completely
Divergent transitions (HMC only): even a handful signals the sampler could not follow the posterior geometry — reparameterise (non-centered) or tighten priors, never just ignore
\[
\widehat{R} = \sqrt{\frac{\widehat{\text{Var}}^{+}(\theta \mid y)}{W}}, \qquad
\widehat{\text{Var}}^{+} = \frac{n-1}{n} W + \frac{1}{n} B
\]
Overlay the density of \(y\) on densities of many \(y^{\text{rep}}\) draws — systematic gaps mean model misfit
Compare test statistics: if the observed statistic sits in the tails of its replicated distribution, that feature of the data is not captured
PPCs test adequacy, not truth — a model can pass and still omit the economics you care about
Code
p1 <-pp_check(fit_rs, ndraws =50) +labs(title ="Observed y (dark) vs 50 posterior replications") + theme_lecturep2 <-pp_check(fit_rs, type ="stat", stat ="sd", ndraws =500) +labs(title ="Replicated SD vs observed SD") + theme_lecturep1 + p2
Code
import pymc as pmimport arviz as azimport matplotlib.pyplot as pltwith m_rs: ppc = pm.sample_posterior_predictive(tr_rs, random_seed=14159, progressbar=False)az.plot_ppc(ppc, num_pp_samples=50, figsize=(9, 4))plt.title("Observed y vs 50 posterior replications")plt.tight_layout(); plt.show()
Which pooling level does the data prefer? Compare models by out-of-sample predictive accuracy, estimated without refitting via Pareto-smoothed importance sampling (Vehtari, Gelman & Gabry 2017):
The most re-used panel in econometrics (Kleiber & Zeileis 2010): 10 large US firms observed annually 1935–1954.
\[
\text{inv}_{it} = \text{gross investment}, \qquad
\text{value}_{it} = \text{market value of the firm}, \qquad
\text{capital}_{it} = \text{capital stock}
\]
The units range from General Motors to Diamond Match — five orders of magnitude apart in size, so firm heterogeneity is not a nuisance, it is the point
Only \(N = 10\) firms: exactly the small-\(N\) regime where variance components are hard to pin down and priors matter
We model the standardised variables (inv_s, value_s, capital_s, computed once in bayesian-hp-data.R) so all three engines share identical scales and priors
inv value capital
General Motors 608.0 4333.8 648.4
US Steel 410.5 1971.8 294.9
General Electric 102.3 1941.3 400.2
Chrysler 86.1 693.2 121.2
Atlantic Refining 61.8 231.5 486.8
IBM 55.4 419.9 104.3
Union Oil 47.6 149.8 314.9
Westinghouse 42.9 670.9 85.6
Goodyear 41.9 333.6 297.9
Diamond Match 3.1 70.9 5.9
Code
import delimited "../data/bayes-hp-grunfeld.csv", clearquietlydestring_all, replacextset firm yearxtsuminvvalue capital
(encoding automatically selected: ISO-8859-1)
(8 vars, 200 obs)
Panel variable: firm (strongly balanced)
Time variable: year, 1935 to 1954
Delta: 1 unit
Variable | Mean Std. dev. Min Max | Observations
-----------------+--------------------------------------------+----------------
inv overall | 145.9583 216.8753 .93 1486.7 | N = 200
between | 198.8242 3.0845 608.02 | n = 10
within | 106.1986 -204.3617 1024.638 | T = 20
| |
value overall | 1081.681 1314.47 58.12 6241.7 | N = 200
between | 1334.917 70.921 4333.845 | n = 10
within | 340.5421 -459.964 2989.536 | T = 20
| |
capital overall | 276.0172 301.1039 .8 2226.3 | N = 200
between | 200.9701 5.9415 648.435 | n = 10
within | 232.6603 -369.6179 1853.882 | T = 20
(encoding automatically selected: ISO-8859-1)
(8 vars, 200 obs)
note: Gibbs sampling is used for regression coefficients and variance
components.
Burn-in 2500 aaaaaaaaa1000aaaaaaaaa2000aaaaa done
Simulation 2500 .........1000.........2000..... done
Multilevel structure
------------------------------------------------------------------------------
firm
{U0}: random intercepts
{U1}: random coefficients for value_s
{U2}: random coefficients for capital_s
------------------------------------------------------------------------------
Model summary
------------------------------------------------------------------------------
Likelihood:
inv_s ~ normal(xb_inv_s,{e.inv_s:sigma2})
Priors:
{inv_s:value_s capital_s _cons} ~ normal(0,10000) (1)
{U0} ~ normal(0,{U0:sigma2}) (1)
{U1} ~ normal(0,{U1:sigma2}) (1)
{U2} ~ normal(0,{U2:sigma2}) (1)
{e.inv_s:sigma2} ~ igamma(.01,.01)
Hyperpriors:
{U0:sigma2} ~ igamma(.01,.01)
{U1:sigma2} ~ igamma(.01,.01)
{U2:sigma2} ~ igamma(.01,.01)
------------------------------------------------------------------------------
(1) Parameters are elements of the linear form xb_inv_s.
Bayesian multilevel regression MCMC iterations = 5,000
Metropolis–Hastings and Gibbs sampling Burn-in = 2,500
MCMC sample size = 2,500
Group variable: firm Number of groups = 10
Obs per group:
min = 20
avg = 20.0
max = 20
Number of obs = 200
Acceptance rate = .7424
Efficiency: min = .003359
avg = .0939
Log marginal-likelihood max = .5143
------------------------------------------------------------------------------
| Equal-tailed
| Mean Std. dev. MCSE Median [95% cred. interval]
-------------+----------------------------------------------------------------
inv_s |
value_s | .5474894 .1272203 .043903 .5610818 .2952109 .7524473
capital_s | .3313063 .1665682 .05176 .3443998 -.0840036 .5842809
_cons | -.0649826 .134059 .036025 -.0582597 -.3278946 .1706248
-------------+----------------------------------------------------------------
firm |
U0:sigma2 | .1178473 .07983 .006285 .0980914 .0350814 .3267856
U1:sigma2 | .1379313 .1075998 .010385 .1099134 .0272493 .3978944
U2:sigma2 | .0773361 .0660366 .008817 .056464 .0158845 .2500928
-------------+----------------------------------------------------------------
e.inv_s |
sigma2 | .0405176 .0084391 .000235 .0394661 .0322807 .0530224
------------------------------------------------------------------------------
Note: Default priors are used for model parameters.
Note: There is a high autocorrelation after 500 lags.
Firm-Level Posteriors — Who Invests Differently?
Code
ce <-coef(fit_grun)$firm[, , "value_s"]df_cat <-data.frame(firm = firm_names, est = ce[, "Estimate"],lo = ce[, "Q2.5"], hi = ce[, "Q97.5"])df_cat <- df_cat[order(df_cat$est), ]df_cat$firm <-factor(df_cat$firm, levels = df_cat$firm)ggplot(df_cat, aes(x = est, y = firm)) +geom_vline(xintercept =fixef(fit_grun)["value_s", "Estimate"],color ="#D85A30", linewidth =1) +geom_pointrange(aes(xmin = lo, xmax = hi), color ="#185FA5",linewidth =0.9, size =0.6) +labs(x ="Firm-specific coefficient on market value (standardised)",y =NULL, title ="Posterior 95% intervals for the value coefficient",subtitle ="Red line = population mean; intervals shrink towards it") + theme_lecture
Code
import arviz as azimport matplotlib.pyplot as pltnames = ["General Motors", "US Steel", "General Electric", "Chrysler","Atlantic Refining", "IBM", "Union Oil", "Westinghouse","Goodyear", "Diamond Match"]bval = tr_grun.posterior["coefs"].sel(coefs_dim_0=1) # value_s coefficientsfig, ax = plt.subplots(figsize=(9, 4.6))means = bval.mean(dim=["chain", "draw"]).valueslo = bval.quantile(0.025, dim=["chain", "draw"]).valueshi = bval.quantile(0.975, dim=["chain", "draw"]).valuesorder = means.argsort()ax.axvline(tr_grun.posterior["mu"].sel(mu_dim_0=1).mean(), color="#D85A30", lw=2)ax.hlines(range(len(names)), lo[order], hi[order], color="#185FA5", lw=2)ax.plot(means[order], range(len(names)), "o", color="#185FA5", ms=6)ax.set_yticks(range(len(names)))ax.set_yticklabels([names[i] for i in order])ax.set_xlabel("Firm-specific coefficient on market value (standardised)")ax.set_title("Posterior 95% intervals (red = population mean)")plt.tight_layout(); plt.show()
Part VI: Real Data II — Union Wages
The union premium in the Vella–Verbeek young-men panel
(encoding automatically selected: ISO-8859-1)
(10 vars, 1,200 obs)
Panel variable: nr (strongly balanced)
Time variable: year, 1980 to 1987
Delta: 1 unit
Summary for variables: lwage
Group variable: union
union | Mean SD N
---------+------------------------------
0 | 1.655 0.478 921.000
1 | 1.846 0.466 279.000
---------+------------------------------
Total | 1.700 0.482 1200.000
----------------------------------------
This is the probability the hypothesis is true given data and prior — the quantity a \(p\)-value is so often mistaken for
Computed by counting MCMC draws: trivially easy, valid in finite samples
For a point null \(H_0\!: \beta = 0\) nested in \(H_1\), the Bayes factor equals the Savage–Dickey density ratio — prior density over posterior density, both evaluated at zero:
Posterior mass moved away from zero \(\Rightarrow\)\(BF_{01} < 1\): evidence against the null
Bayes factors depend heavily on the prior width — always report the prior alongside a Bayes factor
Requires prior draws, hence sample_prior = "yes" in the wage model
Point nulls are rarely economically interesting. Define a Region of Practical Equivalence — effects too small to matter — and compute its posterior mass:
ROPE mass near 0 \(\Rightarrow\) the effect is practically relevant; near 1 \(\Rightarrow\) practically null
Forces the researcher to state what effect size matters economically — a feature, not a burden
Testing the Union Premium
Code
# directional: P(beta_union > 0 | y); Evid.Ratio = posterior oddsh1 <-hypothesis(fit_wage, "union > 0")print(h1)# Savage-Dickey: BF for the point null beta_union = 0h0 <-hypothesis(fit_wage, "union = 0")print(h0)cat(sprintf("BF01 = %.4f => BF10 = %.1f : strong evidence for a positive premium\n", h0$hypothesis$Evid.Ratio, 1/ h0$hypothesis$Evid.Ratio))
Hypothesis Tests for class b:
Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob Star
1 (union) > 0 0.12 0.03 0.07 0.17 Inf 1 *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.
Hypothesis Tests for class b:
Hypothesis Estimate Est.Error CI.Lower CI.Upper Evid.Ratio Post.Prob Star
1 (union) = 0 0.12 0.03 0.06 0.18 0 0 *
---
'CI': 90%-CI for one-sided and 95%-CI for two-sided hypotheses.
'*': For one-sided hypotheses, the posterior probability exceeds 95%;
for two-sided hypotheses, the value tested against lies outside the 95%-CI.
Posterior probabilities of point hypotheses assume equal prior probabilities.
BF01 = 0.0000 => BF10 = 129173.6 : strong evidence for a positive premium
Code
import numpy as npbu = tr_wage.posterior["beta"].sel(coef="union").values.flatten()print(f"Posterior mean union premium : {bu.mean():.3f}")
import delimited "../data/bayes-hp-wagepan.csv", clearquietlydestring_all, replacequietly bayes, rseed(14159) mcmcsize(2500) burnin(2500) /// prior({lwage:union married exper expersq educ black hisp}, normal(0, 0.25)): /// mixed lwage union married exper expersq educ black hisp || nr:* posterior probability of a positive premium, and of the ROPE |beta| < 0.02bayestest interval {lwage:union}, lower(0)bayestest interval {lwage:union}, lower(-0.02) upper(0.02)
brms: y ~ x + (1 | id) + (1 | year) — a partial-pooling version of two-way fixed effects
Shrinking year effects is natural for macro shocks of similar magnitude
The hierarchical prior is ridge regularisation with an estimated penalty:
MAP estimate of \(\alpha_i\) under \(\mathcal{N}(\mu_\alpha, \tau_\alpha^2)\) = ridge with penalty \(\sigma^2 / \tau_\alpha^2\)
But Bayes learns the penalty from the data and propagates its uncertainty
Bridge to the machine-learning-regularisation deck: same mathematics, different tuning philosophy
Exercises — Estimation
Re-run the known-truth laboratory keeping only the first \(T = 4\) periods of each unit. Recompute the no-pooling vs partial-pooling RMSE for the slopes. How much bigger is the gain from shrinkage than at \(T = 10\)? Relate your answer to the weight \(w_i = \tau_\beta^2 / (\tau_\beta^2 + \sigma^2 \cdot c_i)\).
The PyMC model on the estimation slide assumes independent \(\alpha_i, \beta_i\). Rewrite it with pm.LKJCholeskyCov so the unit effects are correlated, and compare the posterior correlation with the cor(Intercept, x) estimate reported by brms.
Prior sensitivity on a small panel: refit the Grunfeld model with \(\tau \sim \text{half-Cauchy}(0, 1)\) instead of half-normal, and again with Stata’s default \(\text{InvGamma}(0.01, 0.01)\). Tabulate the three posteriors of \(\tau_{\beta_1}\). Which prior forces the most pooling?
Apply the Mundlak device to the wage model: add person means of union and married as regressors. Does the posterior for the union premium move towards the fixed-effects estimate from the comparison slide?
Give each worker his own union premium: lwage ~ ... + (1 + union | nr). Report the posterior of \(\tau_{\text{union}}\) and the 5 workers with the largest and smallest premiums. Is the heterogeneity economically meaningful?
Posterior prediction for a new unit: using the simulated-panel posterior, draw \((\alpha_{41}, \beta_{41})\) from the population distribution and generate \(\tilde{y}\) for 10 fresh periods. Verify by repetition that the 95% predictive intervals cover the new unit’s data about 95% of the time.
Delete 40% of the simulated panel’s rows at random (an unbalanced panel — no changes to the code are needed). Show that units with fewer remaining observations are shrunk harder, by plotting \(|\hat\beta_i^{PP} - \hat\beta_i^{NP}|\) against \(T_i\).
Exercises — Testing
Compute \(\widehat{R}\) for \(\mu_\beta\) by hand from the two chains of the simulated-panel fit (split each chain in half first, following current practice) and confirm it matches the reported value.
Rewrite the PyMC laboratory model in its centered parameterisation (\(\alpha_i \sim \mathcal{N}(\mu_\alpha, \tau_\alpha)\) sampled directly). Count divergent transitions in both versions, then shrink the data to \(N = 40, T = 3\) and count again. Explain the funnel.
Reproduce the Savage–Dickey Bayes factor for the union premium by hand: kernel-density-estimate the prior and posterior draws of \(\beta_{\text{union}}\) at zero and take the ratio. Compare with hypothesis().
Use LOO to compare the wage model with random intercepts only against the random-union-slope model of Exercise 5. Is the extra heterogeneity worth its predictive cost?
Run a grouped posterior predictive check on the Grunfeld model: pp_check(fit_grun, type = "stat_grouped", group = "firm", stat = "sd"). Which firm’s volatility does the model miss most badly, and why? (Look at the 1935–1954 history of that firm.)
In Stata, use bayestest model to compare the wage model under the \(\mathcal{N}(0, 0.25)\) coefficient prior against a \(\mathcal{N}(0, 10000)\) prior. Interpret the posterior model probabilities — what do they say about prior choice, and why should you not use them as a substitute for LOO?
Testing \(\tau_\beta = 0\) (no slope heterogeneity) is a boundary problem: simulate 50 panels with \(\tau_\beta = 0\), fit the hierarchical model to each, and record the posterior 5% quantile of \(\tau_\beta\). How often is it far from zero? Explain why credible intervals for variance components never contain zero, and what to report instead.
Gelman, A., & Hill, J. (2007). Data Analysis Using Regression and Multilevel/Hierarchical Models. Cambridge University Press. DOI: 10.1017/CBO9780511790942
Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., & Rubin, D. B. (2013). Bayesian Data Analysis, 3rd ed. CRC Press. DOI: 10.1201/b16018
Swamy, P. A. V. B. (1970). Efficient inference in a random coefficient regression model. Econometrica, 38(2), 311–323. DOI: 10.2307/1913012
Mundlak, Y. (1978). On the pooling of time series and cross section data. Econometrica, 46(1), 69–85. DOI: 10.2307/1913646
Gelman, A. (2006). Prior distributions for variance parameters in hierarchical models. Bayesian Analysis, 1(3), 515–534. DOI: 10.1214/06-BA117A
Vehtari, A., Gelman, A., & Gabry, J. (2017). Practical Bayesian model evaluation using LOO-CV and WAIC. Statistics and Computing, 27, 1413–1432. DOI: 10.1007/s11222-016-9696-4
Lewandowski, D., Kurowicka, D., & Joe, H. (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis, 100(9), 1989–2001. DOI: 10.1016/j.jmva.2009.04.008
Betancourt, M., & Girolami, M. (2015). Hamiltonian Monte Carlo for hierarchical models. arXiv: 1312.0906