Variance Reduction
18 min
Monte Carlo error shrinks with 1/√n — halving your error means
quadrupling your sample count, which gets expensive fast. Variance
reduction techniques squeeze a more accurate estimate out of the SAME
number of samples, without introducing bias.
Antithetic variates is the simplest one: for every random draw z,
also use its mirror image -z. Since a standard normal distribution is
symmetric around 0, -z is exactly as likely as z — but pairing them
means an unusually high draw is automatically balanced by an unusually
low one in the same batch, instead of relying on luck to average out over
many independent draws.
import random
random.seed(0)
z = random.gauss(0, 1)
pair = [z, -z] # both equally valid draws from the same distribution
The mean of an antithetic-paired sample of Z itself is ALWAYS exactly 0, by construction -- that's not the useful part. The technique shines when averaging something NONLINEAR in Z, like the next exercise's exp(Z).
Averaging Z itself with antithetic pairs gives exactly 0 every time —
not a useful demonstration on its own, since it doesn't actually reduce
variance in anything interesting (the true mean of Z is already 0). The
technique earns its keep on a NONLINEAR function of Z, like exp(Z)
(closely related to how a stock price is computed from a random shock in
GBM) — there, exp(z) and exp(-z) are NOT mirror images of each other,
so averaging the pair genuinely pulls the estimate toward the true
expected value faster than the same number of fully independent draws
would.
# without antithetic variates: n fully independent draws
total = sum(math.exp(random.gauss(0, 1)) for _ in range(n))
# with antithetic variates: n // 2 draws, each contributing TWO terms
total = sum(math.exp(z) + math.exp(-z) for z in [random.gauss(0, 1) for _ in range(n // 2)])
Both use the same total number of exp() evaluations — the antithetic
version just tends to converge to the true value with less sample-to-
sample noise.
Write `antithetic_pairs(n, seed=42)`: generate `n // 2` standard normal draws (`random.gauss(0, 1)`), then return a list of all `n` values — the `n // 2` draws followed by their negatives, each rounded to 6 decimal places, in the order generated.
Write `estimate_exp_z_antithetic(n, seed=42)`: estimate E[e^Z] for a standard normal Z using antithetic variates. Generate `n // 2` draws; for each `z`, include both `exp(z)` and `exp(-z)` in the average. Return the result rounded to 4 decimal places. (The true value is e^0.5 ≈ 1.6487.)
What's the core idea behind the antithetic variates technique?
You can generate antithetic variate pairs and use them to reduce variance when estimating the expectation of a nonlinear function of a random variable.