A number whose value is not yet known
A random variable is a quantity whose value depends on chance: the roll of a die, tomorrow's return, the price of a stock at expiry. A discrete random variable takes a list of possible values, each with a probability. Together the values and probabilities are its distribution. The probabilities are never negative and add up to 1. In Python this is just two lists of the same length, values and probs. A fair die is values = [1, 2, 3, 4, 5, 6] with probs = [1/6] * 6.
import math
values = [1, 2, 3, 4, 5, 6]
probs = [1 / 6] * 6
# A valid distribution: no negatives, and the probabilities add up to 1
print(all(p >= 0 for p in probs)) # True
print(math.isclose(sum(probs), 1.0)) # True (compare floats with isclose)
# A stock tomorrow: down 5%, up 2% or up 8%
returns = [-0.05, 0.02, 0.08]
chances = [0.2, 0.5, 0.3]
print(math.isclose(sum(chances), 1.0)) # TrueExpectation: the probability-weighted average
The expected value E[X] is the average you would get over many repetitions: multiply each value by its probability and add up, sum(v * p). A die has E[X] = 3.5, a number it can never show, which is fine: it is a long-run average, not a prediction of one outcome. Expectation is linear: E[a*X + b] = a*E[X] + b. The expectation of a function E[g(X)] applies g to each value first: sum(g(v) * p). Careful: in general E[g(X)] is not g(E[X]). An option's payoff max(S - K, 0) is the classic case.
def expected_value(values, probs):
return sum(v * p for v, p in zip(values, probs))
die = [1, 2, 3, 4, 5, 6]
fair = [1 / 6] * 6
print(round(expected_value(die, fair), 4)) # 3.5
# E[g(X)]: apply g to each value first. Call payoff with strike 100:
prices = [90, 100, 110, 120]
chances = [0.1, 0.4, 0.3, 0.2]
mean_price = expected_value(prices, chances)
print(mean_price) # 106.0
print(expected_value([max(s - 100, 0) for s in prices], chances)) # 7.0 = E[max(S - K, 0)]
print(max(mean_price - 100, 0)) # 6.0 = max(E[S] - K, 0), not the sameVariance: how spread out is it?
Two investments can have the same expected return and very different risk. The variance measures the spread around the mean: Var(X) = E[(X - mu)^2], which can be computed as E[X^2] - E[X]^2. The standard deviation is its square root, in the same units as X; for returns this is the volatility. For the die, Var = 35/12 = 2.9167. Both are built from expectations of functions of X, so one well-written expected_of(g, values, probs) gives you the mean, the second moment and everything that follows.
def expected_value(values, probs):
return sum(v * p for v, p in zip(values, probs))
def expected_of(g, values, probs):
return expected_value([g(v) for v in values], probs)
def variance(values, probs):
mean = expected_of(lambda v: v, values, probs)
return expected_of(lambda v: v * v, values, probs) - mean ** 2
die, fair = [1, 2, 3, 4, 5, 6], [1 / 6] * 6
print(round(variance(die, fair), 4)) # 2.9167
returns, chances = [-0.05, 0.02, 0.08], [0.2, 0.5, 0.3]
print(round(expected_value(returns, chances), 4)) # 0.024
print(round(variance(returns, chances), 6)) # 0.002044Simulation and the law of large numbers
You can also estimate an expectation by drawing samples. rng.choices(values, weights=probs, k=n) returns n draws, and the average of the draws approaches E[X] as n grows: the law of large numbers. This is the heart of Monte Carlo pricing. Use your own random.Random(seed) so that the run is repeatable and does not disturb other code. With few draws the average is noisy; the error shrinks only like one over the square root of n, so 100 times more draws buy 10 times more accuracy.
import random
die, fair = [1, 2, 3, 4, 5, 6], [1 / 6] * 6
def sample_mean(values, probs, n, seed):
rng = random.Random(seed) # private, repeatable generator
draws = rng.choices(values, weights=probs, k=n)
return sum(draws) / n
for n in (10, 1000, 100000):
print(n, round(sample_mean(die, fair, n, 1), 4))
# 10 3.1
# 1000 3.598
# 100000 3.5017 (closing in on 3.5)
print(sample_mean(die, fair, 1000, 1) == sample_mean(die, fair, 1000, 1)) # True: same seed, same answermath.isclose(sum(probs), 1.0). Raise ValueError for anything else. Remember too that an expectation is a long-run average: a strategy with a positive expected value can still lose money on any single day.- A distribution is
valuesplusprobs(no negatives, sum 1);E[X] = sum(v * p). E[g(X)] = sum(g(v) * p), which is notg(E[X]);Var = E[X^2] - E[X]^2.- Sample means converge to
E[X](law of large numbers); use your ownrandom.Random(seed).
Next up: NumPy arrays and dtypes, the fast, typed arrays that the rest of the course is built on.
Lab · what's graded
- Average outcomes by their probabilities with
expected_value25 pts - Take the expectation of a payoff with
expected_of25 pts - Measure the spread with
variance25 pts - Estimate the expectation by simulation with
sample_mean25 pts