Stan Coding Guidelines
Use this skill when writing or modifying Stan programs to ensure clean, efficient code.
Program Structure
Use canonical block order: functions, data, transformed data, parameters, transformed parameters, model, generated quantities.
Follow Stan style:
- Two-space indents, no tabs, ≤80 character lines
- Opening braces at end of line:
for (n in 1:N) {
- Spaces around operators and after commas
- Variable names: lowercase with underscores (
sigma_y, mu_group)
- Dimension constants: single uppercase letters (
N, K, J)
- Declare locals close to use; scalars inside loops, reused containers outside
Types and Containers
Use appropriate types:
- Linear algebra:
matrix, vector, row_vector with matrix operations (x * beta)
- Indexing/containers:
array[N] real y (not legacy real y[N])
- Repeated row access:
array[M] row_vector[N] x over matrix[M, N]
- Heterogeneous returns:
tuple(...) for multiple values
- Sum-to-zero:
sum_to_zero_vector, sum_to_zero_matrix instead of manual constraints
Memory layout: matrices are column-major, arrays are row-major.
Distributions and Vectorization
Always use log form:
- Write
y ~ normal(mu, sigma) or target += normal_lpdf(y | mu, sigma)
- Vectorize:
y ~ normal(mu, sigma) for arrays, not loops
- Use GLM functions:
bernoulli_logit_glm, poisson_log_glm, normal_id_glm
- Precompute shared expressions: compute
mu = X * beta once, reuse
- Finite mixtures: use
log_sum_exp on log scale
Parameterization
Use constrained types over manual checks:
<lower=0>, <upper=...>, ordered, positive_ordered, simplex, unit_vector
- Covariance (K≥3):
cholesky_factor_corr[K] L_Omega with multi_normal_cholesky
- Sum-to-zero: use built-in types, not "last element = minus sum"
For custom transforms, use built-in *_constrain, *_unconstrain, *_jacobian functions.
Parallelization
For large-N models with independent terms, use reduce_sum:
- Write partial sum function that takes data slice and returns log-density contribution
- Keep partial sum vectorized internally
- No side effects (no printing, no mutation)
Functions
Modularize complex logic in functions block:
- Reused operations, complex math, custom likelihoods
- Signature: data arguments first, then parameters, then tuning constants
- Use
tuple returns for multiple heterogeneous outputs
Preventing Crashes
Both compilation and sampling can crash or OOM.
Defensive Stan patterns:
- Always use tight bounds:
int<lower=1, upper=K> id[N]
- Guard math: check parameters before
log, sqrt, division
- Add explicit bounds for dispersion:
real<lower=0.01> phi (never exactly 0)
Execution:
- Wrap
CmdStanModel() and model.sample() in try-except
- Probe with short runs before full sampling
On crash/OOM:
- Reduce
parallel_chains (4 → 2 → 1)
- Reduce
max_treedepth (10 → 8)
- Subsample data or simplify model
ArviZ Integration
Design Stan programs for downstream ArviZ workflow:
Generated quantities:
- Always include pointwise log-likelihood:
vector[N] log_lik - required for model comparison and downstream workflow
- Always include posterior predictive draws:
vector[N] y_rep - required for all predictive checks
- For multiple observed variables, use one vector per variable:
log_lik_y1, log_lik_y2
- This will incur modest overhead, but might be worth workflow simplicity
Transformed parameters:
- Put reusable intermediate quantities here (e.g.,
vector[N] mu = alpha + X * beta)
- Avoids recomputation in Python and makes them available in posterior samples
Extending without refitting:
- To add new derived quantities, use
generate_quantities mode with original posterior draws
- Write new Stan file with same data/parameters/transformed parameters but extended generated quantities
- Call
model.generate_quantities(data=data, mcmc_sample=fit) - orders of magnitude faster than refitting
Save and cache:
- Convert to InferenceData:
az.from_cmdstanpy(fit, log_likelihood="log_lik", posterior_predictive=["y_rep"])
- Save as NetCDF:
idata.to_netcdf("posterior.nc") - makes all downstream analysis instant
- Use consistent coords/dims for all models in the workflow
Known Issues
- CmdStanPy
diagnose() OOMs on large data (N > 10K). Use check_convergence() from shared_utils instead.
- ArviZ column names are lowercase (
r_hat, ess_bulk). CmdStanPy uses uppercase (R_hat, ESS_bulk).
- Stan CSV columns use dots:
beta.1 not beta[1].
References
If stuck on Stan patterns or ArviZ usage, search these resources:
Use WebSearch or WebFetch to find specific examples.
1---2name: stan-coding3description: Best practices for writing efficient, clean Stan programs4---5
6# Stan Coding Guidelines
7
8Use this skill when writing or modifying Stan programs to ensure clean, efficient code.
9
10## Program Structure
11
12Use canonical block order: `functions`, `data`, `transformed data`, `parameters`, `transformed parameters`, `model`, `generated quantities`.
13
14Follow Stan style:
15- Two-space indents, no tabs, ≤80 character lines
16- Opening braces at end of line: `for (n in 1:N) {`
17- Spaces around operators and after commas
18- Variable names: lowercase with underscores (`sigma_y`, `mu_group`)
19- Dimension constants: single uppercase letters (`N`, `K`, `J`)
20- Declare locals close to use; scalars inside loops, reused containers outside
21
22## Types and Containers
23
24Use appropriate types:
25- Linear algebra: `matrix`, `vector`, `row_vector` with matrix operations (`x * beta`)
26- Indexing/containers: `array[N] real y` (not legacy `real y[N]`)
27- Repeated row access: `array[M] row_vector[N] x` over `matrix[M, N]`
28- Heterogeneous returns: `tuple(...)` for multiple values
29- Sum-to-zero: `sum_to_zero_vector`, `sum_to_zero_matrix` instead of manual constraints
30
31Memory layout: matrices are column-major, arrays are row-major.
32
33## Distributions and Vectorization
34
35Always use log form:
36- Write `y ~ normal(mu, sigma)` or `target += normal_lpdf(y | mu, sigma)`
37- Vectorize: `y ~ normal(mu, sigma)` for arrays, not loops
38- Use GLM functions: `bernoulli_logit_glm`, `poisson_log_glm`, `normal_id_glm`
39- Precompute shared expressions: compute `mu = X * beta` once, reuse
40- Finite mixtures: use `log_sum_exp` on log scale
41
42## Parameterization
43
44Use constrained types over manual checks:
45- `<lower=0>`, `<upper=...>`, `ordered`, `positive_ordered`, `simplex`, `unit_vector`
46- Covariance (K≥3): `cholesky_factor_corr[K] L_Omega` with `multi_normal_cholesky`
47- Sum-to-zero: use built-in types, not "last element = minus sum"
48
49For custom transforms, use built-in `*_constrain`, `*_unconstrain`, `*_jacobian` functions.
50
51## Parallelization
52
53For large-N models with independent terms, use `reduce_sum`:
54- Write partial sum function that takes data slice and returns log-density contribution
55- Keep partial sum vectorized internally
56- No side effects (no printing, no mutation)
57
58## Functions
59
60Modularize complex logic in `functions` block:
61- Reused operations, complex math, custom likelihoods
62- Signature: data arguments first, then parameters, then tuning constants
63- Use `tuple` returns for multiple heterogeneous outputs
64
65## Preventing Crashes
66
67Both compilation and sampling can crash or OOM.
68
69**Defensive Stan patterns:**
70- Always use tight bounds: `int<lower=1, upper=K> id[N]`
71- Guard math: check parameters before `log`, `sqrt`, division
72- Add explicit bounds for dispersion: `real<lower=0.01> phi` (never exactly 0)
73
74**Execution:**
75- Wrap `CmdStanModel()` and `model.sample()` in try-except
76- Probe with short runs before full sampling
77
78**On crash/OOM:**
79- Reduce `parallel_chains` (4 → 2 → 1)
80- Reduce `max_treedepth` (10 → 8)
81- Subsample data or simplify model
82
83## ArviZ Integration
84
85Design Stan programs for downstream ArviZ workflow:
86
87**Generated quantities:**
88- Always include pointwise log-likelihood: `vector[N] log_lik` - required for model comparison and downstream workflow
89- Always include posterior predictive draws: `vector[N] y_rep` - required for all predictive checks
90- For multiple observed variables, use one vector per variable: `log_lik_y1`, `log_lik_y2`
91- This will incur modest overhead, but might be worth workflow simplicity
92
93**Transformed parameters:**
94- Put reusable intermediate quantities here (e.g., `vector[N] mu = alpha + X * beta`)
95- Avoids recomputation in Python and makes them available in posterior samples
96
97**Extending without refitting:**
98- To add new derived quantities, use `generate_quantities` mode with original posterior draws
99- Write new Stan file with same data/parameters/transformed parameters but extended generated quantities
100- Call `model.generate_quantities(data=data, mcmc_sample=fit)` - orders of magnitude faster than refitting
101
102**Save and cache:**
103- Convert to InferenceData: `az.from_cmdstanpy(fit, log_likelihood="log_lik", posterior_predictive=["y_rep"])`
104- Save as NetCDF: `idata.to_netcdf("posterior.nc")` - makes all downstream analysis instant
105- Use consistent coords/dims for all models in the workflow
106
107## Known Issues
108
109- **CmdStanPy `diagnose()` OOMs** on large data (N > 10K). Use `check_convergence()` from `shared_utils` instead.
110- **ArviZ column names** are lowercase (`r_hat`, `ess_bulk`). CmdStanPy uses uppercase (`R_hat`, `ESS_bulk`).
111- **Stan CSV columns** use dots: `beta.1` not `beta[1]`.
112
113## References
114
115If stuck on Stan patterns or ArviZ usage, search these resources:
116- Stan case studies: https://mc-stan.org/learn-stan/case-studies.html
117- ArviZ API documentation: https://python.arviz.org/en/latest/api/index.html
118
119Use WebSearch or WebFetch to find specific examples.