Probabilistic Programming in Python: Bayesian Modeling and Probabilistic Machine Learning with PyTensor
—
PyMC provides a comprehensive suite of sampling and inference methods for Bayesian analysis. The library supports Markov Chain Monte Carlo (MCMC), variational inference, sequential Monte Carlo, and predictive sampling with automatic tuning and diagnostics.
import pymc as pm
def sample(draws=1000, tune=1000, chains=None, cores=None,
random_seed=None, progressbar=True, step=None,
nuts_sampler='nutpie', initvals=None, init='auto',
jitter_max_retries=10, n_init=200000, trace=None,
discard_tuned_samples=True, compute_convergence_checks=True,
keep_warning_stat=False, return_inferencedata=True,
idata_kwargs=None, callback=None, mp_ctx=None, **kwargs):
"""
Draw samples from the posterior distribution using MCMC.
Parameters:
- draws (int): Number of samples to draw (default: 1000)
- tune (int): Number of tuning samples (default: 1000)
- chains (int): Number of chains (default: auto)
- cores (int): Number of parallel processes (default: auto)
- random_seed (int): Random seed for reproducibility
- step (step method): Custom step method (default: auto-select)
- init (str): Initialization method ('auto', 'adapt_diag', 'jitter+adapt_diag')
- nuts_sampler (str): NUTS implementation ('nutpie', 'pymc')
- return_inferencedata (bool): Return ArviZ InferenceData object
Returns:
- trace: MCMC samples as InferenceData or MultiTrace
"""
# Basic sampling
with pm.Model() as model:
# Define model...
trace = pm.sample()
# Custom sampling configuration
trace = pm.sample(
draws=2000, # More samples
tune=1500, # More tuning
chains=4, # Parallel chains
cores=4, # Use 4 cores
target_accept=0.9, # Higher acceptance rate
max_treedepth=12 # Deeper trees for complex models
)def init_nuts(init='auto', chains=None, n_init=200000, model=None,
random_seed=None, progressbar=True, jitter_max_retries=10,
tune=None, initvals=None, **kwargs):
"""
Initialize NUTS sampler with optimal step size and mass matrix.
Parameters:
- init (str): Initialization strategy
- 'auto': Automatic initialization
- 'adapt_diag': Adapt diagonal mass matrix
- 'jitter+adapt_diag': Add jitter + adapt diagonal
- 'adapt_full': Adapt full mass matrix
- chains (int): Number of chains to initialize
- n_init (int): Number of initialization samples
- initvals (dict): Initial parameter values
Returns:
- step: Initialized NUTS step method
- potential: Model potential function
"""
# Initialize NUTS with custom settings
step, potential = pm.init_nuts(
init='adapt_full', # Full mass matrix adaptation
n_init=500000, # More initialization samples
chains=4
)
# Use initialized sampler
trace = pm.sample(step=step)The default and most efficient sampler for continuous variables:
from pymc.step_methods import NUTS
class NUTS:
"""
No-U-Turn Sampler for continuous variables.
Parameters:
- vars (list): Variables to sample (default: all continuous)
- target_accept (float): Target acceptance probability (default: 0.8)
- max_treedepth (int): Maximum tree depth (default: 10)
- step_scale (float): Initial step size scaling
- is_cov (array): Covariance matrix for mass matrix
"""
def __init__(self, vars=None, target_accept=0.8, max_treedepth=10, **kwargs):
pass
# Custom NUTS configuration
nuts_step = pm.NUTS(
vars=[param1, param2], # Specific variables
target_accept=0.95, # High acceptance rate
max_treedepth=15 # Deep trees for complex geometry
)
trace = pm.sample(step=nuts_step)from pymc.step_methods import HamiltonianMC
class HamiltonianMC:
"""
Hamiltonian Monte Carlo sampler.
Parameters:
- vars (list): Variables to sample
- path_length (float): Length of Hamiltonian trajectory
- step_rand (function): Step size randomization function
- step_scale (float): Step size scaling factor
"""
# HMC with fixed trajectory length
hmc_step = pm.HamiltonianMC(vars=continuous_vars, path_length=2.0)from pymc.step_methods import (Metropolis, BinaryMetropolis,
CategoricalGibbsMetropolis, DEMetropolis)
# Standard Metropolis-Hastings
metropolis = pm.Metropolis(vars=param, proposal_dist=pm.NormalProposal)
# For binary variables
binary_metropolis = pm.BinaryMetropolis(vars=binary_var)
# For categorical variables
categorical_gibbs = pm.CategoricalGibbsMetropolis(vars=categorical_var)
# Differential Evolution Metropolis
de_metropolis = pm.DEMetropolis(vars=param, scaling=1e-4)from pymc.step_methods import Slice
class Slice:
"""
Slice sampler for univariate distributions.
Parameters:
- vars (list): Variables to sample
- w (float): Initial width of slice
- tune (bool): Tune width during sampling
"""
# Slice sampling for specific parameter
slice_step = pm.Slice(vars=[difficult_param], w=2.0)from pymc.step_methods import (
BinaryGibbsMetropolis, BinaryMetropolis, CategoricalGibbsMetropolis,
DEMetropolis, DEMetropolisZ, CompoundStep, BlockedStep
)
# Binary Gibbs Metropolis for binary variables
binary_gibbs = pm.BinaryGibbsMetropolis('binary_var')
# Binary Metropolis with custom proposal
binary_metro = pm.BinaryMetropolis('binary_var')
# Categorical Gibbs for categorical variables
cat_gibbs = pm.CategoricalGibbsMetropolis('categorical_var')
# Differential Evolution Metropolis
de_metro = pm.DEMetropolis(vars=[var1, var2],
scaling=0.001, # Scaling factor
tune_scaling=True, # Auto-tune scaling
tune_interval=100) # Tuning frequency
# DE Metropolis with Z proposals
de_metro_z = pm.DEMetropolisZ(vars=[var1, var2])from pymc.step_methods.metropolis import (
CauchyProposal, LaplaceProposal, MultivariateNormalProposal,
NormalProposal, PoissonProposal, UniformProposal
)
# Custom Metropolis with different proposals
metro_cauchy = pm.Metropolis('var', proposal_dist=CauchyProposal)
metro_laplace = pm.Metropolis('var', proposal_dist=LaplaceProposal)
metro_uniform = pm.Metropolis('var', proposal_dist=UniformProposal)
# Multivariate normal proposal with custom covariance
mv_proposal = MultivariateNormalProposal(np.eye(3) * 0.1)
metro_mv = pm.Metropolis(vars=[var1, var2, var3], proposal_dist=mv_proposal)
# Poisson proposal for count data
metro_poisson = pm.Metropolis('count_var', proposal_dist=PoissonProposal)# Compound step combining different samplers
step1 = pm.NUTS([continuous_var1, continuous_var2])
step2 = pm.BinaryGibbsMetropolis([binary_var])
step3 = pm.CategoricalGibbsMetropolis([categorical_var])
compound_step = pm.CompoundStep([step1, step2, step3])
# Use compound step in sampling
trace = pm.sample(step=compound_step)
# Blocked step for grouping variables
blocked_step = pm.BlockedStep(methods=[step1, step2], blocking=[0, 1])from pymc.step_methods import CompoundStep
# Combine different step methods
compound = pm.CompoundStep([
pm.NUTS(vars=continuous_vars),
pm.BinaryMetropolis(vars=binary_vars),
pm.CategoricalGibbsMetropolis(vars=categorical_vars)
])
trace = pm.sample(step=compound)from pymc.step_methods.metropolis import (
NormalProposal, UniformProposal, LaplaceProposal,
CauchyProposal, PoissonProposal, MultivariateNormalProposal
)
# Normal proposal for Metropolis
normal_prop = pm.NormalProposal(s=0.5) # Standard deviation
# Uniform proposal
uniform_prop = pm.UniformProposal(s=1.0) # Half-width
# Multivariate normal proposal
mv_prop = pm.MultivariateNormalProposal(cov=covariance_matrix)def sample_smc(draws=1000, kernel='metropolis', n_steps=25, parallel=False,
start=None, cores=None, compute_convergence_checks=True,
model=None, random_seed=None, progressbar=True, **kernel_kwargs):
"""
Sequential Monte Carlo sampling.
Parameters:
- draws (int): Number of samples to draw
- kernel (str): SMC kernel ('metropolis', 'nuts')
- n_steps (int): Number of SMC steps
- parallel (bool): Parallel chain execution
- start (dict): Starting values
Returns:
- trace: SMC samples as InferenceData
"""
# Basic SMC sampling
with pm.Model() as model:
# Define model...
smc_trace = pm.sample_smc(draws=2000, kernel='metropolis')
# Advanced SMC configuration
smc_trace = pm.sample_smc(
draws=5000,
kernel='nuts',
n_steps=50,
parallel=True,
cores=4
)def sample_prior_predictive(samples=500, var_names=None, model=None,
size=None, keep_size=False, random_seed=None,
return_inferencedata=True, extend_inferencedata=True,
predictions=False, **kwargs):
"""
Sample from prior distributions.
Parameters:
- samples (int): Number of prior samples
- var_names (list): Variables to sample (default: all)
- size (int): Size for each sample draw
- predictions (bool): Include deterministic variables
- return_inferencedata (bool): Return ArviZ InferenceData
Returns:
- prior_samples: Prior predictive samples
"""
# Sample from priors
with pm.Model() as model:
# Define priors and likelihood...
prior_pred = pm.sample_prior_predictive(samples=1000)
# Sample specific variables only
prior_pred = pm.sample_prior_predictive(
samples=2000,
var_names=['alpha', 'beta', 'sigma']
)def sample_posterior_predictive(trace, samples=None, var_names=None,
size=None, keep_size=False, model=None,
progressbar=True, random_seed=None,
extend_inferencedata=True, predictions=False,
return_inferencedata=True, **kwargs):
"""
Sample from posterior predictive distribution.
Parameters:
- trace: MCMC trace or InferenceData object
- samples (int): Number of predictive samples per posterior sample
- var_names (list): Variables to predict
- size (int): Size for each prediction
- predictions (bool): Include deterministic predictions
Returns:
- posterior_pred: Posterior predictive samples
"""
# Basic posterior predictive sampling
with pm.Model() as model:
# Define model and sample...
trace = pm.sample()
post_pred = pm.sample_posterior_predictive(trace)
# Out-of-sample predictions
pm.set_data({'X_new': X_test}) # Update data
post_pred = pm.sample_posterior_predictive(
trace,
var_names=['y_pred'],
predictions=True
)def draw(vars, draws=1, random_seed=None, **kwargs):
"""
Draw samples directly from distributions.
Parameters:
- vars: Distribution(s) to sample from
- draws (int): Number of samples
- random_seed (int): Random seed
Returns:
- samples: Array of samples
"""
# Draw from distribution objects
normal_samples = pm.draw(pm.Normal.dist(0, 1), draws=1000)
# Draw from multiple distributions
samples = pm.draw([
pm.Normal.dist(0, 1),
pm.Beta.dist(2, 3)
], draws=500)
# Draw within model context
with pm.Model() as model:
mu = pm.Normal('mu', 0, 1)
sigma = pm.HalfNormal('sigma', 1)
# Draw from prior
mu_samples = pm.draw(mu, draws=100)def compile_forward_sampling_function(outputs, inputs, **kwargs):
"""
Compile fast forward sampling function.
Parameters:
- outputs (list): Output variables to sample
- inputs (list): Input parameter variables
- **kwargs: Additional compilation options
Returns:
- compiled_fn: Compiled sampling function
"""
# Compile sampling function for predictions
with pm.Model() as model:
# Define model...
sampling_fn = pm.compile_forward_sampling_function(
outputs=[y_pred],
inputs=[X, theta]
)
# Use compiled function
predictions = sampling_fn(X_new, theta_samples)def vectorize_over_posterior(fn, trace, var_names=None, **kwargs):
"""
Vectorize function over posterior samples.
Parameters:
- fn (callable): Function to vectorize
- trace: Posterior samples
- var_names (list): Variables to pass to function
Returns:
- results: Vectorized function results
"""
# Define custom function
def custom_prediction(alpha, beta, X):
return alpha + beta * X
# Vectorize over posterior
results = pm.vectorize_over_posterior(
custom_prediction,
trace,
var_names=['alpha', 'beta'],
X=X_new
)from pymc.step_methods import BlockedStep
class CustomStep(BlockedStep):
"""
Template for custom step methods.
"""
def __init__(self, vars, model=None, **kwargs):
self.vars = vars
self.model = model
super().__init__(vars, [model.compile_logp(vars)])
def step(self, point):
"""Implement custom step logic."""
# Custom sampling logic here
return new_point, stats# Parallel sampling with custom configuration
import multiprocessing as mp
# Set up parallel context
with mp.Pool(processes=4) as pool:
trace = pm.sample(
chains=4,
cores=4,
mp_ctx=pool
)
# Control parallelization
trace = pm.sample(
chains=8, # More chains than cores
cores=4, # Limit parallel processes
chain_method='sequential' # Sequential within process
)# Use Zarr backend for large traces
trace = pm.sample(
draws=100000,
trace=pm.backends.ZarrTrace('large_trace.zarr')
)
# Streaming inference for very large models
with pm.Model() as streaming_model:
# Large model definition...
# Sample in batches
for batch in range(10):
batch_trace = pm.sample(
draws=1000,
trace=pm.backends.NDArray(),
compute_convergence_checks=False
)
# Process batch_trace...# Sample with full diagnostics
trace = pm.sample(
compute_convergence_checks=True, # Compute R-hat, ESS
keep_warning_stat=True, # Keep warning statistics
progressbar=True # Show progress
)
# Access sampling statistics
print(trace.get_sampler_stats('diverging').sum()) # Divergences
print(trace.get_sampler_stats('energy')) # Energy statisticsdef custom_callback(trace, draw):
"""Custom callback function called during sampling."""
if draw % 100 == 0:
print(f"Completed draw {draw}")
# Custom diagnostics or early stopping logic
# Sample with callback
trace = pm.sample(callback=custom_callback)# Compare models using sampling
models = [model1, model2, model3]
traces = []
for model in models:
with model:
trace = pm.sample()
traces.append(trace)
# Compare using information criteria
comparison = pm.compare({f'model_{i}': trace
for i, trace in enumerate(traces)})# Robust sampling for difficult models
with pm.Model() as difficult_model:
# Complex model definition...
# Start with MAP estimate
map_estimate = pm.find_MAP()
# Initialize sampler carefully
step = pm.NUTS(
target_accept=0.99, # Very high acceptance
max_treedepth=15, # Deep trees
step_scale=0.1 # Small initial steps
)
# Sample with more tuning
trace = pm.sample(
draws=2000,
tune=3000, # Extra tuning
init='adapt_full', # Full mass matrix
initvals=map_estimate, # Start from MAP
step=step
)PyMC's sampling system provides flexible and efficient inference for a wide range of Bayesian models, from simple linear regression to complex hierarchical models with custom likelihood functions.
Install with Tessl CLI
npx tessl i tessl/pypi-pymc