pymc
Probabilistic programming for Bayesian statistical modeling and inference. PyMC provides declarative model specification with MCMC (NUTS) and variational inference samplers; NumPyro offers JAX-accelerated equivalent for large-scale problems. Use when: quantifying uncertainty in parameter estimates, building hierarchical or mixed-effects models, Bayesian A/B testing or experimentation, posterior predictive checks, model comparison with WAIC or LOO-CV, scientific measurement with error propagation, any analysis requiring credible intervals, probability statements like P(effect > 0), or situations where understanding the full posterior distribution matters more than a single p-value. Also use when priors encode domain knowledge, sample sizes are small, or data is naturally nested.
Best use case
pymc is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Probabilistic programming for Bayesian statistical modeling and inference. PyMC provides declarative model specification with MCMC (NUTS) and variational inference samplers; NumPyro offers JAX-accelerated equivalent for large-scale problems. Use when: quantifying uncertainty in parameter estimates, building hierarchical or mixed-effects models, Bayesian A/B testing or experimentation, posterior predictive checks, model comparison with WAIC or LOO-CV, scientific measurement with error propagation, any analysis requiring credible intervals, probability statements like P(effect > 0), or situations where understanding the full posterior distribution matters more than a single p-value. Also use when priors encode domain knowledge, sample sizes are small, or data is naturally nested.
Teams using pymc should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/pymc & numpyro/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How pymc Compares
| Feature / Agent | pymc | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Probabilistic programming for Bayesian statistical modeling and inference. PyMC provides declarative model specification with MCMC (NUTS) and variational inference samplers; NumPyro offers JAX-accelerated equivalent for large-scale problems. Use when: quantifying uncertainty in parameter estimates, building hierarchical or mixed-effects models, Bayesian A/B testing or experimentation, posterior predictive checks, model comparison with WAIC or LOO-CV, scientific measurement with error propagation, any analysis requiring credible intervals, probability statements like P(effect > 0), or situations where understanding the full posterior distribution matters more than a single p-value. Also use when priors encode domain knowledge, sample sizes are small, or data is naturally nested.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# PyMC & NumPyro — Probabilistic Programming
PyMC is the leading Python library for Bayesian statistical modeling. You declare priors and a likelihood — the sampler computes the posterior automatically. NumPyro provides the identical paradigm on JAX for GPU-accelerated inference. Both integrate with ArviZ for diagnostics.
**Core value:** Instead of "is this significant?" (p-value), Bayesian methods answer "what is the full probability distribution over possible values?" — a fundamentally richer answer for every scientific and business question.
## When to Use
- Quantifying uncertainty: credible intervals, prediction intervals, error propagation through calculations.
- Hierarchical models: data naturally nested (students in schools, patients in hospitals, stores in regions).
- A/B testing where "P(B > A) = 0.95" is more actionable than "p = 0.03".
- Scientific measurement where uncertainty must propagate through a pipeline.
- Model comparison: which explanation fits better? (WAIC, LOO-CV)
- Small samples: Bayesian priors regularize and prevent overfitting where MLE fails.
- Any problem where you can articulate the generative story (how data was produced).
**When NOT to use:** Simple hypothesis tests on huge datasets where a p-value suffices. Pure prediction without uncertainty (use sklearn). Real-time inference (MCMC is too slow).
## Reference Documentation
**PyMC docs**: https://pymc.io/en/stable/
**NumPyro docs**: https://num.pyro.ai/en/stable/
**ArviZ docs**: https://arviz.org/en/stable/
**GitHub**: https://github.com/pymc-devs/pymc
**Search patterns**: `pm.Model`, `pm.sample`, `pm.Normal`, `az.summary`, `az.plot_trace`
## Core Principles
### Bayes' Theorem
**posterior ∝ prior × likelihood**
Prior = belief before data. Likelihood = how probable is this data given the parameter? Posterior = updated belief after seeing data. PyMC specifies prior + likelihood; MCMC computes the posterior by sampling.
### MCMC — Markov Chain Monte Carlo
The posterior is rarely analytically tractable. MCMC draws samples that, in the limit, are distributed exactly as the posterior. PyMC uses NUTS (No-U-Turn Sampler) — state of the art. Output: thousands of parameter values drawn from the posterior.
### Posterior Predictive Check (PPC)
Generate synthetic data from the fitted model. Compare to real data. If they match visually, the model is reasonable. This catches systematic failures that summary statistics miss — the single most important validation step.
### Credible Intervals
A 95% credible interval [a, b] means: "Given the data, there is a 95% probability the true parameter lies in [a, b]." This is the intuitive interpretation people actually want. (Frequentist confidence intervals do NOT mean this.)
## Quick Reference
### Installation
```bash
pip install pymc arviz # PyMC + diagnostics
pip install numpyro jax jaxlib # NumPyro (JAX backend, GPU-accelerated)
```
### Standard Imports
```python
import pymc as pm
import arviz as az
import numpy as np
import pandas as pd
```
### Basic Pattern — Coin Flip (Simplest Bayesian Model)
```python
import pymc as pm
import arviz as az
# Observed: 15 heads out of 50 flips
n_flips, n_heads = 50, 15
with pm.Model() as model:
# Prior: belief about P(heads) before seeing data
p = pm.Beta('p', alpha=2, beta=2) # Weakly informative, centered at 0.5
# Likelihood: how many heads given p
heads = pm.Binomial('heads', n=n_flips, p=p, observed=n_heads)
# Sample the posterior
trace = pm.sample(4000, cores=2, return_inferencedata=True, random_seed=42)
# Interpret posterior
az.summary(trace, var_names=['p'])
# → mean ≈ 0.31, HDI[94%] = [0.19, 0.43]
# Translation: "P(heads) is 31%; 94% credible that it's between 19% and 43%"
az.plot_posterior(trace, var_names=['p'])
```
### Basic Pattern — NumPyro Equivalent
```python
import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS
from jax import random
import jax.numpy as jnp
n_flips, n_heads = 50, 15
def model():
p = numpyro.sample('p', dist.Beta(2, 2))
numpyro.sample('heads', dist.Binomial(n_flips, p), obs=n_heads)
kernel = NUTS(model)
mcmc = MCMC(kernel, num_warmup=1000, num_samples=4000, num_chains=2)
mcmc.run(random.PRNGKey(0))
samples = mcmc.get_samples()
print(f"P(heads): {samples['p'].mean():.3f} "
f"HDI=[{jnp.percentile(samples['p'], 3):.3f}, {jnp.percentile(samples['p'], 97):.3f}]")
```
## Critical Rules
### ✅ DO
- **Check convergence ALWAYS** — `az.summary()` shows Rhat. Rhat > 1.01 means not converged. Never trust results without this.
- **Use ≥ 2 chains, prefer 4** — Cannot assess convergence with 1 chain.
- **Do posterior predictive checks** — `pm.sample_posterior_predictive()`. Non-negotiable validation.
- **Use informative priors where possible** — Even weakly informative (`HalfNormal(1)` for σ) beats flat priors. Flat priors on unbounded scales cause convergence failures.
- **Use `HalfNormal` or `Gamma` for scale parameters** — σ must be positive. `pm.HalfNormal('sigma', sigma=1)` is the default choice.
- **Wrap derived quantities in `pm.Deterministic`** — Only way to track computed posteriors (odds ratios, differences, lifts).
- **Set `random_seed`** — Reproducibility.
- **Use `return_inferencedata=True`** — Returns ArviZ InferenceData. Legacy dict format is deprecated.
### ❌ DON'T
- **Don't ignore divergences** — Divergence = sampler lost. Requires reparameterization or prior adjustment.
- **Don't use `Uniform` on scale parameters** — `pm.Uniform('sigma', 0, 100)` causes sampler to struggle near 0. Use `HalfNormal`.
- **Don't confuse `observed` and unobserved** — `observed=data` binds to data. Without it, the variable is sampled from the prior.
- **Don't run only 1 chain** — Cannot diagnose convergence.
- **Don't use PyMC for >100k rows without considering NumPyro** — CPU-bound PyMC becomes very slow. NumPyro + JAX + GPU is 10–100x faster.
- **Don't mix up loss functions / label formats** — In Bayesian context: if labels are integers use `Categorical`; if binary use `Bernoulli`; if proportions use `Beta`.
## Anti-Patterns (NEVER)
```python
import pymc as pm
import numpy as np
# ❌ BAD: Flat prior on scale → sampler struggles near zero
with pm.Model():
sigma = pm.Uniform('sigma', lower=0, upper=100) # Divergences guaranteed
# ✅ GOOD: HalfNormal is the standard for positive scales
with pm.Model():
sigma = pm.HalfNormal('sigma', sigma=1.0)
# ❌ BAD: Ignoring convergence — trusting undiagnosed results
with pm.Model():
mu = pm.Normal('mu', mu=0, sigma=10)
obs = pm.Normal('obs', mu=mu, sigma=1, observed=data)
trace = pm.sample(100, cores=1) # Too few draws, 1 chain
print(trace.posterior['mu'].mean().values) # Meaningless if not converged
# ✅ GOOD: Proper setup + convergence gate
with pm.Model():
mu = pm.Normal('mu', mu=0, sigma=10)
obs = pm.Normal('obs', mu=mu, sigma=1, observed=data)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
import arviz as az
summary = az.summary(trace)
assert summary['r_hat'].max() < 1.01, f"Not converged: Rhat={summary['r_hat'].max():.3f}"
# ❌ BAD: Derived quantity not tracked — lost after sampling
with pm.Model():
p_a = pm.Beta('p_a', 1, 1)
p_b = pm.Beta('p_b', 1, 1)
# Difference p_b - p_a is never recorded in the trace
# ✅ GOOD: Deterministic captures derived posteriors
with pm.Model():
p_a = pm.Beta('p_a', 1, 1)
p_b = pm.Beta('p_b', 1, 1)
diff = pm.Deterministic('diff', p_b - p_a) # Now in trace.posterior['diff']
# ❌ BAD: No posterior predictive check — model could be completely wrong
with pm.Model() as model:
mu = pm.Normal('mu', mu=0, sigma=10)
sigma = pm.HalfNormal('sigma', sigma=1)
obs = pm.Normal('obs', mu=mu, sigma=sigma, observed=data)
trace = pm.sample(4000, return_inferencedata=True)
# "Converged" ≠ "correct". Convergence just means the sampler found A distribution.
# ✅ GOOD: PPC validates whether the model generates plausible data
with pm.Model() as model:
mu = pm.Normal('mu', mu=0, sigma=10)
sigma = pm.HalfNormal('sigma', sigma=1)
obs = pm.Normal('obs', mu=mu, sigma=sigma, observed=data)
trace = pm.sample(4000, return_inferencedata=True)
ppc = pm.sample_posterior_predictive(trace, model=model)
az.plot_ppc(ppc, var_names=['obs']) # Simulated vs real — must look similar
```
## Prior Selection Guide
```
PARAMETER | RECOMMENDED PRIOR | NOTES
---------------------------|--------------------------------|--------------------------------------
Mean / intercept | Normal(domain_mean, wide_sd) | Weakly informative; scale to data
Positive scale (σ, τ) | HalfNormal(1) | Default; adjust sigma to data scale
Rate / proportion (0–1) | Beta(1, 1) | Uniform on [0,1]; Beta(2,5) if informed
Count rate (λ) | Gamma(2, 1) or Exponential(1) | Positive, right-skewed
Regression slope | Normal(0, 2–5) | Weakly regularizing toward zero
Odds ratio | Lognormal(0, 1) | Positive, multiplicative scale
Correlation | LKJCorrelation(1) | PyMC-specific; valid [-1, 1]
RULES OF THUMB:
→ Know the rough scale? Use Normal(known_mean, 2–10 × known_std).
→ Know nothing? Wide but bounded priors, NOT Uniform(-∞, ∞).
→ Scale parameters (σ, τ) ALWAYS positive → HalfNormal, Gamma, or Exponential.
→ More data = prior matters less. With n > 100, even bad priors get washed out.
```
## Model Specification — PyMC Patterns
### Basic Structure
```python
import pymc as pm
with pm.Model() as model:
# 1. PRIORS — parameters to learn
mu = pm.Normal('mu', mu=0, sigma=10)
sigma = pm.HalfNormal('sigma', sigma=1)
# 2. LIKELIHOOD — data generation mechanism
obs = pm.Normal('obs', mu=mu, sigma=sigma, observed=data)
# 3. DERIVED QUANTITIES (optional)
# pm.Deterministic('name', expression)
# 4. SAMPLE
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
```
### Linear Regression
```python
import pymc as pm
import numpy as np
# X shape: (n,) single predictor or (n, p) multiple
# y shape: (n,)
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=5)
slope = pm.Normal('slope', mu=0, sigma=5) # shape=(p,) for multivariate
sigma = pm.HalfNormal('sigma', sigma=2)
mu = pm.Deterministic('mu', intercept + slope * X) # Track mean in trace
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
```
### Multivariate Regression
```python
import pymc as pm
import numpy as np
X = np.array([...]) # Shape: (n, p)
y = np.array([...])
n, p = X.shape
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=5)
slopes = pm.Normal('slopes', mu=0, sigma=5, shape=p) # One per feature
sigma = pm.HalfNormal('sigma', sigma=2)
mu = pm.Deterministic('mu', intercept + X @ slopes) # Matrix multiply
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True)
```
## Sampling and Diagnostics
### Sampling Parameters
```python
import pymc as pm
with pm.Model() as model:
# ... model ...
trace = pm.sample(
draws=4000, # Posterior samples per chain (after warmup)
tune=2000, # Warmup draws — discarded; sampler adapts here
cores=4, # Parallel chains = CPU cores
chains=4, # Independent chains for convergence check
return_inferencedata=True, # ArviZ InferenceData format
random_seed=42, # Reproducibility
)
```
### Diagnostics Checklist
```python
import pymc as pm
import arviz as az
# 1. SUMMARY — Rhat + ESS at a glance
# Rhat ≈ 1.00 → converged | ESS > 100 → enough effective samples
summary = az.summary(trace)
print(summary[['mean', 'sd', 'hdi_3%', 'hdi_97%', 'r_hat', 'ess_bulk']])
# 2. TRACE PLOT — visual convergence
# Chains should overlap, look like random noise. No trends or drift.
az.plot_trace(trace, var_names=['mu', 'sigma'])
# 3. POSTERIOR
az.plot_posterior(trace, var_names=['mu', 'sigma'])
# 4. ENERGY — divergence check
az.plot_energy(trace) # Two distributions should overlap heavily
# 5. PPC — model validation
ppc = pm.sample_posterior_predictive(trace, model=model)
az.plot_ppc(ppc, var_names=['y_obs'])
# ─── Convergence decision tree ───
# Rhat > 1.05 → STOP. Increase tune, add chains, or reparameterize.
# Rhat 1.01–1.05 → WARNING. Increase draws and tune.
# Rhat < 1.01 → OK. Verify ESS > 100.
# Many divergences → Reparameterize or tighten priors.
```
## NumPyro — The Fast Alternative
### When to Switch
```
PyMC (default):
✅ Easier syntax, better diagnostics, richer tutorials
✅ Use when: accuracy > speed, data < 50k rows
NumPyro (switch when):
✅ JAX backend → GPU-accelerated, 10–100x faster
✅ Use when: data > 50k rows, you have a GPU, or need speed
```
### Side-by-Side: Linear Regression
```python
# ─── PyMC ─────────────────────────────────────────────
import pymc as pm
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=5)
slope = pm.Normal('slope', mu=0, sigma=5)
sigma = pm.HalfNormal('sigma', sigma=2)
mu = intercept + slope * X
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True)
# ─── NumPyro equivalent ────────────────────────────────
import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS, Predictive
from jax import random
def model(X, y=None):
intercept = numpyro.sample('intercept', dist.Normal(0, 5))
slope = numpyro.sample('slope', dist.Normal(0, 5))
sigma = numpyro.sample('sigma', dist.HalfNormal(2))
mu = intercept + slope * X
numpyro.sample('y', dist.Normal(mu, sigma), obs=y)
kernel = NUTS(model)
mcmc = MCMC(kernel, num_warmup=2000, num_samples=4000, num_chains=4)
mcmc.run(random.PRNGKey(0), X=X_train, y=y_train)
samples = mcmc.get_samples()
# Prediction: full posterior predictive
pred_fn = Predictive(model, samples)
preds = pred_fn(random.PRNGKey(1), X=X_test)
# preds['y'] shape: (4000, n_test) — one prediction per posterior sample
```
### NumPyro: Multivariate
```python
import numpyro
import numpyro.distributions as dist
from numpyro.infer import MCMC, NUTS
from jax import random
import jax.numpy as jnp
def model(X, y=None):
n, p = X.shape
slopes = numpyro.sample('slopes', dist.Normal(jnp.zeros(p), 5))
intercept = numpyro.sample('intercept', dist.Normal(0, 5))
sigma = numpyro.sample('sigma', dist.HalfNormal(2))
mu = intercept + X @ slopes
numpyro.sample('y', dist.Normal(mu, sigma), obs=y)
kernel = NUTS(model)
mcmc = MCMC(kernel, num_warmup=2000, num_samples=4000, num_chains=4)
mcmc.run(random.PRNGKey(42), X=X_train, y=y_train)
```
## Common Model Patterns
### 1. Linear Regression with Uncertainty Bands
```python
import pymc as pm
import arviz as az
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
X = np.random.randn(100)
y = 2.5 * X + 1.0 + np.random.randn(100) * 0.8
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=5)
slope = pm.Normal('slope', mu=0, sigma=5)
sigma = pm.HalfNormal('sigma', sigma=2)
mu = pm.Deterministic('mu', intercept + slope * X)
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Posterior of slope (compare to true value 2.5)
az.plot_posterior(trace, var_names=['slope'], ref_val=2.5)
# Prediction with credible + predictive bands
X_new = np.linspace(-3, 3, 100)
slopes = trace.posterior['slope'].values.flatten()
intercepts = trace.posterior['intercept'].values.flatten()
sigmas = trace.posterior['sigma'].values.flatten()
mu_pred = intercepts[:, None] + slopes[:, None] * X_new[None, :] # (samples, 100)
y_pred = mu_pred + sigmas[:, None] * np.random.randn(*mu_pred.shape) # Add obs noise
plt.fill_between(X_new, np.percentile(y_pred, 2.5, 0), np.percentile(y_pred, 97.5, 0),
alpha=0.15, color='blue', label='95% predictive interval')
plt.fill_between(X_new, np.percentile(mu_pred, 2.5, 0), np.percentile(mu_pred, 97.5, 0),
alpha=0.35, color='blue', label='95% credible interval (mean)')
plt.plot(X_new, mu_pred.mean(0), 'k-', lw=2, label='Posterior mean')
plt.scatter(X, y, s=15, alpha=0.5, color='gray', label='Data')
plt.legend(); plt.xlabel('X'); plt.ylabel('y'); plt.show()
```
### 2. Hierarchical Model — Borrowing Strength Across Groups
```python
import pymc as pm
import arviz as az
import numpy as np
# Test scores: students nested in schools, schools have different sizes
n_schools = 8
students_per_school = np.array([5, 3, 12, 8, 2, 15, 6, 4])
school_ids = np.repeat(np.arange(n_schools), students_per_school)
scores = np.array([...]) # All student scores
with pm.Model():
# Population-level hyperparameters
mu_pop = pm.Normal('mu_pop', mu=60, sigma=20) # Overall mean
tau_pop = pm.HalfNormal('tau_pop', sigma=10) # Between-school variation
# School-level means — drawn from population (the hierarchical link)
school_mu = pm.Normal('school_mu', mu=mu_pop, sigma=tau_pop, shape=n_schools)
# Within-school noise
sigma = pm.HalfNormal('sigma', sigma=5)
# Each student's score depends on their school
score = pm.Normal('score', mu=school_mu[school_ids], sigma=sigma, observed=scores)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Key effect: small schools (n=2, 3) SHRINK toward population mean
# Large schools (n=12, 15) keep their own signal
# This is automatic Bayesian regularization — no manual tuning needed
az.plot_forest(trace, var_names=['school_mu'], combined=True)
```
### 3. Bayesian A/B Test
```python
import pymc as pm
import arviz as az
import numpy as np
# Conversion data
n_control, conv_control = 1000, 45 # 4.5%
n_treatment, conv_treatment = 1200, 72 # 6.0%
with pm.Model():
# Priors: conversion rates (Beta is natural for proportions)
p_control = pm.Beta('p_control', alpha=1, beta=1)
p_treatment = pm.Beta('p_treatment', alpha=1, beta=1)
# Likelihoods: observed conversions
pm.Binomial('obs_control', n=n_control, p=p_control, observed=conv_control)
pm.Binomial('obs_treatment', n=n_treatment, p=p_treatment, observed=conv_treatment)
# Derived quantities — the decision metrics
diff = pm.Deterministic('diff', p_treatment - p_control)
lift = pm.Deterministic('lift', (p_treatment - p_control) / p_control)
trace = pm.sample(10000, cores=4, return_inferencedata=True, random_seed=42)
# Decision outputs
diff_post = trace.posterior['diff'].values.flatten()
lift_post = trace.posterior['lift'].values.flatten()
p_better = (diff_post > 0).mean()
p_lift_gt_10 = (lift_post > 0.10).mean() # P(lift > 10%)
expected_lift = lift_post.mean()
print(f"P(treatment > control): {p_better:.3f}")
print(f"P(lift > 10%): {p_lift_gt_10:.3f}")
print(f"Expected lift: {expected_lift:.1%}")
print(f"Lift HDI [3%, 97%]: [{np.percentile(lift_post, 3):.1%}, {np.percentile(lift_post, 97):.1%}]")
# Ship rule: P(lift > MDE) > decision_threshold → ship
# e.g., if P(lift > 5%) > 0.90 → ship
```
### 4. Logistic Regression
```python
import pymc as pm
import arviz as az
import numpy as np
X = np.array([...]) # Shape: (n, p)
y = np.array([...]) # Binary: 0 or 1
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=2)
slopes = pm.Normal('slopes', mu=0, sigma=2, shape=X.shape[1])
# Log-odds → probability via sigmoid
logit_p = intercept + X @ slopes
p = pm.Deterministic('p', pm.math.sigmoid(logit_p))
# Likelihood
y_obs = pm.Bernoulli('y_obs', p=p, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Odds ratios with credible intervals
import arviz as az
slopes_post = trace.posterior['slopes'].values # (chains, draws, p)
odds_ratios = np.exp(slopes_post)
for i in range(X.shape[1]):
or_vals = odds_ratios[:, :, i].flatten()
print(f"Feature {i}: OR = {or_vals.mean():.2f} "
f"HDI=[{np.percentile(or_vals, 3):.2f}, {np.percentile(or_vals, 97):.2f}]")
# If HDI excludes 1.0 → feature has credible effect on odds
```
### 5. Poisson Regression (Count Data)
```python
import pymc as pm
import numpy as np
X = np.array([...]) # Covariates, shape: (n, p)
y = np.array([...]) # Counts: 0, 1, 2, 3, ... (non-negative integers)
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=2)
slopes = pm.Normal('slopes', mu=0, sigma=2, shape=X.shape[1])
# Log-rate link: λ = exp(Xβ)
log_mu = intercept + X @ slopes
mu = pm.Deterministic('mu', pm.math.exp(log_mu))
y_obs = pm.Poisson('y_obs', mu=mu, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Rate ratios: exponentiated slopes = multiplicative effect on expected count
rate_ratios = np.exp(trace.posterior['slopes'].values)
for i in range(X.shape[1]):
rr = rate_ratios[:, :, i].flatten()
print(f"Feature {i}: RR = {rr.mean():.2f} HDI=[{np.percentile(rr, 3):.2f}, {np.percentile(rr, 97):.2f}]")
```
### 6. AR(1) Time Series
```python
import pymc as pm
import arviz as az
import numpy as np
y = np.array([...]) # Observed time series, shape: (T,)
with pm.Model():
mu = pm.Normal('mu', mu=y.mean(), sigma=y.std() * 3) # Long-term mean
rho = pm.Uniform('rho', lower=-0.99, upper=0.99) # Autocorrelation
sigma = pm.HalfNormal('sigma', sigma=y.std()) # Innovation noise
# Conditional likelihood: y_t | y_{t-1} ~ Normal(mu + rho*(y_{t-1} - mu), sigma)
# For observed series, previous values are known — vectorized, no loop needed
y_lag = y[:-1] # y_{t-1} (known)
expected = mu + rho * (y_lag - mu)
y_obs = pm.Normal('y_obs', mu=expected, sigma=sigma, observed=y[1:])
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Posterior of autocorrelation coefficient
az.plot_posterior(trace, var_names=['rho', 'mu', 'sigma'])
# If rho HDI excludes 0 → significant autocorrelation
```
## Model Comparison
```python
import pymc as pm
import arviz as az
import numpy as np
# ─── Fit two competing models ───
# Model A: intercept only
with pm.Model() as model_a:
mu = pm.Normal('mu', mu=0, sigma=10)
sigma = pm.HalfNormal('sigma', sigma=2)
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace_a = pm.sample(4000, cores=4, return_inferencedata=True)
pm.sample_posterior_predictive(trace_a, extend_inferencedata=True)
# Model B: with covariate
with pm.Model() as model_b:
intercept = pm.Normal('intercept', mu=0, sigma=10)
slope = pm.Normal('slope', mu=0, sigma=5)
sigma = pm.HalfNormal('sigma', sigma=2)
mu = intercept + slope * X
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace_b = pm.sample(4000, cores=4, return_inferencedata=True)
pm.sample_posterior_predictive(trace_b, extend_inferencedata=True)
# ─── Compare via WAIC or LOO ───
comparison = az.compare(
{'intercept_only': trace_a, 'with_covariate': trace_b},
ic='loo' # or ic='waic'
)
print(comparison[['loo', 'dloo', 'dloo_se', 'weight']])
# Lower LOO = better. If |dloo| > 2 * dloo_se → meaningfully different.
# 'weight' column: model averaging weights (sum to 1)
az.plot_compare(comparison)
# ─── Interpretation ───
# LOO/WAIC automatically penalize complexity (like AIC/BIC but Bayesian).
# Best model: comparison.index[0]
# If weights are 0.9 vs 0.1 → clear winner.
# If weights are 0.6 vs 0.4 → both plausible, consider model averaging.
```
## Practical Workflows
### 1. A/B Test with Full Decision Support
```python
import pymc as pm
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def bayesian_ab_test(data: pd.DataFrame,
group_col: str,
conversion_col: str,
min_detectable_effect: float = 0.05,
decision_threshold: float = 0.90):
"""
Full Bayesian A/B test → ship / don't ship decision.
Returns probability treatment wins, expected lift, confidence in exceeding MDE.
"""
groups = data[group_col].unique()
assert len(groups) == 2
stats = {}
for g in groups:
mask = data[group_col] == g
stats[g] = {'n': int(mask.sum()), 'conv': int(data.loc[mask, conversion_col].sum())}
ctrl, treat = groups[0], groups[1]
with pm.Model():
p_c = pm.Beta('p_control', alpha=1, beta=1)
p_t = pm.Beta('p_treatment', alpha=1, beta=1)
pm.Binomial('obs_c', n=stats[ctrl]['n'], p=p_c, observed=stats[ctrl]['conv'])
pm.Binomial('obs_t', n=stats[treat]['n'], p=p_t, observed=stats[treat]['conv'])
pm.Deterministic('diff', p_t - p_c)
pm.Deterministic('lift', (p_t - p_c) / p_c)
trace = pm.sample(10000, cores=4, return_inferencedata=True, random_seed=42)
diff_post = trace.posterior['diff'].values.flatten()
lift_post = trace.posterior['lift'].values.flatten()
p_better = (diff_post > 0).mean()
p_lift_gt_mde = (lift_post > min_detectable_effect).mean()
decision = "✅ SHIP" if p_lift_gt_mde >= decision_threshold else "❌ DON'T SHIP"
# Visualization
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.hist(lift_post, bins=120, density=True, alpha=0.7, color='steelblue', edgecolor='none')
ax1.axvline(0, color='red', ls='--', lw=1.5, label='No effect')
ax1.axvline(min_detectable_effect, color='green', ls='--', lw=1.5, label=f'MDE = {min_detectable_effect:.0%}')
ax1.set_xlabel('Relative lift')
ax1.set_title(f'{decision}\nP(lift > MDE) = {p_lift_gt_mde:.1%}')
ax1.legend(fontsize=9)
ax2.hist(diff_post, bins=120, density=True, alpha=0.7, color='coral', edgecolor='none')
ax2.axvline(0, color='red', ls='--', lw=1.5)
ax2.set_xlabel('Absolute difference')
ax2.set_title(f'P(treatment > control) = {p_better:.1%}')
plt.tight_layout(); plt.show()
return {
'p_treatment_better': p_better,
'p_lift_gt_mde': p_lift_gt_mde,
'expected_lift': lift_post.mean(),
'lift_hdi_94': (np.percentile(lift_post, 3), np.percentile(lift_post, 97)),
'decision': decision
}
# report = bayesian_ab_test(df, group_col='variant', conversion_col='purchased')
```
### 2. Hierarchical Geo-Marketing Test
```python
import pymc as pm
import arviz as az
import numpy as np
import pandas as pd
def hierarchical_geo_test(df: pd.DataFrame,
region_col: str,
treatment_col: str,
outcome_col: str):
"""
Regions vary in baseline; treatment effect is shared across all regions.
Small regions borrow strength from the population — no region left noisy.
"""
regions = df[region_col].cat.codes.values
n_reg = df[region_col].nunique()
treated = df[treatment_col].values.astype(float)
outcome = df[outcome_col].values.astype(float)
with pm.Model():
# Population baseline
mu_base = pm.Normal('mu_baseline', mu=outcome.mean(), sigma=outcome.std())
tau_region = pm.HalfNormal('tau_region', sigma=1.0)
# Per-region baselines — shrink toward population
region_eff = pm.Normal('region_effect', mu=0, sigma=tau_region, shape=n_reg)
# THE quantity of interest: shared treatment effect
treatment_eff = pm.Normal('treatment_effect', mu=0, sigma=outcome.std())
# Noise
sigma = pm.HalfNormal('sigma', sigma=outcome.std())
# Expected outcome
mu = mu_base + region_eff[regions] + treatment_eff * treated
pm.Normal('y', mu=mu, sigma=sigma, observed=outcome)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Key output: treatment effect with full uncertainty
effect = trace.posterior['treatment_effect'].values.flatten()
p_pos = (effect > 0).mean()
print(f"Treatment effect: {effect.mean():.3f} ± {effect.std():.3f}")
print(f"P(effect > 0): {p_pos:.3f}")
print(f"94% HDI: [{np.percentile(effect, 3):.3f}, {np.percentile(effect, 97):.3f}]")
az.plot_posterior(trace, var_names=['treatment_effect'], ref_val=0)
return trace
# trace = hierarchical_geo_test(df, 'region', 'is_treated', 'revenue')
```
### 3. Scientific Measurement — Error Propagation
```python
import pymc as pm
import arviz as az
import numpy as np
def calibrate_instrument(readings, measurements, new_readings):
"""
Calibration: true_value = a * reading + b + noise.
Predict new readings with FULL uncertainty propagation:
both parameter uncertainty AND measurement noise.
"""
readings = np.array(readings, dtype=float)
measurements = np.array(measurements, dtype=float)
new_readings = np.array(new_readings, dtype=float)
with pm.Model() as model:
# Calibration curve parameters
a = pm.Normal('a', mu=1.0, sigma=2.0) # Scale
b = pm.Normal('b', mu=0.0, sigma=1.0) # Offset
sigma = pm.HalfNormal('sigma', sigma=0.5) # Measurement noise
# Calibration likelihood
mu_cal = a * readings + b
pm.Normal('calibration', mu=mu_cal, sigma=sigma, observed=measurements)
# Prediction: deterministic mean for new readings
mu_new = pm.Deterministic('predicted_mean', a * new_readings + b)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
# Posterior predictive for new readings (includes measurement noise)
# Sample: predicted_value = a*x + b + sigma*eps
a_post = trace.posterior['a'].values.flatten()
b_post = trace.posterior['b'].values.flatten()
sigma_post = trace.posterior['sigma'].values.flatten()
for i, x_new in enumerate(new_readings):
mu_samples = a_post * x_new + b_post
pred_samples = mu_samples + sigma_post * np.random.randn(len(sigma_post))
print(f"Reading {x_new:.1f} → "
f"Predicted: {pred_samples.mean():.3f} ± {pred_samples.std():.3f} | "
f"94% HDI: [{np.percentile(pred_samples, 3):.3f}, {np.percentile(pred_samples, 97):.3f}]")
return trace
# trace = calibrate_instrument(
# readings=[1, 2, 3, 4, 5],
# measurements=[2.1, 4.0, 5.9, 8.1, 10.0],
# new_readings=[6.0, 7.0]
# )
```
### 4. Bayesian Model Selection Pipeline
```python
import pymc as pm
import arviz as az
import numpy as np
def compare_regression_models(X, y, feature_sets: dict):
"""
Compare regression models defined by different feature subsets.
feature_sets: dict of {name: list_of_column_indices}
e.g., {'baseline': [0], 'add_age': [0,1], 'full': [0,1,2,3]}
Returns: comparison table + traces + best model name
"""
traces = {}
for name, cols in feature_sets.items():
X_sub = X[:, cols]
p = len(cols)
with pm.Model():
intercept = pm.Normal('intercept', mu=0, sigma=10)
slopes = pm.Normal('slopes', mu=0, sigma=5, shape=p)
sigma = pm.HalfNormal('sigma', sigma=y.std())
mu = intercept + X_sub @ slopes
pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(4000, cores=4, return_inferencedata=True, random_seed=42)
pm.sample_posterior_predictive(trace, extend_inferencedata=True)
traces[name] = trace
print(f" ✓ {name} sampled")
# Compare
comparison = az.compare(traces, ic='loo')
print("\n" + comparison[['loo', 'dloo', 'dloo_se', 'weight']].to_string())
az.plot_compare(comparison)
best = comparison.index[0]
print(f"\nBest model: {best} (LOO weight = {comparison.loc[best, 'weight']:.2f})")
return comparison, traces, best
# feature_sets = {
# 'intercept_only': [0],
# 'linear': [0, 1],
# 'quadratic': [0, 1, 2],
# 'full': [0, 1, 2, 3],
# }
# comp, traces, best = compare_regression_models(X, y, feature_sets)
```
## Performance and Scaling
### PyMC vs NumPyro Decision
```
Data < 10k rows, simple model → PyMC + MCMC. Best diagnostics.
Data 10k–100k rows → PyMC first. If >30 min → switch to NumPyro.
Data > 100k rows or GPU available → NumPyro + JAX directly.
Need speed >> accuracy → Variational inference (see below).
```
### Variational Inference — Fast Approximation
```python
import pymc as pm
import arviz as az
with pm.Model() as model:
mu = pm.Normal('mu', mu=0, sigma=10)
sigma = pm.HalfNormal('sigma', sigma=2)
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
# ADVI: approximate posterior with a Gaussian. Much faster than MCMC.
approx = pm.fit(method='advi', n=10000)
# Draw from approximate posterior
trace_vi = approx.sample(5000)
# Validate: run MCMC on a data subset, compare posteriors.
# If they agree → safe to use VI on full data.
```
### Parallel Chains
```python
import pymc as pm
import os
n_cores = os.cpu_count()
with pm.Model():
# ... model ...
trace = pm.sample(
draws=4000, tune=2000,
cores=n_cores, # One chain per core
chains=n_cores, # Match chains to cores for max parallelism
return_inferencedata=True
)
```
## Common Pitfalls and Solutions
### Divergences
```python
# SYMPTOM: "X divergences detected" in sampling output
# MEANING: Sampler got lost in a region of high curvature
# Diagnose:
import arviz as az
az.plot_energy(trace) # Energy-4 should overlap with marginal energy
# FIX (in order of likelihood):
# 1. Reparameterize — use log-scale for positive parameters:
# Instead of: sigma = pm.HalfNormal('sigma', sigma=1)
# Try: log_sigma = pm.Normal('log_sigma', mu=0, sigma=1)
# sigma = pm.Deterministic('sigma', pm.math.exp(log_sigma))
#
# 2. Increase warmup: pm.sample(tune=5000)
# 3. Increase target_accept: pm.sample(target_accept=0.95) — more conservative steps
# 4. Tighten priors — reduces the volume the sampler must explore
```
### Slow Sampling
```python
# Benchmark: how long per 100 draws?
import time
with pm.Model() as model:
# ... model ...
start = time.time()
_ = pm.sample(100, tune=100, cores=1, chains=1)
per_100 = time.time() - start
print(f"100 draws: {per_100:.1f}s → full run estimate: {per_100 * 40 + per_100 * 20:.0f}s")
# If per_100 > 10s:
# → Switch to NumPyro (JAX + GPU)
# → Or use variational inference: pm.fit(method='advi')
# → Or simplify the model (fewer parameters, simpler likelihood)
```
### Model Converges But PPC Fails
```python
# SYMPTOM: Rhat OK, but simulated data looks nothing like real data
# MEANING: The model converged to the WRONG distribution
# Diagnose:
ppc = pm.sample_posterior_predictive(trace, model=model)
az.plot_ppc(ppc) # Obvious mismatch
# FIX:
# 1. Check the generative story — does the likelihood match how data was actually produced?
# 2. Add missing structure: interactions, nonlinear terms, extra variables
# 3. Switch likelihood for heavy tails: Normal → StudentT
# 4. Check outliers: plot residuals, identify systematic bias
# 5. Visualize per-observation: predicted vs observed — find WHERE it breaks
```
### Label Switching in Mixture Models
```python
# SYMPTOM: Rhat looks bad for mixture components, but chains mirror each other
# MEANING: Component labels swap between chains (mu_1=0,mu_2=5 vs mu_1=5,mu_2=0)
# FIX: ordered constraint forces mu_1 < mu_2
with pm.Model():
mu = pm.Uniform('mu', lower=-10, upper=10, shape=2,
transform=pm.distributions.transforms.ordered)
# Now mu[0] < mu[1] guaranteed → no label switching
```
---
PyMC and NumPyro shift every question from "is this significant?" to "what is the full distribution of possibilities, and how confident are we?" The workflow is always: **specify the generative story → sample the posterior → check convergence → validate with PPC → interpret**. Master this loop and Bayesian inference becomes a universal tool across every scientific and business domain.Related Skills
xgboost-lightgbm
Industry-standard gradient boosting libraries for tabular data and structured datasets. XGBoost and LightGBM excel at classification and regression tasks on tables, CSVs, and databases. Use when working with tabular machine learning, gradient boosting trees, Kaggle competitions, feature importance analysis, hyperparameter tuning, or when you need state-of-the-art performance on structured data.
xarray
N-dimensional labeled arrays and datasets in Python. Built on top of NumPy and Dask. It introduces labels in the form of dimensions, coordinates, and attributes on top of raw NumPy-like arrays, making data analysis in physical sciences more intuitive and less error-prone. Use for working with multi-dimensional scientific data, NetCDF/GRIB/Zarr files, climate/weather/oceanographic datasets, remote sensing, geospatial imaging, large out-of-memory datasets with Dask, and labeled array operations.
transformers
State-of-the-art Machine Learning for PyTorch, TensorFlow, and JAX. Provides thousands of pretrained models to perform tasks on different modalities such as text, vision, and audio. The industry standard for Large Language Models (LLMs) and foundation models in science.
tqdm
A fast, extensible progress bar for Python and CLI. Instantly makes your loops show a smart progress meter with ETA, iterations per second, and customizable statistics. Minimal overhead. Use for monitoring long-running loops, simulations, data processing, ML training, file downloads, I/O operations, command-line tools, pandas operations, parallel tasks, and nested progress bars.
tensorflow
Comprehensive deep learning framework for building, training, and deploying neural networks. TensorFlow provides tf.keras high-level API for model construction, tf.data for efficient data pipelines, and tf.function for graph-mode optimization. Use when working with: neural network training and inference, image classification/detection/segmentation, NLP/text processing with embeddings or transformers, time series forecasting, generative models (VAE, GAN), transfer learning with pretrained models, custom training loops with GradientTape, GPU/TPU accelerated computation, or any deep learning task.
sympy
Comprehensive guide for SymPy - Python library for symbolic mathematics. Use for symbolic expressions, calculus (derivatives, integrals, limits, series), equation solving (algebraic, differential, systems), linear algebra, simplification, matrix operations, special functions, code generation, and mathematical proofs. Essential for analytical mathematics and computer algebra.
sunpy
The community-developed free and open-source software package for solar physics. Provides tools for data search and download, coordinate transformations specific to solar physics, and powerful image processing through the Map object. Use when working with solar data, solar images (EUV, magnetograms, white light), solar coordinates (Helioprojective, Heliographic), Fido data search, solar time series, differential rotation, limb fitting, or multi-instrument solar analysis (AIA, HMI, GOES).
statsmodels
Advanced statistical modeling and hypothesis testing. Complementary to SciPy's stats module, it provides classes and functions for the estimation of many different statistical models, as well as for conducting statistical tests and statistical data exploration. Use for linear regression, GLM, time series analysis, ANOVA, survival analysis, causal inference, and statistical hypothesis testing. Load when working with OLS, WLS, logistic regression, Poisson regression, ARIMA, SARIMAX, statistical diagnostics, p-values, confidence intervals, or R-style statistical analysis.
spacy-nltk
Natural Language Processing for text analysis, corpus linguistics, and production NLP pipelines. spaCy provides fast production-grade tokenization, POS tagging, NER, dependency parsing, and custom model training. NLTK provides classical corpus linguistics, linguistic analysis, VADER sentiment, collocation analysis, and access to standard linguistic corpora. Use when: processing and analyzing text data, extracting named entities (people, orgs, locations, dates), dependency parsing and syntactic analysis, building text classification pipelines, performing corpus-level linguistic analysis (frequency, collocations, readability), sentiment analysis, lemmatization and stemming, working with multilingual text, training custom NER or text classifiers, or any task requiring structured understanding of natural language beyond simple string operations.
sktime-tsfresh
Time series machine learning layer (Tier 1): integration of **sktime** and **tsfresh** for building production-grade pipelines that transform raw time series into tabular feature representations suitable for classical machine-learning models. *sktime* provides a unified, sklearn-compatible interface for time-series data types, transformations, and pipelines, while *tsfresh* enables large-scale automated extraction of statistical, spectral, and autocorrelation features, with optional statistically grounded feature relevance selection (FRESH).
sklearn-explainability
Advanced sub-skill for scikit-learn focused on model interpretability, feature importance, and diagnostic tools. Covers global and local explanations using built-in inspection tools and SHAP/LIME integrations.
sklearn-advanced
Professional sub-skill for scikit-learn focused on robust pipeline architecture, custom estimator development, advanced feature engineering, and rigorous model validation. Covers Target Encoding, Nested Cross-Validation, and Production Deployment.