The Monte Carlo Family

Importance Sampling: reweight
the right samples

Sample from a proposal you can afford, then reweight to the truth — and watch a 1-in-a-million event become reachable.

Prerequisites: Monte Carlo (the 1/√N law, expectations as averages). That's it.
11
Chapters
11
Simulations
0
Assumed Knowledge

Chapter 0: Why — The Event You Never See

You are an insurance actuary, and someone asks a deceptively simple question: what is the chance our portfolio loses more than $10 million in a single day? You have a loss model, so you do the obvious thing — you simulate.

You draw a million possible days from the loss model and count how many cross the $10M line. You let it run overnight. In the morning the counter reads zero. Zero exceedances out of a million draws. Your estimate is 0 / 1,000,000 = 0, with no error bar at all.

The event is real. Catastrophes happen. It just never showed up in your sample, because it is rare. This is the failure mode that importance sampling exists to cure, and to understand the cure we first have to feel exactly how badly crude Monte Carlo fails when the interesting region is small.

You built the basic Monte Carlo estimator in Monte Carlo: to estimate an expectation, draw samples and average. That works beautifully when typical samples carry the answer. The new problem here is that for a rare event, almost every sample carries no information at all — it lands in the boring bulk where the indicator is zero.

Concretely, model a single day's standardized loss as X ∼ N(0,1) (a standard bell curve) and call a "catastrophe" any day past a threshold t. Crude Monte Carlo estimates P(X > t) by the fraction of draws that exceed t. The deeper you push t into the tail, the rarer the event, and the more samples you need before you ever see one.

The widget below makes this tangible: dots rain down from p, almost all landing in the bulk, while a counter tracks how many cross the threshold and the relative-error readout explodes as you push t to the right. Watch the counter stall at zero while the "samples needed" number climbs into the millions.

Crude Monte Carlo vs the Tail

Samples rain down from p = N(0,1). The red threshold is t. Push t right and watch the hit counter stall while the relative error and "samples for 10 hits" blow up.

Threshold t (σ)3.0
Speed60

Let us pin this down with exact arithmetic — no randomness needed, because the normal tail has a closed form. The survival function P(X > t) is the area under the bell curve past t.

P(X > 3) = 0.0013499    and    P(X > 4) = 0.000031671

So at t = 3 the event happens about 1.3 times in 1000; at t = 4 about 3 times in 100,000. To estimate a probability at all, you need at least a handful of hits — say ten. The expected hit count after N draws is N · p, so the N you need to expect ten hits is N = 10 / p.

N10(t=3) = 10 / 0.0013499 ≈ 7408    N10(t=4) = 10 / 0.000031671 ≈ 315744

Read that again: just to see ten catastrophes at the 4σ level you must simulate over three hundred thousand days, and ten hits is a noisy, barely-usable basis for an estimate. Push to a one-in-a-million event and you need millions of draws to see anything. That is the wall.

There is a second, sharper way to say why crude MC hurts here. The single-sample estimator of a rare probability is the indicator 1{X > t}, a coin that is 1 with probability p and 0 otherwise. Its standard deviation is √(p(1−p)), and dividing by the mean p gives the per-sample relative error.

rel-err per sample = √((1−p)/p)

At t = 3 this is √(0.99865 / 0.0013499) = 27.2. Each crude sample carries 27× more noise than signal. Importance sampling will drive this number down by making the rare region common under a proposal of our choosing, then honestly correcting the bias with a weight.

The misconception to kill now: "Zero hits in a million draws means the probability is basically zero, so 0 is a fine estimate." No — 0/N is not a small number, it is no information. Crude MC's per-sample variance is p(1−p); to resolve a probability of 10−6 you need on the order of a million hits, i.e. ~1012 samples. The event's rarity is exactly why crude sampling fails, not why it is safe to ignore.

So we will keep the same underlying truth but change where we look. The whole lesson is one idea pushed to its limits: draw from a distribution that visits the interesting region often, then multiply each sample by an honest correction factor so the answer is still exactly right.

Here is the crude estimator in code — the thing that returns a useless zero — followed by the analytic answer it cannot reach.

python — crude MC fails on the tail
import numpy as np
rng = np.random.default_rng(0)
N = 1_000_000; t = 4.0
x = rng.standard_normal(N)
hits = (x > t).sum()
p_hat = hits / N            # often 0 -> useless
rel_err = np.sqrt((1-p_hat)/p_hat)/np.sqrt(N) if p_hat>0 else np.inf
print(hits, p_hat, rel_err)
python — the analytic answer crude MC can't reach
from scipy.stats import norm
p_true = norm.sf(4)   # 3.17e-05, the truth
Crude Monte Carlo draws 1,000,000 samples from N(0,1) to estimate P(X>4) ≈ 3.2×10−5. Roughly how many samples land past 4?

Chapter 1: Reweight — Sample Here, Answer There

Your problem in Chapter 0 was that p almost never visits the dangerous region. So let's cheat — deliberately. We draw samples from a proposal q that we get to choose, one that does visit the tail, by pushing its mean toward the danger zone.

But now the samples are biased toward danger: they over-represent the tail and under-represent the bulk. If we just averaged them we would get the wrong answer. The fix is pure bookkeeping. Each sample x carries an honest correction factor — the importance weight w(x) = p(x)/q(x) — that says "q over-produced this point, so down-weight it."

The magic is that this correction is exact, not approximate. Watch the algebra. We want an expectation under p, and we multiply and divide by q inside the integral:

Ep[f] = ∫ p(x) f(x) dx = ∫ q(x) · (p(x)/q(x)) · f(x) dx = Eq[ w(x) f(x) ]

Read it left to right: the expectation of f under p equals the expectation of w(x)f(x) under q. So we can sample from q, compute w(x)f(x) for each draw, average, and get Ep[f]. This is the importance sampling identity, and it is the entire foundation of the lesson.

Ep[f] ≈ (1/N) ∑i w(xi) f(xi),   xi ∼ q,   w = p/q

The only requirement is that q is nonzero wherever p·f is nonzero (you can't reweight a region you never sample). Subject to that, the identity holds for any valid q. The estimator is unbiased no matter where q sits. What q changes is not the mean — it's the variance, which is the subject of the next chapter.

Play with the widget: drag q's mean and width. Dots are drawn from q, so they cluster under q's hump. Each dot's size encodes its weight w = p/q — dots in q's over-sampled region shrink (down-weighted), dots in p-favored regions grow. The weighted average tracks the true Ep[f] no matter where you drag q.

The Weight w = p/q Makes Any Proposal Honest

Target p = N(0,1), proposal q. Dots are drawn from q; dot size = weight w = p/q. The running IS estimate of Ep[X] tracks the truth (0) wherever you drag q.

q mean3.0
q sigma1.0

Let's compute weights by hand to demystify them. Take p = N(0,1) and q = N(3,1). Both are Gaussians, so the ratio of their densities collapses to a clean exponential — the normalizing constants cancel and only the exponents remain:

w(x) = φ(x) / φ(x−3) = exp(−x²/2 + (x−3)²/2) = exp(−3x + 4.5)

Now evaluate at three fixed points (no randomness — this is deterministic arithmetic). At x = 2.5: exponent = −3(2.5) + 4.5 = −7.5 + 4.5 = −3, so w = e−3 = 0.049787. At x = 3.5: −10.5 + 4.5 = −6, so w = e−6 = 0.002479. At x = 4.0: −12 + 4.5 = −7.5, so w = e−7.5 = 0.000553.

Every weight is far below 1, and they shrink fast as x grows. That makes sense: q is centered at 3, so it over-represents the deep tail relative to p (centered at 0), and the correction pulls those over-sampled points sharply down. The weight is doing exactly its job — undoing q's bias point by point.

Sanity check the cancellation: compute w at x = 3.5 from the raw normal kernels instead. p-kernel = exp(−3.5²/2) = exp(−6.125); q-kernel = exp(−(3.5−3)²/2) = exp(−0.125); ratio = exp(−6.125 + 0.125) = exp(−6) = 0.002479. Same answer — the √(2π) factors really do cancel.
The misconception: "Reweighting introduces bias, since I'm not sampling from the real distribution." Exactly backwards — the weight w = p/q removes the bias q introduced. The identity Eq[w·f] = Ep[f] is exact for any valid q. Reweighting is what makes a convenient proposal honest, not what corrupts it.

Here is the whole estimator in code. We sample around 3 yet recover Ep[X] ≈ 0.

python — importance sampling from scratch
import numpy as np
from scipy.stats import norm
rng = np.random.default_rng(0)
N = 100_000
x = rng.normal(3.0, 1.0, N)               # draw from proposal q
w = norm.pdf(x, 0, 1) / norm.pdf(x, 3, 1)   # p/q correction
f = x                                      # estimating E_p[X] (should be ~0)
est = np.mean(w * f)
print(est)                                   # ~0 despite sampling around 3
python — the whole estimator is one line
est = np.mean(w * f)   # a weighted average, nothing more
You draw x from q = N(3,1) but want an expectation under p = N(0,1). For a sample at x = 2.5, what weight does it carry, and why?

Chapter 2: Variance — The Two-Faced Estimator

You just learned IS is unbiased for any q. So why not pick q sloppily? Because unbiased does not mean accurate. Run the same IS estimator with two different proposals and one converges in a hundred samples while the other still flails after a million. The mean is right either way; the spread is what kills you.

Importance sampling is a sharp knife. With a proposal that hugs where the integrand has its mass, it cuts variance dramatically below crude MC. With a proposal that misses that mass, it cuts you — the variance can be larger than crude MC, even literally infinite. Same estimator, two faces.

The variance of the IS estimator is the variance of the random quantity w(x)f(x) under q, divided by N. So we care about the second moment Eq[(w f)2]. Writing it out exposes the danger:

Eq[(w f)²] = ∫ q(x) (p(x)/q(x))² f(x)² dx = ∫ (p(x)² f(x)² / q(x)) dx

Look at where q sits: in the denominator. Wherever q is tiny but p·f is not, the integrand blows up. If q has lighter tails than p (it decays faster), then far out in the tail p/q → ∞, this integral can diverge, and the IS variance is infinite even though the estimate stays unbiased. That is the whole story of IS variance in one line.

Run the widget to feel it. One slider morphs q from "matched" to "mismatched." Watch the right-hand running estimate destabilize: it sits flat, then leaps when one giant weight finally appears. The weight histogram develops a fat tail with a single dominating spike — the visual signature of exploding IS variance.

Good q Converges; Bad q Lurches

Both panels estimate E[X²] = 1 with p = N(0,1). Left uses a heavier-tailed q (safe); right shrinks q's width below p (dangerous). Watch the right trace lurch and its max/mean weight ratio explode.

bad-q sigma0.60

Let's anchor the safe case with exact arithmetic. Estimate Ep[f] with p = N(0,1) and f(x) = x². The truth is Ep[x²] = 1 (the variance of a standard normal). Take the "do-nothing" proposal q = p, so w = 1 everywhere and IS reduces to plain MC.

The per-sample variance of plain MC here is Var(x²) = E[x4] − (E[x²])2. The fourth moment of a standard normal is E[x4] = 3 (a fact you can look up or derive from the moment generating function). So:

Varp(x²) = E[x4] − (E[x²])² = 3 − 1² = 2

That 2 is the baseline. A heavier-tailed q (larger width) keeps w = p/q bounded and can push the per-sample variance below 2. A lighter-tailed q makes w blow up exactly where x² is largest, so Eq[(w x²)2] can diverge — the variance goes to infinity while the mean stays glued to 1.

The misconception: "A wider proposal is always safer than a narrow one." Width per se isn't the rule — what matters is that q has heavier tails than p·|f| everywhere mass lives, so w = p/q stays bounded. The fatal case is a proposal lighter-tailed than the target: a few samples get monstrous weights and the IS variance can be literally infinite, even though the estimate is still unbiased.

The practical takeaway: in IS you watch the variance, not the mean. A stable-looking running mean that suddenly jumps is your alarm bell.

python — same mean, wildly different variance
import numpy as np
from scipy.stats import norm
rng = np.random.default_rng(1)
N = 200_000; f = lambda x: x**2
def is_est(q_sigma):
    x = rng.normal(0, q_sigma, N)
    w = norm.pdf(x,0,1)/norm.pdf(x,0,q_sigma)
    vals = w*f(x)
    return vals.mean(), vals.var()   # mean ~1 always; var explodes if q_sigma<1
print(is_est(1.5))   # safe (heavier tails)
print(is_est(0.5))   # dangerous (lighter tails)
python — watch var, not mean
mean, var = (w*f(x)).mean(), (w*f(x)).var()
Your IS run's estimate sits near 0.3 for thousands of samples, then suddenly leaps to 8 when one new sample appears. What does this signature mean?

Chapter 3: Optimal q — The Proposal That Beats Noise

If choosing q changes variance, what is the best possible q? Intuitively, put your samples exactly where the integrand f(x)p(x) is large — don't waste a single draw on regions that contribute nothing. Push that intuition to its limit and something startling falls out.

The variance-minimizing proposal for estimating Ep[f] (with f ≥ 0) is

q*(x) = |f(x)| p(x) / Z,   where Z = Ep[|f|] = ∫ |f(x)| p(x) dx

That is, q* is shaped like the product |f|·p, renormalized to integrate to 1. Now watch what happens to the weighted integrand under q*. We substitute q* into w(x)f(x) = (p/q*)·f and the p and f factors cancel:

w(x) f(x) = (p(x) / q*(x)) f(x) = p(x) f(x) / ( f(x) p(x) / Z ) = Z

The weighted integrand is a constant — it equals Z for every x. A constant random variable has zero variance. So under q*, a single sample returns the exact answer Z with no error at all. This is the zero-variance proposal, and it is the north star that tells us which way to push q.

Make it concrete with p = N(0,1) and f(x) = x². Then Z = Ep[x²] = 1, and q*(x) = x²·φ(x)/1. Check the cancellation at two arbitrary points. At x = 2: q*(2) = 4·φ(2), so w = φ(2)/(4φ(2)) = 1/4, and w·f = (1/4)·4 = 1. At x = 0.5: q*(0.5) = 0.25·φ(0.5), so w = 1/0.25 = 4, and w·f = 4·0.25 = 1. Both give exactly Z = 1.

Notice how the weight and the integrand trade off perfectly: where f is large the weight is small, where f is small the weight is large, and the product is pinned at Z everywhere. That balance is the entire point.

Play with the widget: a blend slider morphs q from p toward q* ∝ |f|p. As you approach q*, the per-sample w·f bars flatten to a single level and the variance readout collapses toward zero. Then flip the "reveal the catch" toggle.

Blend Toward q* and Watch Variance Collapse

The curve is |f(x)|p(x) (the mass to chase). Bars below are per-sample w·f at fixed points. Slide from q = p toward q* ∝ |f|p and watch the bars flatten to the constant Z and the variance hit ~0.

blend p → q*0.00
The misconception: "Great, just use q* ∝ |f|p and get zero-variance estimates for free." The optimal proposal is a unicorn: its normalizing constant Z is exactly Ep[|f|], the quantity you set out to estimate. If you knew Z you wouldn't be sampling at all. q* is a target to approximate, not a recipe — it tells you to put samples where |f|p is large, which for rare events means tilt q toward the rare region (Chapter 6).

So the practical doctrine is: you cannot have q*, but you can imitate it. Match the shape of |f|p, and crucially keep tails at least as heavy as p·|f| (Chapter 2's warning) so the imitation never blows up.

python — the integrand is constant under q*
import numpy as np
from scipy.stats import norm
# f = x^2, Z = E_p[x^2] = 1, so q* = x^2 * phi(x) / Z
Z = 1.0
for x in [0.5, 2.0, -1.3]:
    p = norm.pdf(x); qstar = (x**2 * p) / Z
    w = p/qstar; print(x, w*x**2)   # all == Z exactly
python — the recipe is guidance, not code
# pick q with shape ~ |f(x)|*p(x), and tails >= p*|f|
Why can't you actually use the optimal proposal q*(x) ∝ |f(x)|·p(x) in practice?

Chapter 4: ESS — How Many Samples Do You Really Have?

You drew 1000 weighted samples — but one of them has weight 100 and the rest have weight 1. Is that really 1000 samples' worth of information? Obviously not: the weighted average is dominated by that single sample. You would be a fool to trust it like an honest 1000.

There is a one-line diagnostic that converts "N weighted samples" into "how many fair, equal-weight samples this is actually worth." It is the effective sample size, and it is the single most important health check in all of importance sampling.

ESS = (∑i wi)² / ∑i wi²

Read the formula's behavior at its two extremes. If all N weights are equal, the numerator is (Nw)² = N²w² and the denominator is Nw², so ESS = N — every sample counts fully. If one weight dwarfs all others, the sums are both dominated by that one term, the ratio collapses toward 1 — you effectively have a single sample.

Why this exact formula? It comes from setting the variance of a weighted mean of equal-information samples equal to the variance you actually have. The ratio (∑w)²/∑w² is precisely the "equivalent number of equal samples" — it is the inverse of the sum of squared normalized weights, which is a standard concentration measure.

Let's compute it by hand for two fixed weight vectors of length N = 5 — pure arithmetic, no randomness. First a moderately skewed vector w = [0.1, 0.2, 0.05, 4.0, 0.3].

Sum: 0.1 + 0.2 + 0.05 + 4.0 + 0.3 = 4.65, so (∑w)² = 4.65² = 21.6225. Sum of squares: 0.01 + 0.04 + 0.0025 + 16.0 + 0.09 = 16.1425. Therefore ESS = 21.6225 / 16.1425 = 1.3395. Five skewed samples are worth barely more than one — that single weight of 4.0 eats the set.

Now a degenerate vector w = [1, 1, 1, 1, 100]. Sum: 4 + 100 = 104, so (∑w)² = 10816. Sum of squares: 4×1 + 100² = 4 + 10000 = 10004. So ESS = 10816 / 10004 = 1.0812 — essentially one effective sample despite having five. The lone giant weight swallows everything.

Drag the skew slider in the widget and watch the ESS dial swing from N (healthy, even weights) down toward 1 (degenerate, one spike). The efficiency readout ESS/N tells you what fraction of your sampling effort is actually paying off.

The ESS Dial: Effective Samples from Uneven Weights

Bars are normalized weights for N = 12 samples. Slide "skew" to concentrate weight onto fewer samples and watch ESS collapse from 12 toward 1. The dial and ESS/N efficiency update live.

skew0.20
The misconception: "High ESS guarantees my estimate is correct." ESS measures weight evenness only, not whether q actually covered the important region. A proposal that completely misses the tail can have perfectly uniform weights (high ESS) yet be badly biased toward whatever it did sample. ESS is a necessary health check, not a sufficient one: low ESS is a definite red flag, high ESS is reassurance, not proof.

In practice you compute ESS every run and treat a low value as a stop sign — re-center q, widen it, or reach for a more powerful method. We'll use ESS again in Chapter 6 (over-tilting), Chapter 9 (too few annealing rungs), and Chapter 10 (the universal diagnostic).

python — effective sample size from scratch
import numpy as np
def ess(w):
    w = np.asarray(w, float)
    return w.sum()**2 / (w**2).sum()
print(ess([0.1,0.2,0.05,4.0,0.3]))   # 1.3395
print(ess([1,1,1,1,100]))            # 1.0812 -> degenerate
python — equivalent one-liner
ess = (w.sum()**2) / (w**2).sum()   # or 1/np.sum((w/w.sum())**2)
You have N = 5 weighted samples with weights [1, 1, 1, 1, 100]. What is the effective sample size, and what does it mean?

Chapter 5: Self-norm — When You Only Know p Up to a Constant

In Bayesian inference your posterior is p(θ|data) = likelihood × prior / evidence, and the evidence in the denominator is a brutal integral you usually cannot compute. So you can write down the posterior only up to that mystery constant. You know the shape, not the scale.

Standard IS needs the true normalized p(x) inside the weight w = p/q. If you only have an unnormalized p̃(x) = c·p(x) for some unknown c, ordinary IS gives you c·Ep[f] — off by the very factor you can't compute. Dead end? No. There is a trick so clean it feels like a cancellation by magic.

Form the weights from the unnormalized density, then divide the weighted sum by the sum of the weights. The unknown constant appears in both the numerator and the denominator, so it cancels. This is self-normalized importance sampling (SNIS).

Ep[f] ≈ ( ∑ii f(xi) ) / ( ∑ii ),   w̃ = p̃/q

Because w̃ = c·w, both sums scale by c, and c divides itself out. As a free bonus, the denominator mean(w̃) is itself an estimate of the unknown normalizer — we recover Ẑ without ever computing the evidence integral directly.

Let's do it by hand with four fixed samples. Take unnormalized target p̃(x) = exp(−x²/2)·exp(x), which is N(1,1) times an unknown constant. Use proposal q = N(0,1). The normal kernels cancel cleanly: w̃(x) = p̃(x)/q(x) = exp(−x²/2 + x)/exp(−x²/2) = exp(x).

Evaluate the unnormalized weights at the fixed samples x = [−1, 0, 1, 2]: w̃ = [e−1, e0, e1, e2] = [0.3679, 1.0, 2.7183, 7.3891]. Their sum is 0.3679 + 1.0 + 2.7183 + 7.3891 = 11.4753.

Now the weighted sum of x: (−1)(0.3679) + (0)(1.0) + (1)(2.7183) + (2)(7.3891) = −0.3679 + 0 + 2.7183 + 14.7782 = 17.1286. Divide:

SNIS estimate of E[X] = 17.1286 / 11.4753 = 1.4927

The true mean of N(1,1) is 1; with only four samples we land at 1.49, in the right ballpark — and crucially we never needed the normalizer of p̃, because it cancelled in the ratio. With more samples this converges to 1 exactly.

In the widget, a slider scales p̃ by an arbitrary constant c (mimicking the unknown normalizer). Ordinary IS shifts with c — broken. SNIS stays pinned on the truth — visual proof the constant cancels. The recovered Ẑ tracks c on its own.

SNIS Ignores the Unknown Constant

Target p̃ = N(1,1) up to a constant c (slider). Ordinary IS drifts with c (wrong); SNIS stays on the true mean = 1. The recovered follows c for free.

unknown c1.0
The misconception: "Self-normalized IS is unbiased, just like ordinary IS." It is not unbiased for finite N — it's a ratio of two random sums, and ratios are biased. The bias is O(1/N) and vanishes as N grows (the estimator is consistent), so it's perfectly usable; but if you claim unbiasedness you'll be wrong, and surprised when small-N SNIS estimates sit slightly off. The vanishing bias is the price for handling an unnormalized target.

This single trick is what makes IS viable across Bayesian posteriors, statistical physics (where the partition function is the unknown constant), and variational inference. Almost every real target you meet is unnormalized; SNIS is how you cope.

python — self-normalized IS from scratch
import numpy as np
from scipy.stats import norm
rng = np.random.default_rng(2)
x = rng.standard_normal(50_000)            # proposal q = N(0,1)
log_ptilde = -x**2/2 + x                    # unnormalized log target (N(1,1)*const)
w = np.exp(log_ptilde - norm.logpdf(x))     # w-tilde = exp(x)
est = np.sum(w * x) / np.sum(w)             # SNIS -> ~1
print(est)
python — a weighted mean IS self-normalized IS
est = np.average(f(x), weights=w)   # numpy does the ratio for you
Self-normalized IS divides the weighted sum by the sum of weights. What does this buy you, and what does it cost?

Chapter 6: Rare events — Tilting Toward Disaster

Back to the catastrophe estimate from Chapter 0 — the one that returned a useless 0. Now you have the machinery: the IS identity (Chapter 1), the variance warning (Chapter 2), the q* north star (Chapter 3), and ESS to keep you honest (Chapter 4). It is time to actually estimate a rare-event probability.

Don't sample from the loss model and pray. Sample from a tilted model — a proposal shifted so that most samples land in the danger zone — then reweight each by w = p/q to undo the tilt. A million-to-one event now shows up in every batch, and your estimate finally has an error bar.

This is the killer application of importance sampling. In structural reliability you tilt toward the failure surface; in finance you tilt toward the Value-at-Risk threshold; in telecom you tilt toward buffer overflow. The recipe is always the same: shift mass into the rare region, then correct.

Let's estimate P(X > 3) for X ∼ N(0,1), whose true value is 0.0013499. Tilt the proposal to q = N(3,1), centered exactly on the threshold so that roughly half the samples cross it. The weight is the same closed form we derived in Chapter 1: w(x) = exp(−3x + 4.5).

Use four fixed proposal samples x = [2.5, 3.2, 3.5, 4.0] and the indicator f = 1{x > 3}. First the weights. w(2.5) = exp(−7.5 + 4.5) = exp(−3) = 0.049787. w(3.2) = exp(−9.6 + 4.5) = exp(−5.1) = 0.006097. w(3.5) = exp(−10.5 + 4.5) = exp(−6) = 0.002479. w(4.0) = exp(−12 + 4.5) = exp(−7.5) = 0.000553.

Now the indicators. Only x = 2.5 fails the test x > 3, so 1{x > 3} = [0, 1, 1, 1]. Multiply weights by indicators: w·f = [0, 0.006097, 0.002479, 0.000553]. The IS estimate is the average:

P̂(X>3) = (0 + 0.006097 + 0.002479 + 0.000553) / 4 = 0.009129 / 4 = 0.002282

From just four samples we land at 0.00228, the same order of magnitude as the truth 0.00135 — whereas crude MC would need around 7400 samples just to see ten hits. The tilt-then-correct trick has bought us a factor of roughly two thousand in efficiency, and more for rarer events.

In the widget, slide q's mean from 0 toward and past the threshold. Watch the IS estimate's error bar shrink dramatically as q approaches the threshold — while crude MC stays stuck at 0/N. Push q too far past t and the ESS readout drops, teaching the Goldilocks lesson below.

Tilt the Proposal, Estimate the Tail

Estimating P(X>3) with p = N(0,1). Crude MC (dots from p) rarely crosses t; IS (dots from q) crosses often and reweights by w. Slide q's mean; watch IS lock onto the truth and ESS warn you if you over-tilt.

q mean3.0
The misconception: "To estimate a rarer event, just push the proposal mean further into the tail — more is better." Over-tilting backfires. If q is shifted way past the threshold, the few samples that fall just inside the region get enormous weights w = p/q (because q barely covers them), ESS collapses, and variance returns. The sweet spot centers q around the threshold (near where q* ∝ |f|p concentrates), not deep beyond it. The tilt is a Goldilocks knob, not a "turn it to max" knob.

This connects directly to Chapter 3: the optimal proposal for a tail probability concentrates right at the boundary of the rare set, where the indicator switches on but p still has appreciable density. Centering q at the threshold is a cheap, excellent imitation of q*.

python — rare-event probability by tilting
import numpy as np
from scipy.stats import norm
rng = np.random.default_rng(3)
N = 10_000; t = 3.0
x = rng.normal(3.0, 1.0, N)              # tilt q toward the tail
w = norm.pdf(x,0,1)/norm.pdf(x,3,1)      # = exp(-3x+4.5)
est = np.mean(w * (x > t))               # ~0.00135 with a real error bar
print(est, norm.sf(t))
python — with the closed-form weight inlined
est = np.mean(np.exp(-3*x+4.5) * (x > 3))   # tilted estimate of P(X>3)
To estimate P(X>3) for X~N(0,1), why shift the proposal to q = N(3,1) rather than sampling from p directly?

Chapter 7: Off-policy — Grading a Policy You Never Ran

A hospital has months of logged treatment decisions made under its current protocol — the behavior policy b. You have designed a new target policy π and you want its expected outcome without running it on real patients. You cannot re-collect data under π; that experiment is exactly what you're trying to avoid.

This is off-policy evaluation, and importance sampling is the classical answer. A trajectory is just a sequence of states and actions; the probability of a trajectory factors over its steps. Reweighting from b to π means multiplying by the ratio of trajectory probabilities — and because the environment dynamics cancel (they're shared), only the policy ratios remain.

You met the per-step ideas in policy gradients and Q-learning; here the new idea is the product of per-step ratios and why it is a variance time bomb.

ρ = ∏t π(at|st) / b(at|st),   V̂π = ρ · G

Here G is the trajectory's observed return and ρ is the cumulative importance ratio. Multiply the return by ρ and you get an unbiased estimate of the target policy's value from a behavior-policy trajectory. The danger lives entirely in that product symbol.

Let's compute it for a 3-step trajectory. Along the actions actually taken, the target probs are π = [0.8, 0.5, 0.9] and the behavior probs are b = [0.4, 0.5, 0.3], with observed return G = 2.0. The per-step ratios are π/b = [0.8/0.4, 0.5/0.5, 0.9/0.3] = [2.0, 1.0, 3.0].

The cumulative weight is the product: ρ = 2.0 × 1.0 × 3.0 = 6.0. The IS return estimate is ρ·G = 6.0 × 2.0 = 12.0. So this one logged trajectory, which earned a return of 2, counts as evidence for a target-policy value of 12 — because π was six times more likely to take this exact path than b was.

Now the variance bomb, with a tiny toy. Suppose each step's ratio equals 2 or 0 with equal probability (mean 1, so still unbiased). Then E[ratio²] per step = 0.5·4 + 0.5·0 = 2. Over H = 10 independent steps, E[ρ²] = 210 = 1024, so Var(ρ) = 1024 − 1 = 1023.

E[ρ²] = (E[ratio²])H = 210 = 1024  ⇒  Var(ρ) = 1023

The second moment grows exponentially in the horizon H. Double the trajectory length and the variance roughly squares. This is the curse of long-horizon off-policy evaluation, and it is why importance sampling alone struggles on anything but short episodes.

In the widget, a horizon slider lengthens the trajectory and a mismatch slider widens the gap between π and b. Watch the spread of cumulative weights across many sampled trajectories balloon as either grows. Toggle to weighted IS (WIS) to see its tighter but slightly biased spread.

The Horizon Curse of Off-Policy Weights

Each trajectory's per-step ratios multiply into a cumulative weight ρ. Slide the horizon and the policy mismatch; the histogram of ρ across trajectories fans out exponentially. Toggle WIS (self-normalized) to tame — but slightly bias — the spread.

horizon H6
policy mismatch0.40

The latest research lives here. Modern off-policy evaluation for LLM agents, recommender systems, and clinical policies leans on doubly-robust estimators (2024–2026) that combine an IS correction with a learned value model: the value model handles the bulk, IS corrects the residual, and the variance is a fraction of pure IS. Weighted IS and per-decision IS are the building blocks underneath them.

The misconception: "Per-decision IS (weighting each reward only by ratios up to its own timestep) fixes the variance explosion." Per-decision IS genuinely helps — a reward at time t needn't be weighted by future ratios — but variance still grows with horizon because early-step ratios keep multiplying. The exponential-blowup killers need weighted IS (self-normalizing the trajectory weights, trading a little bias for big variance cuts) and doubly-robust estimators. There is no free lunch from per-decision IS alone.
python — off-policy IS return estimate
import numpy as np
# logged trajectory: action probs under behavior b and target pi, plus return G
pi = np.array([0.8, 0.5, 0.9]); b = np.array([0.4, 0.5, 0.3]); G = 2.0
rho = np.prod(pi / b)            # cumulative importance ratio = 6.0
is_return = rho * G             # ordinary IS estimate = 12.0
print(rho, is_return)
python — weighted IS across many trajectories
value_hat = np.average(returns, weights=rhos)   # self-normalized, lower variance
A target policy's per-step action ratios π/b along a trajectory are [2.0, 1.0, 3.0], and the trajectory's return is G = 2.0. What is the ordinary importance-sampled return estimate?

Chapter 8: Renderer — Sampling the Light That Matters

You're rendering a glossy floor. Each pixel's color is an integral over all incoming light directions, weighted by how the surface reflects them. The reflectance function is the BRDF (bidirectional reflectance distribution function), and the pixel value is the integral of incoming radiance times the BRDF times a cosine term over the hemisphere.

Sample directions uniformly over the hemisphere and most rays fly off where the surface barely reflects anything — wasted samples, and the image is a blizzard of noise. Importance sampling says: draw directions from the reflection lobe itself, so every ray lands where light actually contributes. This is THE reason every offline film frame is renderable at all.

This is importance sampling in disguise. The "proposal" q is the pdf you sample directions from; the "weight" is the estimator f(dir)/pdf(dir). Matching pdf to the integrand is exactly Chapter 3's q* idea, now in a graphics costume.

Consider a Lambertian (matte) surface with constant incoming radiance L = 1. The pixel integral is the integral over the hemisphere of L·cos(θ)/π. The single-sample estimator is f(dir)/pdf(dir). Let's compare two pdfs at a direction with cos(θ) = 0.8.

The integrand there is f = L·cos/π = 0.8/π = 0.2546. Under uniform sampling the pdf is 1/(2π) = 0.15915, so the estimator is f/pdf = 0.2546/0.15915 = 1.6. At a grazing direction cos = 0.2 the same uniform estimator is (0.2/π)/(1/(2π)) = 0.4. Uniform estimates swing from 0.4 to 1.6 across directions — high variance.

Under cosine-weighted sampling the pdf is cos/π = 0.8/π = 0.25465, so the estimator is f/pdf = 0.2546/0.25465 = 1.0. At cos = 0.2 it is (0.2/π)/(0.2/π) = 1.0 again — in fact it is 1.0 at every direction, because the cos in the integrand cancels the cos in the pdf.

f/pdf = (L·cos/π) / (cos/π) = L   for every direction ⇒ zero variance

A constant estimator has zero variance: for constant radiance, cosine-weighted sampling renders the exact pixel value from a single sample. This is the textbook win of BRDF importance sampling — the proposal perfectly cancels the integrand, just like q* in Chapter 3.

This is the showcase. Toggle uniform vs cosine sampling; sharpen the lobe with the glossiness slider (making good sampling matter more); sweep the sample count. Watch the two pixel patches: the importance-sampled one converges smoothly to clean while the uniform one stays grainy at the same budget. The f/pdf bars tell the whole story — uniform's vary wildly, cosine's are flat.

Uniform vs BRDF Sampling: Same Image, Different Noise

Left hemisphere = sampling pattern; right = the converging pixel patch + its f/pdf spread. Toggle the sampler, sharpen the lobe, sweep N. Cosine sampling's estimator is constant (flat bars, zero variance); uniform's is noisy.

glossiness6
samples / frame10
Both are correct, only one is usable: uniform and cosine sampling are both unbiased and converge to the same image. But at any affordable sample count the uniform render is unusably noisy while the BRDF-sampled one is clean. In production you never reach "enough" uniform samples — importance sampling is what makes the integral tractable at all. Same mean, radically different variance, and variance is the entire game in rendering.
python — uniform vs cosine estimator variance
import numpy as np
rng = np.random.default_rng(4)
N = 20_000; L = 1.0
# uniform hemisphere: pdf = 1/(2pi)
c_u = rng.random(N)                       # cos(theta) proxy ~ uniform
est_u = (L*c_u/np.pi)/(1/(2*np.pi))    # f/pdf, high variance
# cosine-weighted: pdf = cos/pi -> estimator is L, constant
c_c = np.sqrt(rng.random(N))             # cosine-weighted cos(theta)
est_c = (L*c_c/np.pi)/(c_c/np.pi)         # all == L
print(est_u.var(), est_c.var())          # cosine variance ~ 0
python — the one line every path tracer runs
# wi = brdf.sample(); weight = brdf(wi)*cos / pdf(wi)   # f/pdf at the sampled direction

No quiz here — the simulation is the test. Toggle the sampler back and forth at low sample count and watch which patch you would actually ship.

Chapter 9: Annealed IS — Bridging Two Worlds With a Chain of Proposals

Your proposal q and target p are so different that a single p/q reweighting has near-zero ESS — every weight is either 0 or astronomical. From Chapter 4 you know that means you effectively have one sample no matter how many you draw. More draws don't help when q and p barely overlap.

The fix is a relay race. Don't jump from q to p in one leap; lay down a ladder of intermediate distributions that morph gradually from the easy q to the hard p, and let importance weights accumulate one rung at a time. This is annealed importance sampling (AIS), and it is the doorway to the entire Sequential Monte Carlo lesson next door.

The intermediate distributions are usually geometric blends: pβ ∝ p0(1−β)·p1β as β sweeps from 0 to 1. At each rung you nudge a particle with an MCMC move targeting pβ, then multiply in an incremental weight — the ratio of consecutive unnormalized densities. The accumulated weight estimates the normalizer ratio Z1/Z0.

Let's see the telescoping with a single deterministic particle. Take base p0 = N(0,1) with normalizer Z0 = 1, and unnormalized target p̃1(x) = exp(−x²/2)·exp(x), which is N(1,1) up to a constant. The true normalizer ratio is the Gaussian integral:

Z1/Z0 = ∫ exp(−x²/2 + x) dx / ∫ exp(−x²/2) dx = exp(0.5) = 1.648721

Now hold a particle at x = 0.5 (no MCMC move, for illustration) and let the per-rung density ratios chain. The intermediate-density ratios telescope — consecutive numerators and denominators cancel — so the product collapses to p̃1(x)/p0(x) = exp(x) regardless of how many rungs you used.

At x = 0.5 the accumulated weight is exp(0.5) = 1.6487, which is exactly the true normalizer ratio Z1/Z0 = exp(0.5) = 1.6487. So the average of accumulated AIS weights recovers the otherwise-intractable normalizer ratio — the "evidence" or partition-function ratio that Bayesian model comparison and IWAE-style bounds both crave.

This is the showcase. The widget shows a ladder of densities morphing from p0 to p1; a particle walks down it; the accumulated log-weight grows rung by rung; and the running Z1/Z0 estimate and ESS update live. Crank the "rungs" slider from 1 (single-step IS) upward and watch ESS climb as the estimate stabilizes onto exp(0.5).

The Annealing Ladder: Many Easy Reweightings Beat One Impossible One

Particles walk a ladder from p0 = N(0,1) to p1 = N(1,1), accumulating weights. With 1 rung, ESS is awful; add rungs and ESS climbs while the Z1/Z0 estimate locks onto exp(0.5) ≈ 1.6487.

rungs1
gap (target mean)1.0
The misconception: "More intermediate distributions just cost more compute for the same answer — one step would do if I were patient." The ladder isn't a speed knob, it's a variance knob with no single-step substitute. When q and p barely overlap, single-step IS has effectively zero ESS no matter how many samples you draw, so the estimate never converges. Adding rungs makes each reweighting overlap-friendly, multiplying many well-behaved weights instead of one catastrophic one. AIS can estimate normalizer ratios that single-step IS literally cannot.

This is a bridge, not the destination. The full unbiased log-Z estimator — averaging accumulated weights with the log-mean-exp machinery — belongs to Sequential Monte Carlo, which generalizes AIS by adding resampling between rungs. Here we just plant the seed: chain IS reweightings with MCMC moves along a tempering ladder.

python — annealed importance sampling (sketch)
import numpy as np
rng = np.random.default_rng(5)
def logp0(x): return -x**2/2
def logp1(x): return -x**2/2 + x          # unnormalized target
betas = np.linspace(0,1,8)                  # the annealing ladder
x = rng.standard_normal(10_000); logw = np.zeros_like(x)
for b0,b1 in zip(betas[:-1], betas[1:]):
    logw += (b1-b0)*(logp1(x)-logp0(x))     # incremental AIS weight
    # (here: + an MCMC move targeting p_b1 between rungs)
Zratio = np.exp(logw).mean()                # -> ~exp(0.5) = 1.6487
print(Zratio)
python — the bridge to SMC
# AIS = a chain of IS reweightings with MCMC moves; the SMC lesson adds resampling

No quiz here — the simulation is the test. Set rungs to 1 with a wide gap and watch ESS collapse; then add rungs and watch it recover.

Chapter 10: Connections — Where the Weights Lead

You started Chapter 0 with a useless 0 / 1,000,000. You now hold a precision instrument: sample where it's convenient, reweight to where it's true, and always check ESS to know whether you can trust the answer. That single discipline — w = p/q, then watch ESS — is the whole lesson.

But every chapter also hinted at a wall. Long horizons make the off-policy weight product explode (Chapter 7). Near-disjoint q and p collapse the ESS to one (Chapter 9). Lighter-tailed proposals send the variance to infinity (Chapter 2). Those walls are exactly where importance sampling hands off to its more powerful cousins.

Here is the map. Importance sampling is the reweighting primitive sitting underneath rare-event simulation, off-policy RL, modern variational inference, and sequential Monte Carlo. Hover the nodes in the widget to surface each one's idea and its characteristic failure mode; the same ESS diagnostic routes you between them.

The IS Reweighting Map

Central node = w = p/q. Branches reach the methods IS powers. Click a node to surface its modern anchor and its variance pitfall (weight collapse, horizon blow-up, disjoint support). The toggle overlays each branch's failure mode.

Click a node to read its summary.

Let's close with a capstone consistency check that ties three chapters together with one shared number. Take the degenerate weight vector from Chapter 4, w = [1, 1, 1, 1, 100], and ask what it implies for every downstream application — rare-event, off-policy, AIS. They all read the same diagnostic.

We already computed ESS = (∑w)²/∑w² = 104²/10004 = 1.0812. The efficiency is ESS/N = 1.0812/5 = 0.2162, i.e. about 22% — these five weighted samples carry the information of barely one.

ESS = 104²/10004 = 1.0812,   ESS/N = 1.0812/5 = 0.2162 ≈ 22%

Whether this weight vector came from a rare-event tilt that overshot (Chapter 6), an off-policy trajectory product (Chapter 7), or a too-coarse AIS ladder (Chapter 9), the verdict is identical: the proposal or bridge is degenerate, trust nothing, and add structure. Re-center q, switch to weighted IS or a doubly-robust estimator, or add AIS rungs and move to SMC. One diagnostic, every application.

Wall you hitWhat it looks likeWhere to go next
Lighter-tailed qInfinite variance, max weight dominatesWiden q / match q* (Ch 2, 3)
q and p barely overlapESS ≈ 1, estimate never convergesAnnealed IS → SMC (Ch 9)
Long horizon productsVar(ρ) grows exponentially in HWIS + doubly-robust (Ch 7)
Need exact target samplingIS can't draw from p, only reweightMCMC (a chain whose stationary dist is p)
The misconception: "Importance sampling and MCMC are competing alternatives — pick one." They're complementary layers, not rivals. IS reweights samples from a proposal; MCMC builds a chain whose stationary distribution is the target; annealed IS and SMC literally interleave the two (IS reweighting between MCMC moves along a tempering ladder). The right mental model is a toolkit where ESS and variance diagnostics route you between them, not a fork where choosing IS means abandoning MCMC.

The threads forward: Sequential Monte Carlo generalizes the annealing ladder of Chapter 9; policy gradients and Q-learning use the off-policy ratios of Chapter 7 inside their updates; and Monte Carlo is the root where it all began.

python — the one diagnostic every IS-based method shares
import numpy as np
def ess(w):
    w = np.asarray(w, float); return w.sum()**2 / (w**2).sum()
# rare-event tilt, off-policy products, AIS ladders -> all read ESS
w = np.array([1,1,1,1,100.0])
print(ess(w), ess(w)/len(w))   # 1.08, 22% efficiency -> degenerate
python — the universal stop sign
if ess(w)/len(w) < 0.1: print('proposal degenerate -> add structure (SMC/WIS/DR)')
Across rare-event estimation, off-policy RL, and annealed IS, what single diagnostic warns you that the proposal (or bridge) has degenerated and the estimate can't be trusted?
The closing thought: "It is the mark of an educated mind to rest satisfied with the degree of precision which the nature of the subject admits." Importance sampling is precision engineering for the rare and the awkward — and the ESS number is how you know exactly how much precision you've actually bought.