Demo: Monte Carlo Methods

Estimate a normal tail probability by crude Monte Carlo and by importance sampling. This is a compact way to see why proposal choice matters for rare events.

Mathematical setup

The target probability is

\[p_\gamma=\Pr(Z>\gamma)=E_f[\mathbf 1\{Z>\gamma\}], \qquad Z\sim N(0,1).\]

Crude Monte Carlo uses

\[\hat p_{\mathrm{crude}}=\frac{1}{M}\sum_{m=1}^M \mathbf 1\{Z_m>\gamma\}, \qquad Z_m\sim f.\]

Importance sampling draws $Y_m\sim q=N(\delta,1)$ and weights by $w(Y_m)=f(Y_m)/q(Y_m)$:

\[\hat p_{\mathrm{IS}}=\frac{1}{M}\sum_{m=1}^M \mathbf 1\{Y_m>\gamma\}w(Y_m).\]

This estimate is unbiased for the displayed setup because both $f$ and $q$ are normalized and known exactly. The effective sample size displayed by the widget is

\[\mathrm{ESS}=\frac{\left(\sum_m w_m\right)^2}{\sum_m w_m^2},\]

a diagnostic for weight concentration, not a proof of accuracy.

What to try

  • Set $\gamma$ near 1.5. Crude Monte Carlo usually works because the event is not too rare.
  • Move $\gamma$ toward 3 or 4. Crude estimates can become unstable unless $M$ is large.
  • Set the proposal shift near the threshold. If the shift is too small, few proposal samples hit the tail; if too large, weights can become unstable.

The importance estimate averages $\mathbf 1{Y>\gamma}f(Y)/q(Y)$ over the proposal draws, dividing by $M$ rather than by the sum of weights. That non-self-normalized form is unbiased here because both the target and proposal densities are known. The effective sample size summarizes weight degeneracy: large weights from a few draws reduce useful information.

Try it in Python

This cell estimates the same normal tail probability with crude Monte Carlo and shifted-normal importance sampling.

import numpy as np
from scipy import stats

M = 1000
gamma = 2.5
shift = 2.0
rng = np.random.default_rng(531)

z = rng.normal(0, 1, size=M)
crude = np.mean(z > gamma)

y = rng.normal(shift, 1, size=M)
log_weights = stats.norm.logpdf(y, 0, 1) - stats.norm.logpdf(y, shift, 1)
weights = np.exp(log_weights)
importance = np.mean((y > gamma) * weights)
ess = weights.sum()**2 / np.sum(weights**2)

truth = stats.norm.sf(gamma)
crude_se = np.sqrt(crude * (1 - crude) / M)
is_se = np.std((y > gamma) * weights, ddof=1) / np.sqrt(M)

print(f"true probability:       {truth:.6f}")
print(f"crude estimate:        {crude:.6f}  SE about {crude_se:.6f}")
print(f"importance estimate:   {importance:.6f}  SE about {is_se:.6f}")
print(f"effective sample size: {ess:.1f} out of {M}")

Back to topic notes