Brownian Motion¶
Visualize stochastic processes with Matplotlib.
Mental Model
Brownian motion is a random walk with continuous time -- each step is a small random increment. Plotting many sample paths on one figure reveals the distribution of possible trajectories fanning out from the origin. Use ax.plot() in a loop for individual paths and fill_between to shade confidence bands.
Theoretical Foundation
Standard Brownian motion \(B_t\) has three defining properties:
- Independent increments: \(B_t - B_s\) is independent of the history up to time \(s\)
- Normal increments: \(B_t - B_s \sim N(0, t - s)\)
- Continuous paths: \(B_t\) is continuous in \(t\) (with probability 1)
Property 2 means variance grows linearly with time — this is why sample paths fan out as \(\sqrt{t}\), and why the \(\pm 2\sqrt{t}\) confidence band captures ~95% of paths (by the normal distribution's 2\(\sigma\) rule).
Visual Intuition
Brownian motion is not a single path — it is a distribution over paths. Each plotted line is one possible realization; the collection of all lines represents the true stochastic object. At each time \(t\), the vertical slice through the plot is a distribution: \(B_t \sim N(0, t)\). As \(t\) increases, this distribution spreads — which is why the plot "fans out." Brownian motion is a moving distribution, and plotting many paths makes that movement visible.
Geometric Brownian Motion (GBM) models prices via \(S_t = S_0 \exp((\mu - \tfrac{1}{2}\sigma^2)t + \sigma B_t)\). The drift correction \(-\tfrac{1}{2}\sigma^2\) arises from Ito's lemma: applying \(\exp\) to a process with quadratic variation shifts the mean. The result is that \(S_t\) is log-normally distributed — it cannot go negative, matching the behavior of asset prices.
Brownian Motion Class¶
```python import matplotlib.pyplot as plt import numpy as np
class BrownianMotion:
def __init__(self, T=1):
self.T = T
def run_MC(self, num_paths=1, num_steps=None, seed=None):
"""
Generate Brownian motion sample paths.
Parameters
----------
num_paths : int
Number of paths to generate
num_steps : int
Number of time steps
seed : int
Random seed for reproducibility
Returns
-------
t : array
Time points
dt : float
Time step size
sqrt_dt : float
Square root of time step
B : array
Brownian motion paths (num_paths x num_steps+1)
dB : array
Increments (num_paths x num_steps)
"""
if num_steps is None:
num_steps = int(self.T * 12 * 21) # Daily steps for 1 year
if seed is not None:
np.random.seed(seed)
t = np.linspace(0, self.T, num_steps + 1)
dt = t[1] - t[0]
sqrt_dt = np.sqrt(dt)
Z = np.random.standard_normal((num_paths, num_steps))
# Note: we do NOT normalize Z here. Centering/rescaling
# (Z - Z.mean()) / Z.std() would force zero mean and unit
# variance per time step, breaking independence across paths
# and invalidating the Monte Carlo sampling.
dB = Z * sqrt_dt
B = np.concatenate([np.zeros((num_paths, 1)), dB.cumsum(axis=1)], axis=1)
return t, dt, sqrt_dt, B, dB
```
Plotting Sample Paths¶
```python import matplotlib.pyplot as plt import numpy as np
def main(): num_paths = 10 num_steps = 1000
bm = BrownianMotion()
t, _, _, B, _ = bm.run_MC(num_paths, num_steps, seed=0)
fig, ax = plt.subplots(figsize=(12, 4))
ax.set_title('Ten Sample Paths of Brownian Motion $B_t$')
for i in range(num_paths):
ax.plot(t, B[i], alpha=0.7)
ax.set_xlabel('Time $t$')
ax.set_ylabel('$B_t$')
ax.axhline(0, color='black', linewidth=0.5, linestyle='--')
ax.grid(True, alpha=0.3)
plt.show()
if name == "main": main() ```
Distribution at Terminal Time¶
```python import matplotlib.pyplot as plt import numpy as np from scipy import stats
def main(): num_paths = 10000 num_steps = 1000 T = 1
bm = BrownianMotion(T=T)
t, _, _, B, _ = bm.run_MC(num_paths, num_steps, seed=42)
# Terminal values
terminal_values = B[:, -1]
fig, ax = plt.subplots(figsize=(10, 4))
# Histogram
ax.hist(terminal_values, bins=50, density=True, alpha=0.7, label='Simulated')
# Theoretical distribution: N(0, T)
x = np.linspace(-4, 4, 100)
ax.plot(x, stats.norm(0, np.sqrt(T)).pdf(x), 'r-', lw=2, label=f'$N(0, {T})$')
ax.set_xlabel('$B_T$')
ax.set_ylabel('Density')
ax.set_title(f'Distribution of $B_T$ at $T={T}$')
ax.legend()
plt.show()
if name == "main": main() ```
Geometric Brownian Motion¶
Stock price model:
```python import matplotlib.pyplot as plt import numpy as np
def simulate_gbm(S0, mu, sigma, T, num_paths, num_steps, seed=None): """ Simulate Geometric Brownian Motion.
S_t = S_0 * exp((mu - sigma^2/2)*t + sigma*B_t)
"""
if seed is not None:
np.random.seed(seed)
dt = T / num_steps
t = np.linspace(0, T, num_steps + 1)
# Generate standard normal increments
dW = np.random.randn(num_paths, num_steps) * np.sqrt(dt)
W = np.concatenate([np.zeros((num_paths, 1)), dW.cumsum(axis=1)], axis=1)
# GBM formula
drift = (mu - 0.5 * sigma**2) * t
diffusion = sigma * W
S = S0 * np.exp(drift + diffusion)
return t, S
def main(): S0 = 100 # Initial price mu = 0.10 # Drift (10% annual return) sigma = 0.20 # Volatility (20%) T = 1 # Time horizon (1 year) num_paths = 20 num_steps = 252 # Trading days
t, S = simulate_gbm(S0, mu, sigma, T, num_paths, num_steps, seed=42)
fig, ax = plt.subplots(figsize=(12, 5))
for i in range(num_paths):
ax.plot(t * 252, S[i], alpha=0.6)
ax.axhline(S0, color='black', linestyle='--', label=f'$S_0={S0}$')
ax.axhline(S0 * np.exp(mu * T), color='red', linestyle='--',
label=f'Expected: ${S0 * np.exp(mu * T):.1f}$')
ax.set_xlabel('Trading Days')
ax.set_ylabel('Stock Price')
ax.set_title('Geometric Brownian Motion - Stock Price Simulation')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
if name == "main": main() ```
Confidence Bands¶
```python import matplotlib.pyplot as plt import numpy as np
def main(): num_paths = 1000 num_steps = 252 T = 1
bm = BrownianMotion(T=T)
t, _, _, B, _ = bm.run_MC(num_paths, num_steps, seed=42)
# Calculate statistics
mean = B.mean(axis=0)
std = B.std(axis=0)
# Theoretical standard deviation
theoretical_std = np.sqrt(t)
fig, ax = plt.subplots(figsize=(12, 5))
# Plot a few sample paths
for i in range(5):
ax.plot(t, B[i], alpha=0.3, color='blue')
# Confidence bands
ax.fill_between(t, mean - 2*std, mean + 2*std,
alpha=0.2, color='blue', label='±2σ (empirical)')
ax.plot(t, 2*theoretical_std, 'r--', label='±2σ (theoretical)')
ax.plot(t, -2*theoretical_std, 'r--')
ax.set_xlabel('Time $t$')
ax.set_ylabel('$B_t$')
ax.set_title('Brownian Motion with Confidence Bands')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
if name == "main": main() ```
Key Takeaways¶
- Brownian motion is the foundation of stochastic calculus
- Use NumPy for efficient path generation
- Plot multiple paths to show distribution of outcomes
- GBM models stock prices with log-normal distribution
- Confidence bands show expected variation over time
Exercises¶
Exercise 1. Write code that generates and plots 5 sample paths of standard Brownian motion over the interval \([0, 1]\) with 500 time steps. Add a horizontal dashed line at \(y = 0\), label the axes, and include a grid.
Solution to Exercise 1
```python import matplotlib.pyplot as plt import numpy as np
np.random.seed(42) T, n_steps, n_paths = 1.0, 500, 5 dt = T / n_steps t = np.linspace(0, T, n_steps + 1)
dB = np.random.randn(n_paths, n_steps) * np.sqrt(dt) B = np.concatenate([np.zeros((n_paths, 1)), dB.cumsum(axis=1)], axis=1)
fig, ax = plt.subplots(figsize=(10, 4)) for i in range(n_paths): ax.plot(t, B[i], alpha=0.7) ax.axhline(0, color='black', linewidth=0.5, linestyle='--') ax.set_xlabel('Time \(t\)') ax.set_ylabel('\(B_t\)') ax.set_title('Sample Paths of Brownian Motion') ax.grid(True, alpha=0.3) plt.show() ```
Exercise 2. Explain why the theoretical standard deviation of Brownian motion \(B_t\) at time \(t\) is \(\sqrt{t}\). Write code that plots the empirical standard deviation of 1000 Brownian paths alongside the theoretical curve \(\sqrt{t}\).
Solution to Exercise 2
Brownian motion increments \(B_t - B_s \sim N(0, t - s)\) for \(s < t\). Starting from \(B_0 = 0\), we have \(B_t \sim N(0, t)\), so \(\text{Var}(B_t) = t\) and \(\text{Std}(B_t) = \sqrt{t}\).
```python import matplotlib.pyplot as plt import numpy as np
np.random.seed(42) T, n_steps, n_paths = 1.0, 500, 1000 dt = T / n_steps t = np.linspace(0, T, n_steps + 1)
dB = np.random.randn(n_paths, n_steps) * np.sqrt(dt) B = np.concatenate([np.zeros((n_paths, 1)), dB.cumsum(axis=1)], axis=1)
empirical_std = B.std(axis=0) theoretical_std = np.sqrt(t)
fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(t, empirical_std, 'b-', label='Empirical Std') ax.plot(t, theoretical_std, 'r--', label=r'Theoretical \(\sqrt{t}\)') ax.set_xlabel('Time \(t\)') ax.set_ylabel('Standard Deviation') ax.set_title('Empirical vs Theoretical Std of Brownian Motion') ax.legend() ax.grid(True, alpha=0.3) plt.show() ```
Exercise 3. Simulate Geometric Brownian Motion (GBM) with parameters \(S_0 = 100\), \(\mu = 0.05\), \(\sigma = 0.3\), and \(T = 1\). Plot 10 sample paths and add a horizontal line at the initial price \(S_0\).
Solution to Exercise 3
```python import matplotlib.pyplot as plt import numpy as np
np.random.seed(42) S0, mu, sigma, T = 100, 0.05, 0.3, 1.0 n_steps, n_paths = 252, 10 dt = T / n_steps t = np.linspace(0, T, n_steps + 1)
dW = np.random.randn(n_paths, n_steps) * np.sqrt(dt) W = np.concatenate([np.zeros((n_paths, 1)), dW.cumsum(axis=1)], axis=1) S = S0 * np.exp((mu - 0.5 * sigma**2) * t + sigma * W)
fig, ax = plt.subplots(figsize=(10, 5)) for i in range(n_paths): ax.plot(t * 252, S[i], alpha=0.7) ax.axhline(S0, color='black', linestyle='--', label=f'\(S_0 = {S0}\)') ax.set_xlabel('Trading Days') ax.set_ylabel('Price') ax.set_title('Geometric Brownian Motion Paths') ax.legend() ax.grid(True, alpha=0.3) plt.show() ```
Exercise 4. Create a figure with two subplots side by side. The left subplot shows 5 Brownian motion sample paths. The right subplot shows a histogram of the terminal values \(B_T\) for 5000 paths, overlaid with the theoretical \(N(0, T)\) density curve.
Solution to Exercise 4
```python import matplotlib.pyplot as plt import numpy as np from scipy import stats
np.random.seed(42) T, n_steps = 1.0, 500 dt = T / n_steps t = np.linspace(0, T, n_steps + 1)
For paths subplot¶
dB5 = np.random.randn(5, n_steps) * np.sqrt(dt) B5 = np.concatenate([np.zeros((5, 1)), dB5.cumsum(axis=1)], axis=1)
For histogram subplot¶
dB_many = np.random.randn(5000, n_steps) * np.sqrt(dt) B_many = np.concatenate([np.zeros((5000, 1)), dB_many.cumsum(axis=1)], axis=1) terminal = B_many[:, -1]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
for i in range(5): ax1.plot(t, B5[i], alpha=0.7) ax1.axhline(0, color='black', linestyle='--', linewidth=0.5) ax1.set_xlabel('Time \(t\)') ax1.set_ylabel('\(B_t\)') ax1.set_title('Sample Paths') ax1.grid(True, alpha=0.3)
ax2.hist(terminal, bins=50, density=True, alpha=0.7, label='Simulated') x_range = np.linspace(-4, 4, 200) ax2.plot(x_range, stats.norm(0, np.sqrt(T)).pdf(x_range), 'r-', lw=2, label=f'\(N(0, {T})\)') ax2.set_xlabel('\(B_T\)') ax2.set_ylabel('Density') ax2.set_title(f'Terminal Distribution at \(T={T}\)') ax2.legend()
plt.tight_layout() plt.show() ```