Gelman, Hill and Vehtari, Regression and Other Stories, in StatsPAI¶
Regression and Other Stories (Cambridge University Press, 2020) teaches
regression as a workflow. Fit a model, simulate from it, check the
simulations against the data, compare models by how well they predict,
and only then read the coefficients. Active Statistics (Gelman and
Vehtari, 2024) is its companion of classroom stories, activities and
problems. The two books share one set of examples, the
ROS-Examples repository,
written in R around rstanarm::stan_glm, loo and arm.
This guide maps that workflow to StatsPAI, says where StatsPAI follows a different convention and why, and lists what has changed in practice since the book was printed.
The book's data are not redistributed with StatsPAI. The examples below read them from a local copy of ROS-Examples.
The workflow in six calls¶
import pandas as pd
import statspai as sp
kidiq = pd.read_csv("ROS-Examples/KidIQ/data/kidiq.csv")
fit = sp.bayes_regress("kid_score ~ mom_hs + mom_iq", kidiq,
prior="weakly_informative", chains=4,
draws=2000, burnin=1000, seed=1)
fit.summary() # posterior mean, sd, interval, ESS
sp.ppc(fit, stat="min", seed=1) # can the model produce data like these?
sp.loo(fit) # expected log predictive density
sp.bayes_r2(fit); sp.loo_r2(fit) # variance explained, in and out of sample
small = sp.bayes_regress("kid_score ~ mom_hs", kidiq,
prior="weakly_informative", chains=4,
draws=2000, burnin=1000, seed=1)
sp.loo_compare({"hs + iq": fit, "hs": small})
sp.bayes_regress is NumPy only. The fit above takes a fraction of a
second, against several seconds of compilation and sampling in Stan,
which matters when the activity is "fit it twenty times with different
fake data".
Chapter by chapter¶
| chapters | the book runs | StatsPAI |
|---|---|---|
| 4, 5 | simulation of sampling distributions, quantile, mad |
NumPy; sp.mad_sd |
| 6 to 8 | stan_glm(y ~ x), lm |
sp.bayes_regress(prior='weakly_informative'); sp.regress |
| 9 | posterior_linpred, posterior_epred, posterior_predict |
methods of the same names on the fit |
| 9 | informative priors, prior = normal(...) |
prior_mean=, prior_var= |
| 10 | indicators, interactions, factor(x), y ~ . |
the same formulas; factor() and . are understood |
| 11 | residual plots, pp_check, ppc_stat |
sp.ppc(fit, stat=...), .plot(kind='density') |
| 11 | bayes_R2, loo_R2 |
sp.bayes_r2, sp.loo_r2 |
| 11 | loo, loo_compare, kfold |
sp.loo, sp.loo_compare, sp.kfold |
| 12 | logs, arm::standardize, rescaling by two standard deviations |
formulas with log(); sp.standardize |
| 12 | horseshoe prior hs() on many predictors |
sp.bayes_shrink(prior='horseshoe', p0=, slab_scale=); family='logit' for a binary outcome |
| 13, 14 | stan_glm(family = binomial), invlogit |
sp.bayes_regress(model='logit'); sp.logit; sp.invlogit |
| 14 | average predictive comparisons | sp.margins, sp.margins_at |
| 14 | binnedplot |
sp.binned_residuals, sp.binned_residuals_plot |
| 14 | separation and weakly informative priors | ConvergenceWarning from sp.logit; prior='weakly_informative' |
| 15 | Poisson and negative binomial with offset, overdispersion |
sp.bayes_regress(model='poisson' / 'negbin', exposure=); sp.poisson, sp.nbreg; sp.glm(family='quasipoisson') |
| 15 | cbind(y, n - y) ~ x |
the same formula in sp.glm(family='binomial') and sp.bayes_regress(model='logit') |
| 15 | probit, ordered logit and probit (stan_polr), robit |
sp.probit, sp.ologit, sp.oprobit, sp.bayes_regress(model='ologit' / 'oprobit'); sp.glm(link='robit(4)') |
| 15 | tobit, mixed discrete and continuous outcomes | sp.tobit, sp.bayes_regress(model='tobit'); two fits and posterior_predict |
| 16 | sample size, power, type S and type M errors | sp.power_ttest, sp.power_two_proportions; sp.retrodesign |
| 17 | poststratification | sp.poststratify(fit, cells, count='N') |
| 17 | missing data imputation | sp.mice, sp.mi_estimate |
| 18, 19 | randomized experiments, regression adjustment | sp.regress, sp.lm_lin |
| 20 | propensity scores, matching, weighting, balance | sp.pscore, sp.match, sp.ipw, sp.balance_table, sp.svydesign(...).glm |
| 21 | instrumental variables, regression discontinuity, fixed effects | sp.ivreg, sp.rdrobust, sp.panel |
| 22 | multilevel models (pointer to the next book) | sp.mixed, sp.bayes_mixed |
Priors on the scale of the data¶
The book's first regression predicts the incumbent party's vote share from economic growth, sixteen elections.
hibbs = pd.read_csv("ROS-Examples/ElectionsEconomy/data/hibbs.dat", sep=r"\s+")
fit = sp.bayes_regress("vote ~ growth", hibbs, prior="weakly_informative",
chains=4, draws=5000, seed=1)
fit.draws.median() # Intercept 46.3, growth 3.0
sp.mad_sd(fit.draws) # 1.7 and 0.7, the numbers printed in the book
prior="weakly_informative" reproduces the default of rstanarm. Each
slope gets a normal prior with standard deviation 2.5 sd(y) / sd(x).
The intercept gets N(mean(y), 2.5 sd(y)) with the regressors
centred, which is a correlated prior on the raw coefficients. The
residual standard deviation gets an exponential prior with rate
1 / sd(y). For logit, probit, Poisson and negative binomial models
sd(y) is replaced by 1 and mean(y) by 0.
Under the fixed prior of sp.bayes_regress (prior='vague', variance
1000 on every coefficient) the same fit puts the intercept at 46.1 and says so in a
warning. A prior of fixed scale is tight for a coefficient measured in
large units. The weakly informative prior does not depend on units.
Since StatsPAI 1.40 the weakly informative prior is what an unnamed
prior gives for the models that have one (it was the fixed prior
before). Write prior='vague' for the fixed prior. A fit that sets
prior_mean or prior_var uses the prior it sets.
The prior scales are checked against prior_summary() of rstanarm
to nine digits. The sampler is checked against the exact posterior on a
grid.
Predictive accuracy¶
sp.loo implements Pareto smoothed importance sampling as in Vehtari,
Simpson, Gelman, Yao and Gabry (2024). Given the same matrix of
pointwise log-likelihoods it returns the pointwise elpd, p, Pareto
k and effective sample sizes of R loo 2.9.0 to nine digits or
better. Three things differ from the book's text, all because the
method moved on after 2020.
- The threshold on Pareto
kdepends on the number of draws,min(1 - 1 / log10(S), 0.7). It is 0.7 from about 2,200 draws on and lower for shorter chains. The book's fixed 0.7 is the long-chain limit. - A difference in
elpdbelow about 4 is small whatever its standard error. With fewer than about 100 observations the normal approximation behindse_diffis itself unreliable. posterior_linpred(transform = TRUE)is gone fromrstanarm. The expected outcome isposterior_epred, and that is the name here.
sp.loo works on any draws-by-observations matrix, so it also serves a
PyMC trace or draws produced elsewhere. sp.kfold refits the model and
is the answer when Pareto k values are high or whole groups must be
held out together (sp.kfold_split(n, k, groups=...)).
On the conjugate normal model, where leave-one-out has a closed form,
sp.loo and sp.kfold both reproduce the exact pointwise values.
Average predictive comparisons and average treatment effects¶
Chapter 14 compares households 100 metres from a safe well with
households next to one, averaging the difference in the probability of
switching over the sample. Chapter 19 does the same with a treatment
indicator in a model with interactions. With a Bayesian fit both are the
difference of two calls to posterior_epred, which gives the comparison
for every posterior draw and so its uncertainty.
wells = pd.read_csv("ROS-Examples/Arsenic/data/wells.csv")
fit = sp.bayes_regress("switch ~ dist100 + arsenic + educ4", wells,
model="logit", prior="weakly_informative",
draws=4000, seed=1)
diff = (fit.posterior_epred(wells.assign(dist100=1.0))
- fit.posterior_epred(wells.assign(dist100=0.0)))
apc = diff.mean(axis=1) # one average comparison per draw
apc.mean(), apc.std() # -0.205, 0.023
The maximum-likelihood counterpart is sp.margins_at(sp.logit(...),
wells, at={"dist100": [0, 1]}), whose two margins differ by -0.204 with
a delta-method standard error close to the posterior one. For a
treatment z in a model with z:x interactions, replace the two data
frames by df.assign(z=1) and df.assign(z=0); the row means are draws
of the sample average treatment effect.
What StatsPAI does differently, on purpose¶
Collinear regressors. The child care example of chapter 20 puts all
the race indicators and all the education indicators in the propensity
score model next to the constant. R drops one of each set and reports
NA. StatsPAI omits the later member of each dependent set, like Stata,
and names it in a note and in model_info['omitted']. Until this pass
that was true of sp.regress only. The likelihood-based estimators
returned coefficients of order 1e13 or standard errors of zero, often
silently. sp.logit, sp.probit, sp.cloglog, sp.glm, sp.poisson,
sp.nbreg, sp.ologit, sp.oprobit, sp.mlogit, sp.tobit,
sp.qreg and survey regression now share one omission step.
Separation. sp.logit and sp.glm warn when the linear index
separates the outcome. The earlier detector also required 99 percent of
fitted probabilities to be within 0.01 of the boundary and stayed silent
on small samples while the slope ran past 1,000.
Observed and expected information. For a link that is not canonical
(probit, cloglog, robit) sp.glm and sp.probit report standard errors
from the observed information, as Stata does. R glm uses the expected
information. On the wells data the two differ by 2 percent for probit
and 9 percent for cloglog. sp.glm(..., information='expected') gives R's
numbers.
Negative binomial standard errors. sp.nbreg inverts the joint
information of the coefficients and the dispersion (Stata, statsmodels).
MASS::glm.nb holds the dispersion fixed and uses expected information.
On the roaches data, which are very overdispersed, R's standard error
for roach1 is 0.16 and the joint one is 0.25. The Bayesian fit gives
0.25.
Design analysis with estimated standard errors.
sp.retrodesign(effect, se, dof=) treats the test as a t-test, so
power and the sign error rate are noncentral-t probabilities and the
exaggeration ratio integrates over the estimated standard error. The
function printed in Gelman and Carlin (2014) shifts a central t instead.
With effect 0.5, standard error 1 and 20 degrees of freedom the
exaggeration ratio is 4.63 exactly and 5.18 by the shifted
approximation. A simulated t-test agrees with the first. Pass
method='shifted' for the published formula. With a known standard
error (no dof) there is one answer and it matches the R package
retrodesign to machine precision.
Standardizing transformed terms. arm::standardize rescales x
inside log(x), which changes the model. sp.standardize(df,
formula=...) rescales the columns named in the formula and leaves
transformed terms alone. Create the transformed column first and
standardize that.
Binned residuals where the outcome hardly varies. The default band is
2 sd / sqrt(n) of the residuals in a bin, as in arm. In a bin where
every outcome is 1 the residuals are all tiny and nearly equal, so the
band collapses and the bin is flagged. band='model' uses the standard
error the fitted probabilities imply, 2 sqrt(sum p (1 - p)) / n, which
does not collapse.
Bayesian R-squared for counts. rstanarm::bayes_R2 stops at
Gaussian and binomial models. sp.bayes_r2 applies the same definition,
variance of the fit over variance of the fit plus the residual variance
the model expects, to Poisson (mu) and negative binomial
(mu + alpha mu^2) fits.
What the book leaves to Stan and is not here¶
- User-written Stan programs, such as the geometry-based golf putting
model.
sp.nlsfits nonlinear least squares andsp.abcsimulation models, but there is no general probabilistic programming layer in the NumPy samplers. The PyMC-based estimators are instatspai.bayes. stan_gamm4(splines inside a Bayesian fit).sp.gamis the penalized-likelihood counterpart.- The prior of
stan_polr, which is stated on the R-squared of the latent regression.sp.bayes_regress(model='ologit')puts a normal prior on the coefficients and on the log distances between cutpoints. - NUTS. The logit, ordered logit, Poisson and negative binomial models
are sampled by Metropolis with two proposals, a random walk and an
independence proposal centred at the posterior mode. On the book's
examples the effective sample size is a third to a half of the draws.
A posterior far from normal (very few observations, near separation)
gets less; the acceptance rate of the independence proposal is in
fit._extras['independence_accept']. - Shrinkage priors for count outcomes.
sp.bayes_shrinkcovers Gaussian and binary (family='logit') outcomes.
Reproducing the checks¶
# reference values on a committed synthetic file (needs R with loo,
# rstanarm, arm, retrodesign)
Rscript tests/reference_parity/_fixtures/_generate_regression_stories_R.R
pytest tests/reference_parity/test_regression_stories_r_parity.py
pytest tests/reference_parity/test_bayes_workflow_exact_posterior.py
# the book's own data
export STATSPAI_ROS_DIR=/path/to/ROS-Examples
Rscript tests/external_parity/gelman_ros_reference.R
pytest tests/external_parity/test_gelman_ros_examples.py
References¶
Gelman, A., Hill, J. and Vehtari, A. (2020). Regression and Other
Stories. Cambridge University Press. gelman2020regression
Gelman, A. and Vehtari, A. (2024). Active Statistics: Stories, Games,
Problems, and Hands-on Demonstrations for Applied Regression and Causal
Inference. Cambridge University Press. gelman2024active
The methods are cited by key in the docstrings: vehtari2017practical,
vehtari2024pareto, watanabe2010asymptotic, zhang2009new,
gelman2019rsquared, gabry2019visualization, gelman2008weakly,
gelman2008scaling, gelman2014beyond, gelman2006data,
liu2004robit, carvalho2010horseshoe, piironen2017sparsity,
makalic2016simple. sp.bibtex(keys=[...]) returns the entries.