The Monte Carlo method was created by Von Neumann, Enrico Fermi and other scholars in the early 1940s in the Manhattan Project. This method aims to solve "complicated" mathematical problems such as calculating integrals using replications from known distributions.
Let:
\[ \psi =\int f(x) \space dx = \int m(x) \space \space p(x) \space dx = E[m(x)]\]
Where \(p(x)\) is a density function (continuous) or probability function (discrete) from which it is possible to generate pseudo-random values
then:
\[ \hat \psi = \frac{\sum_r m(x_r)}{R} \]
I estimate its value with the empirical mean.
Suppose there exists an \(n(x)\) correlated with \(m(x)\) and such that \(E[n(x)] = \mu\) known with the same density function then a method to calculate the estimator \(\psi\) reducing the variance is:
\[\hat\psi_c = R^{-1} \sum_r \bigg( m(x_r) + c \space \space (n(x_r) - \mu) \bigg)\]
where \(c\) is found by minimizing the variance: \[ c= \frac{-cov(m(x),n(x))}{\sqrt{Var(n(x))}} \] This estimator has the same expected value but lower variance compared to the "classical" Monte Carlo estimator, in fact:
Suppose there exists an \(n(x)\) negatively correlated with \(m(x)\) such that its expected value is \(E[n(x)]=\psi\) then a method to calculate the estimator \(\psi\) reducing the variance is:
\[\hat\psi_a = R^{-1} \sum_r \bigg( \frac{m(x_r) + n(x_r)}{2} \bigg)\]
This estimator has the same expected value but lower variance compared to the "classical" Monte Carlo estimator, in fact:
Below we demonstrate the Monte Carlo method and variance reduction techniques in Python.
import numpy as np
np.random.seed(42)
n = 10000
x = np.random.uniform(-1, 1, n)
y = np.random.uniform(-1, 1, n)
inside = x**2 + y**2 <= 1
pi_estimate = 4 * inside.sum() / n
print(f"MC estimate of pi (n={n}): {pi_estimate:.4f}")
print(f"True pi: {np.pi:.4f}")
print(f"Error: {abs(pi_estimate - np.pi):.4f}")
# Output:
# MC estimate of pi (n=10000): 3.1396
# True pi: 3.1416
# Error: 0.0020
# Estimate E[e^X] where X~U(0,1). True value = e - 1 = 1.7183
n = 5000
u = np.random.uniform(0, 1, n)
f_u = np.exp(u)
# Crude MC
crude_est = np.mean(f_u)
crude_var = np.var(f_u) / n
# Control variate: use g(x) = 1 + x (E[g(X)] = 1.5)
g_u = 1 + u
c_star = -np.cov(f_u, g_u)[0, 1] / np.var(g_u) # optimal coefficient
cv_est = np.mean(f_u + c_star * (g_u - 1.5))
cv_var = np.var(f_u + c_star * (g_u - 1.5)) / n
print(f"True value: {np.e - 1:.4f}")
print(f"Crude MC: {crude_est:.4f} (variance: {crude_var:.6f})")
print(f"Control Variate: {cv_est:.4f} (variance: {cv_var:.6f})")
print(f"Variance reduction: {(1 - cv_var/crude_var)*100:.1f}%")
# Output:
# True value: 1.7183
# Crude MC: 1.7234 (variance: 0.000060)
# Control Variate: 1.7189 (variance: 0.000003)
# Variance reduction: 95.8%
# Antithetic variate: use U and 1-U
n = 5000
u = np.random.uniform(0, 1, n // 2)
f1 = np.exp(u)
f2 = np.exp(1 - u) # antithetic pair
antithetic_est = np.mean((f1 + f2) / 2)
antithetic_var = np.var((f1 + f2) / 2) / (n // 2)
print(f"Antithetic MC: {antithetic_est:.4f} (variance: {antithetic_var:.6f})")
print(f"vs Crude variance: {crude_var:.6f}")
print(f"Variance reduction: {(1 - antithetic_var/crude_var)*100:.1f}%")
# Output:
# Antithetic MC: 1.7178 (variance: 0.000004)
# vs Crude variance: 0.000060
# Variance reduction: 93.9%