- This notebook teaches how to estimate posteriors by sampling, when exact inference is too expensive or impossible
- Concepts are built on the Garden Wold examples with variables to run the query
- The flow is:
- Turn uniform randomness into samples (inverse transform)
- Sample a whole network (prior sampling)
- Watch estimates converge ()
- Condition on evidence by rejection
- Rescue rare evidence with importance weights
- Walk the state space with MCMC (mixing, Gibbs, Metropolis-Hastings)
- The exact posteriors from
pgmpyare reused throughout as the ground-truth reference that every estimate is compared against
Imports¶
%load_ext autoreload
%autoreload 2
# System libraries.
import logging
# Third-party libraries.# Use this for most notebooks.
import helpers.hintrospection as hintros
import helpers.htutorial as htutori
import L06_02_approximate_inference_utils as utils
htutori.config_notebook()
# Initialize logger.
_LOG = logging.getLogger(__name__)
utils.init_loggers(_LOG)
# Convert `display` into `print()` when running outside IPython.
try:
from IPython.display import display
except ImportError:
display = print # type: ignoreINFO Setting notebook style
INFO Notebook signature
Python 3.12.13
Linux 7f4d4ef8cf8c 6.12.67-linuxkit #1 SMP Sun Jan 25 02:26:28 UTC 2026 aarch64 GNU/Linux
INFO numpy version=2.4.6
WARNING pymc is not installed
INFO matplotlib version=3.10.9
WARNING arviz is not installed
WARNING preliz is not installed
WARNING sns is not installed
WARNING: Logger already initialized: skipping
Part 1: From Randomness to Samples¶
Cell 1.1: Turning uniform randomness into a discrete distribution¶
Goal:
- Show that a stream of uniform numbers becomes samples from a discrete target, a biased die, via the inverse CDF
Implementation: cell1_1_inverse_transform_discrete_widget()
- Draws
Nuniform numbers, then maps each through the die’s inverse CDF in_plot_inverse_transform(): the smallest face with - Overlays the resulting sample histogram on the target PMF
hintros.print_obj_info(utils.cell1_1_inverse_transform_discrete_widget)Usage
Inputs
seed: random seed for the uniform streamN(log scale): number of samples drawn
Panels
Target: biased die: the die’s probability mass functionCDF and inverse map: the staircase CDF, with one sampled mapped to its faceSample histogram: generated samples (solid) vs the target (dotted)Comments: the construction and the achieved accuracy
# Map a uniform r through the die's CDF into a sampled face.
utils.cell1_1_inverse_transform_discrete_widget()Guided usage
- Raise
Nfrom small to large, leavingseedfixed- Observe the sample histogram fill in toward the target PMF, and the
max |freq - P|error in Comments shrink
- Observe the sample histogram fill in toward the target PMF, and the
- Change
seeda few times at a smallN- Observe the single mapped point jump to a different face each time, while the underlying CDF never changes
Cell 1.2: Turning uniform randomness into a continuous distribution¶
Goal:
- Show the same inverse-transform trick on a continuous target, an exponential, where the inverse CDF has a closed form
Implementation: cell1_2_inverse_transform_continuous_widget()
- Draws
Nuniform numbers, then maps each through in_plot_inverse_transform(), the same helper Cell 1.1 uses - Overlays the resulting sample histogram on the target density
hintros.print_obj_info(utils.cell1_2_inverse_transform_continuous_widget)Usage
Inputs
seed: random seed for the uniform streamlambda: rate of the exponentialN(log scale): number of samples drawn
Panels
Target: exponential: the exponential densityCDF and inverse map: the smooth CDF, with one sampled mapped to itsSample histogram: generated samples (solid) vs the target density (dotted)Comments: the construction and the achieved accuracy
# Map a uniform r through the exponential's closed-form inverse CDF.
utils.cell1_2_inverse_transform_continuous_widget()Guided usage
- Raise
Nfrom small to large, leavinglambdafixed- Observe the sample histogram fill in toward the target density, and the sample mean in Comments approach the theoretical mean
- Raise
lambda- Observe the target density and the histogram both compress toward 0, since a larger rate means a smaller expected value
- One trick underlies both cells: stretch a flat number through the CDF and it comes out distributed like the target; when has no closed form the same idea works with numerical inversion
Cell 1.3: Prior sampling from the sprinkler network¶
Goal:
- Scale the single-variable trick up to a whole Bayesian network, by sampling every variable in topological order to generate full events
Implementation: cell1_3_prior_sampling_widget()
- Generates
Nfull events with_prior_sample_array(), sampling each node only after its parents already have a value - Compares the estimated marginal of
Track marginaland the estimated joint over all 16 configurations against the exactpgmpyvalues
hintros.print_obj_info(utils.cell1_3_prior_sampling_widget)Usage
Inputs
seed: random seed for the generated eventsN(log scale): number of full events to generateTrack marginal: variable whose marginal estimate is tracked
Panels
Sample parents before children: the sprinkler DAG, colored by topological depthMarginal of <var>: estimated of the tracked variable vs its exact valueJoint frequencies: estimated joint over all 16 configurations vs the exact jointComments: the sampling order and the achieved errors
# Generate N complete worlds in topological order and compare with the exact joint.
utils.cell1_3_prior_sampling_widget()Guided usage
- Raise
Nfrom small to large, leavingTrack marginalfixed- Observe the estimated marginal bar converge onto the exact reference, and the joint-frequency dots settle onto the exact joint curve
- Switch
Track marginalacross all four variables- Observe every one of them converges the same way: prior sampling realizes the factorization regardless of which variable is tracked
Cell 1.4: Consistency and the 1/sqrt(N) convergence rate¶
Goal:
- Make Monte Carlo convergence tangible, and show that error shrinks like , setting expectations for every later sampler
Implementation: cell1_4_convergence_widget()
- Runs
repsindependent running-estimate chains of the chosenEstimateevent up tomax N, each from its own seed offset fromseed - Plots the RMS error across chains against a reference slope on log-log axes
hintros.print_obj_info(utils.cell1_4_convergence_widget)Usage
Inputs
seed: base random seed for the independent chainsmax N(log scale): largest sample count shownreps: number of independent chains in the fanEstimate: which marginal event is being estimated
Panels
One estimate: a single running estimate converging to the exact value<reps> independent chains: the fan of chains narrowing as growsError vs N: RMS error on log-log axes with a reference slopeComments: the exact value and the final error
# Show one estimate, many estimates, and the 1/sqrt(N) error rate.
utils.cell1_4_convergence_widget()Guided usage
- Watch the fan of chains as
max Ngrows- Observe every chain narrows toward the exact value, but never stops wobbling: consistency guarantees convergence, not a fixed error at any finite
- Compare the
Error vs Ncurve against its reference slope- Observe the two track each other closely: 10x accuracy costs 100x samples, which is why the rest of the notebook focuses on using each sample better, not just drawing more of them
Part 2: Conditioning on Evidence¶
Cell 2.1: Rejection sampling¶
Goal:
- Introduce the simplest way to condition on evidence: generate prior samples and throw away those that disagree with it
Implementation: cell2_1_rejection_sampling_widget()
- Draws
Nprior samples and keeps only the ones matching everyobserve <node>value - Estimates from the kept samples, and reports the retained fraction
hintros.print_obj_info(utils.cell2_1_rejection_sampling_widget)Usage
Inputs
seed: random seed for the prior samplesN(log scale): total prior samples generatedQuery X: the query variableobserve <node>: one checkbox plus True/False value per node, marking it as evidence
Panels
Sample stream (first N): dots colored by kept (matches evidence) vs rejectedRetained fraction: counts of generated, rejected, and kept samplesPosterior estimate: vs the exact referenceComments: the retained fraction and the estimate
# Keep only the prior samples that agree with the evidence.
utils.cell2_1_rejection_sampling_widget()Guided usage
- Mark one more node as evidence, making it harder to satisfy
- Observe
Retained fraction’s kept count drop sharply: the rarer the evidence, the more samples are burned just to learn anything
- Observe
- Raise
Nat a fixed, fairly rare evidence setting- Observe the posterior estimate keeps improving, but far slower than
Nitself grows, since only the kept fraction actually contributes
- Observe the posterior estimate keeps improving, but far slower than
Cell 2.2: Importance sampling and likelihood weighting¶
Goal:
- Fix rejection’s waste by keeping every sample and correcting with importance weights instead of discarding disagreeing ones
Implementation: cell2_2_likelihood_weighting_widget()
- Draws
Nsamples with_likelihood_weight_array(), clamping evidence nodes and weighting each sample by - Reports the effective sample size alongside the weighted posterior estimate
hintros.print_obj_info(utils.cell2_2_likelihood_weighting_widget)Usage
Inputs
seed: random seed for the weighted samplesN(log scale): number of weighted samplesQuery X: the query variableobserve <node>: one checkbox plus True/False value per node, marking it as evidence
Panels
Weighted samples (first N): dots sized by importance weightWeight distribution: histogram of weights, flagging collapsePosterior estimate: weighted estimate vs exact and vs rejectionComments: the effective sample size and the estimates
# Keep every sample and correct the bias with importance weights.
utils.cell2_2_likelihood_weighting_widget()Guided usage
- Set the same rare evidence used in Cell 2.1, at the same
N- Observe the weight distribution grow uneven, but the posterior estimate still tracks the exact reference: no sample was discarded to get there
- Compare the effective sample size in Comments against
N- Observe it fall well below
Nonce weights get uneven: very uneven weights shrink the effective sample size despite a large
- Observe it fall well below
Part 3: Markov Chain Monte Carlo¶
Cell 3.1: Markov chains and the stationary distribution¶
Goal:
- Introduce the core MCMC idea, a designed random walk whose long-run distribution settles to a fixed shape independent of the start
Implementation: cell3_1_markov_chain_widget()
- Steps a fixed transition matrix
ttimes fromInitial state, and compares the evolving distribution against the exact_stationary_distribution() - Tracks total-variation distance to stationary at every step
hintros.print_obj_info(utils.cell3_1_markov_chain_widget)Usage
Inputs
seed: random seed (unused by this deterministic walk, kept for convention)t: number of steps takenInitial state: where the walk starts
Panels
Transition diagram: states as nodes, with the current most-likely state highlightedState distribution: (solid) settling onto the stationary distribution (dotted)Convergence: total-variation distance to stationary decaying withComments: the stationary distribution and current distance
# Step the chain and watch the state distribution converge to a fixed shape.
utils.cell3_1_markov_chain_widget()Guided usage
- Raise
tfrom 0 toward 40, leavingInitial statefixed- Observe settle onto the stationary distribution and the total-variation distance decay toward 0
- Change
Initial stateto a different state, then repeat- Observe still converges to the exact same stationary shape: the limit does not depend on where the walk starts
Cell 3.2: Mixing and burn-in¶
Goal:
- Show that a correct stationary distribution is not enough: mixing speed determines whether finite-sample estimates can be trusted
Implementation: cell3_2_mixing_burnin_widget()
- Runs a Metropolis chain on a bimodal target with
_metropolis_1d(), varying the proposalstepsize - Discards the first
burninsamples, then compares the trace, histogram, and autocorrelation of what remains
hintros.print_obj_info(utils.cell3_2_mixing_burnin_widget)Usage
Inputs
seed: random seed for the chainstep: proposal step size controlling mixing qualityburnin: number of initial samples discardedN(log scale): total iterations
Panels
Trace (first N): the sampled value over iterations, with the burn-in region shadedCollected samples: histogram of kept samples vs the true bimodal posteriorAutocorrelation: autocorrelation vs lag, high for poor mixingComments: the mixing diagnostics
# Tune the step size between poor and good mixing and set the burn-in.
utils.cell3_2_mixing_burnin_widget()Guided usage
- Set
stepvery small, then very large- Observe the trace stay stuck near one mode when
stepis small, but jump between both modes whenstepis large, with autocorrelation dropping to match
- Observe the trace stay stuck near one mode when
- Raise
burninat a smallstep- Observe the histogram still misses one mode: a correct target and a longer burn-in cannot fix a chain that never explores both modes
Cell 3.3: Gibbs sampling and the Markov blanket¶
Goal:
- Specialize MCMC to Bayesian networks with Gibbs sampling: resample one variable at a time from its Markov blanket, evidence held clamped
Implementation: cell3_3_gibbs_sampling_widget()
- Runs
sweepsGibbs sweeps with_gibbs_chain(), resampling each hidden variable from_gibbs_full_conditional()while evidence nodes stay fixed - Tracks the running posterior estimate for
Query Xafter discardingburninsweeps
hintros.print_obj_info(utils.cell3_3_gibbs_sampling_widget)Usage
Inputs
seed: random seed for the sweepssweeps(log scale): number of Gibbs sweepsburnin: burn-in sweeps discardedQuery X: the query variableobserve <node>: one checkbox plus True/False value per node, marking it as evidence
Panels
Resample <var> from its blanket: the DAG with evidence frozen and the resampled variable plus its blanket highlightedP(<var> | blanket): the full conditional currently being sampled fromRunning estimate: vs the exact referenceComments: the hidden variables and the estimate
# Resample each hidden variable from its Markov blanket with evidence clamped.
utils.cell3_3_gibbs_sampling_widget()Guided usage
- Raise
sweepsfrom small to large, leavingburninfixed- Observe the running estimate settle onto the exact reference
- Watch which variable gets resampled across several sweeps
- Observe it cycles through only the hidden variables: evidence variables stay clamped, so no rejection is ever needed
Cell 3.4: Metropolis-Hastings and accept/reject moves¶
Goal:
- Generalize beyond Gibbs to Metropolis-Hastings: correct an arbitrary proposal with an acceptance probability instead of always accepting
Implementation: cell3_4_metropolis_hastings_widget()
- Proposes a local or broad move with probability
p_local, and accepts or rejects it via the Hastings ratio in_mh_chain() - Tracks the running posterior estimate for the fixed query
after discarding
burniniterations
hintros.print_obj_info(utils.cell3_4_metropolis_hastings_widget)Usage
Inputs
seed: random seed for the chainp_local: probability of a local single-variable move vs a broad jumpiters(log scale): number of iterationsburnin: burn-in iterations discarded
Panels
Last proposed move: current vs proposed state, with the acceptance probabilityTrace (accept rate): accepted states over iterations, with the running acceptance rateRunning estimate: vs the exact referenceComments: the proposal mix, acceptance rate, and estimate
# Propose a move, then accept or reject it by the Hastings ratio.
utils.cell3_4_metropolis_hastings_widget()Guided usage
- Lower
p_localtoward 0, favoring broad jumps- Observe the acceptance rate in the trace title drop: broad proposals land in low-probability regions more often and get rejected
- Set
p_localto 1.0- Observe the chain behaves like Cell 3.3’s Gibbs sampler: Gibbs is the special case of Metropolis-Hastings where every proposal is a local move drawn from the exact conditional, so it is always accepted