σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 3 · Probability & Monte Carlo simulation›Topic 3.10·~45 minIntermediate

Sampling from distributions

Skill positionModule 3 of 11 · Topic 10 of 12
3.10 Sampling from distributions
—Lab score— / 100Total XP—Streak—

From uniform to anything

A generator gives numbers spread evenly between 0 and 1. A model needs other shapes: a coin that wins 30% of the time, a waiting time between trades, a normal daily return. The trick is the same: apply a transformation to uniform numbers. The most general is the inverse transform: if F is the cumulative distribution function of what you want, then F⁻¹(u) for uniform u has exactly that distribution. Whole arrays go through the transformation at once.

import math
import numpy as np

# Exponential waiting times with rate 2 (a mean of 0.5): F(x) = 1 - exp(-rate * x), so x = -ln(1 - u) / rate
u = np.array([0.1, 0.5, 0.9])
print((-np.log(1 - u) / 2).round(4).tolist())       # [0.0527, 0.3466, 1.1513]   small u, short wait

rng = np.random.default_rng(42)
waits = -np.log(1 - rng.random(100000)) / 2
print(round(waits.mean(), 2))                        # 0.5   the mean is 1 / rate

Discrete outcomes and the normal distribution

For a discrete table of outcomes, rng.choice(values, size=n, p=probs) does the inverse transform for you: it draws each value with its own probability, such as a day that is -2%, 0% or +3% with probabilities 20%, 50% and 30%. For the normal distribution the inverse of its distribution function has no simple formula, so a classic trick is the Box-Muller transform: from two independent uniform numbers u1 and u2, z = sqrt(-2 ln u1) * cos(2 pi u2) is a standard normal draw. In practice rng.standard_normal(n) does this faster, but writing it once shows there is no magic.

rng = np.random.default_rng(3)
days = rng.choice(np.array([-0.02, 0.0, 0.03]), size=100000, p=[0.2, 0.5, 0.3])
print(round(days.mean(), 3), round(0.2 * -0.02 + 0.3 * 0.03, 3))     # 0.005 0.005   the sample mean matches the table

rng = np.random.default_rng(42)
u1, u2 = 1 - rng.random(100000), rng.random(100000)                   # 1 - random() is in (0, 1]: never log(0)
z = np.sqrt(-2 * np.log(u1)) * np.cos(2 * math.pi * u2)
print(abs(z.mean()) < 0.02, round(z.std(), 2))                        # True 1.0   mean 0 and standard deviation 1

From standard normal draws to prices

A standard normal draw z is a shock; the model decides how it moves a price. The usual model for a stock, geometric Brownian motion, says the price after t years is S = S0 * exp((mu - 0.5 sigma²) t + sigma sqrt(t) z). It is lognormal: always positive, with the long right tail real prices have. The -0.5 sigma² term corrects the average so that the expected price grows exactly at mu: E[S] = S0 * exp(mu t). The median is lower than the mean, which is why a typical outcome is worse than the average one.

s0, mu, sigma, t = 100, 0.08, 0.2, 1
z = np.array([-1.0, 0.0, 1.0])                         # a one-sigma loss, no shock, a one-sigma gain
prices = s0 * np.exp((mu - 0.5 * sigma ** 2) * t + sigma * math.sqrt(t) * z)
print(prices.round(2).tolist())                         # [86.94, 106.18, 129.69]   all positive, the upside is larger

draws = np.random.default_rng(0).standard_normal(100000)
simulated = s0 * np.exp((mu - 0.5 * sigma ** 2) * t + sigma * math.sqrt(t) * draws)
print(round(simulated.mean(), 1), round(s0 * math.exp(mu * t), 1))     # 108.3 108.3   the mean grows at mu
print(round(float(np.median(simulated)), 1))                           # 106.1         the median is lower
# NumPy ships ready-made samplers too; the transformations above show what they do inside
z = np.random.default_rng(0).standard_normal(100000)
print(abs(z.mean()) < 0.02, round(z.std(), 2))      # True 1.0   standard_normal: mean 0, standard deviation 1
Check a sampler against what you knowAfter writing any sampler, draw a large sample and compare its mean, standard deviation or a probability with the exact value: 0.5 for the exponential at rate 2, 0 and 1 for the normal, S0 * exp(mu t) for the price. One sign error or a missing square root produces plausible-looking numbers that are wrong, and only such a check reveals it.
  • Inverse transform: apply the inverse distribution function to uniform numbers; exponential: -ln(1 - u) / rate.
  • rng.choice(values, size=n, p=probs) samples a discrete table; Box-Muller sqrt(-2 ln u1) * cos(2 pi u2) makes normal draws.
  • Price after t years: S0 * exp((mu - 0.5 sigma²) t + sigma sqrt(t) z), lognormal; always check a big sample against the exact mean.

Next up: Monte Carlo estimation, using random draws to compute averages and probabilities that have no formula.

Lab · what's graded

  • Draw from a table of outcomes with sample_discrete25 pts
  • Invert the distribution function with exponential_from_uniform25 pts
  • Make bell-curve numbers with box_muller25 pts
  • Turn shocks into future prices with terminal_prices25 pts
Explorer · lab-63-1
▾ lab-63-1
…
bash · in-browser
learner@quantcraft:~/lab-63-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-63-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub