⚙️ Setup¶
import numpy as np
from scipy import stats
rng = np.random.default_rng(431)
print("ready")
ready
mu_c, sd_c = 20, 5
mu_f = 1.8*mu_c + 32
sd_f = abs(1.8)*sd_c
print(f"mean F = 1.8*20 + 32 = {mu_f}")
print(f"sd F = 1.8*5 = {sd_f}")
mean F = 1.8*20 + 32 = 68.0 sd F = 1.8*5 = 9.0
Answer: mean = 1.8(20) + 32 = 68°F; sd = 1.8(5) = 9°F. The +32 shifts the mean but not the spread; only the multiplier 1.8 scales the standard deviation. F is still normal.
X = rng.normal(0,1,size=300_000); Y = np.exp(X)
print(f"skew of Y = {stats.skew(Y):.3f} (positive -> right-skewed)")
print(f"median of Y = {np.median(Y):.3f} (lognormal median = exp(0) = 1)")
skew of Y = 6.084 (positive -> right-skewed) median of Y = 1.001 (lognormal median = exp(0) = 1)
Answer: Y is strongly right-skewed (positive skew) with median ≈ 1.0, since the median maps through exp as exp(0) = 1. The exponential transform turns a symmetric normal into the asymmetric lognormal.
X = rng.random(300_000); Y = X**2
# fraction of Y below 0.25 should be sqrt(0.25) = 0.5 (since CDF of Y is sqrt(y))
print(f"P(Y < 0.25) simulated = {np.mean(Y<0.25):.3f} (theory sqrt(0.25) = 0.5)")
print(f"P(Y < 0.04) simulated = {np.mean(Y<0.04):.3f} (theory sqrt(0.04) = 0.2)")
P(Y < 0.25) simulated = 0.499 (theory sqrt(0.25) = 0.5) P(Y < 0.04) simulated = 0.199 (theory sqrt(0.04) = 0.2)
Answer: the CDF of Y is √y, so P(Y < 0.25) = √0.25 = 0.5 and P(Y < 0.04) = 0.2, both matched by simulation. Squaring compresses the interval near 0, so the density f_Y(y) = 1/(2√y) blows up there, exactly the Jacobian at work.
U = rng.random(300_000)
rate = 2
X = -np.log(1 - U) / rate # inverse CDF of exponential(rate)
print(f"sample mean = {X.mean():.4f} (theory 1/rate = {1/rate})")
sample mean = 0.5010 (theory 1/rate = 0.5)
Answer: inverting the exponential CDF gives X = −ln(1−U)/rate, and the sample mean is about 0.5, matching 1/rate. Any distribution with an invertible CDF can be sampled this way, the foundation of random-variate generation.
mu, sigma = 10, 3
eps = rng.normal(0,1,size=300_000)
z = mu + sigma*eps
print(f"reparameterized z: mean {z.mean():.3f} (target {mu}), sd {z.std():.3f} (target {sigma})")
reparameterized z: mean 9.995 (target 10), sd 2.995 (target 3)
Answer: z = mu + sigma·epsilon recovers Normal(10, 3) exactly. Because z is a differentiable function of mu and sigma, gradients pass through the sampling step, which is what makes variational autoencoders trainable by backpropagation.