CmdStanPHP

Bayesian school effects

This example fits the school effects model, score ~ study_hours + (1 | school), as a Bayesian model in Stan and compares the posterior with the Phit\fit_lmer() estimates. CmdStanPHP builds the Stan data from the same formula through phit, then runs CmdStan.

Compiling and sampling a Stan model takes far longer than a page request, so the page does not run CmdStan. It reads draws from a seeded fit that is stored in the repository. The command php artisan stan:fit-school-posterior regenerates them, and only that command needs CmdStan.

Code

$df = DataFrame::fromQuery(StudentScore::query());
$data = StanData::fromFormula('score ~ study_hours + (1 | school)', $df);
// N, K, X (with the intercept column), y, J, and school_id

$model = cmdstan_model('school_intercepts.stan');
$fit = $model->sample(data: $data, seed: 20261016, chains: 4, thin: 4);
$draws = $fit->drawsColumns(['beta', 'sigma', 'tau', 'school_intercept']);
Stan program
// score ~ study_hours + (1 | school), with a non-centered random intercept.
// Data come from CmdStan\StanData::fromFormula(); X includes the intercept column.
data {
  int<lower=0> N;
  int<lower=1> K;
  matrix[N, K] X;
  vector[N] y;
  int<lower=1> J;
  array[N] int<lower=1, upper=J> school_id;
}
parameters {
  vector[K] beta;
  real<lower=0> sigma;
  real<lower=0> tau;
  vector[J] z;
}
transformed parameters {
  vector[J] u = tau * z;
}
model {
  beta[1] ~ normal(50, 25);
  beta[2:K] ~ normal(0, 10);
  sigma ~ student_t(3, 0, 10);
  tau ~ student_t(3, 0, 10);
  z ~ std_normal();
  y ~ normal(X * beta + u[school_id], sigma);
}
generated quantities {
  vector[J] school_intercept = beta[1] + u;
}

The priors are weakly informative: normal(50, 25) on the intercept, normal(0, 10) on the slope, and half-Student-t(3, 0, 10) on σ and τ. The random intercepts are non-centered.

Parameters

Parameter Posterior mean Posterior SD 95% interval fit_lmer (REML)
(Intercept) 56.483 1.668 [53.126, 59.680] 56.493
study_hours 2.341 0.108 [2.131, 2.561] 2.343
Residual SD (σ) 5.412 0.243 [4.959, 5.932] 5.394
School SD (τ) 5.030 1.250 [3.111, 8.040] 4.568

School intercepts

Blue points and lines are posterior medians and 95% intervals. Red diamonds are the fit_lmer intercepts (fixed intercept plus the conditional mode of the random effect).

Between-school SD

The histogram shows the posterior draws of τ. The red line is the REML estimate. With 12 schools, the posterior for τ is wide and skewed to the right, which a single point estimate does not show.

The stored fit

CmdStan 2.40.0, seed 20261016, 4 chains of 1,000 iterations after 1,000 warmup, thinned by 4 to 1,000 draws. The largest R-hat is 1.008, the smallest bulk effective sample size is 481, and there were 0 divergent transitions.