8. Discrete random variables and the PMF
So far, outcomes have been labels: heads, rain, the die shows 4. You can't average a label. To do arithmetic — means, spreads, sums — outcomes have to become numbers. That's what a random variable is for.
A random variable is a function from the sample space to the real numbers:
X : \Omega \to \mathbb{R}
That's the whole definition, and it's worth pausing on how unromantic it is. X is not random and it is not a variable. It is an ordinary deterministic function. The randomness is entirely in its input — which \omega nature picks — and X just reports a number about it.
Turning outcomes into numbers
Roll two dice. The sample space is 36 ordered pairs. Let X be their sum:
| Outcome \omega | X(\omega) |
|---|---|
| (1, 1) | 2 |
| (3, 4) | 7 |
| (6, 6) | 12 |
X collapses each messy pair into one number. That collapsing is deliberate and it always loses information: (3,4) and (5,2) both map to 7, and once you've applied X you can no longer tell them apart. You choose a random variable precisely to throw away the detail you don't care about.
A random variable is discrete when the values it can take can be listed — finitely many, or countably many like 0, 1, 2, \dots. Our sum takes values in \{2, 3, \dots, 12\}.
Notation you have to decode
P(X = 7) looks like a statement about a number, but it's shorthand for a statement about a set of outcomes:
P(X = 7) \;=\; P(\{\omega \in \Omega : X(\omega) = 7\})
The event is "the set of dice pairs summing to 7". Every probability of a random variable is really the probability of the event that produces it. Keep that translation in mind and nothing later feels like magic.
The probability mass function
The PMF collects all those probabilities into one function:
p(x) = P(X = x)
For two dice, X = 7 happens six ways — $(1,6), (2,5), (3,4), (4,3), (5,2), (6,1)$ — while X = 2 happens exactly one way:
p(7) = \frac{6}{36} = \frac{1}{6}, \qquad p(2) = \frac{1}{36}
Plot the whole thing and it climbs to a peak at 7 and falls back, symmetric — because 7 is simply the sum with the most ways to make it. The shape is a counting fact, not a mysterious one.
Two rules always hold, and they are just Kolmogorov's axioms restated:
p(x) \ge 0 \;\text{ for all } x, \qquad \sum_x p(x) = 1
Non-negativity is Axiom 1. The sum being 1 is Axiom 2 plus Axiom 3: the events \{X = x\} for different x are disjoint and together they cover \Omega, so their probabilities add to P(\Omega) = 1.
Anything satisfying those two conditions is a valid PMF. You don't need dice behind it — you can simply declare a distribution and work with it.
Mass, not density. For a discrete variable, p(x) genuinely is a probability: the chance of landing exactly on x. That stops being true the moment the variable becomes continuous, which is the next lesson's subject and the single biggest conceptual jump in this section.
The CDF
The cumulative distribution function accumulates the mass:
F(x) = P(X \le x) = \sum_{t \le x} p(t)
Note it's defined for every real x, not just the values X can take. F(4.7) is perfectly meaningful for the dice sum — it's P(X \le 4.7), which equals P(X \le 4) since no mass sits between 4 and 4.7.
So the graph is a staircase: flat everywhere, stepping up by p(x) exactly at each achievable value. Three properties characterise it:
\lim_{x \to -\infty} F(x) = 0, \qquad \lim_{x \to \infty} F(x) = 1, \qquad x \le y \implies F(x) \le F(y)
It starts at 0, ends at 1, and never decreases (each step adds non-negative mass).
Getting ranges out of it
The CDF earns its keep on interval questions:
P(a < X \le b) = F(b) - F(a)
This is exact, and the reason the interval is written half-open is that the formula only works with that convention. For discrete variables the endpoints genuinely matter, because there is real mass sitting on individual points:
P(a \le X \le b) = F(b) - F(a-1) \quad \text{(integer-valued } X)
Getting this wrong by one is the most common discrete-CDF error there is.
Worked example
Roll two dice. Find P(X \le 4) and P(5 \le X \le 7).
Count pairs for the low sums: 2 is only (1,1); 3 is (1,2),(2,1); 4 is (1,3),(2,2),(3,1). So
p(2) = \tfrac{1}{36}, \quad p(3) = \tfrac{2}{36}, \quad p(4) = \tfrac{3}{36}
F(4) = P(X \le 4) = \tfrac{1 + 2 + 3}{36} = \tfrac{6}{36} = \tfrac{1}{6}
For the second, the range includes 5, so we must subtract everything strictly below 5 — that is F(4), not F(5):
P(5 \le X \le 7) = F(7) - F(4)
Counting up to 7: the number of ways to make 2,3,\dots,7 is 1,2,3,4,5,6, so F(7) = \tfrac{21}{36}.
P(5 \le X \le 7) = \tfrac{21}{36} - \tfrac{6}{36} = \tfrac{15}{36} = \tfrac{5}{12}
Check directly: p(5) + p(6) + p(7) = \tfrac{4 + 5 + 6}{36} = \tfrac{15}{36}. ✓
Had we written F(7) - F(5) we'd have got \tfrac{11}{36} — silently dropping p(5) and losing the four outcomes that sum to 5.
Doing it in Python
You can build a PMF by counting the sample space directly, with no formula at
all. Fraction keeps the arithmetic exact, so "total mass = 1" is a genuine
check rather than a floating-point near-miss:
from collections import Counter
from itertools import product
from fractions import Fraction
omega = list(product(range(1, 7), repeat=2)) # all 36 pairs
counts = Counter(a + b for a, b in omega)
pmf = {x: Fraction(c, len(omega)) for x, c in sorted(counts.items())}
for x, p in pmf.items():
print(f"P(X = {x:>2}) = {str(p):>5} {'#' * counts[x]}")
print("\ntotal mass:", sum(pmf.values())) # exactly 1, not 0.9999999
The CDF is a running total, and itertools.accumulate is exactly that:
from collections import Counter
from itertools import product, accumulate
from fractions import Fraction
omega = list(product(range(1, 7), repeat=2))
counts = Counter(a + b for a, b in omega)
xs = sorted(counts)
pmf = [Fraction(counts[x], len(omega)) for x in xs]
cdf = dict(zip(xs, accumulate(pmf)))
for x in xs:
print(f"F({x:>2}) = {str(cdf[x]):>7}")
print("\nP(5 <= X <= 7) =", cdf[7] - cdf[4]) # note: F(4), not F(5)
For named distributions you'd normally reach for SciPy, whose discrete objects
all expose .pmf() and .cdf():
from scipy.stats import binom
# 10 fair coin flips; X = number of heads
n, p = 10, 0.5
print("P(X = 5) =", round(binom.pmf(5, n, p), 4))
print("P(X <= 5) =", round(binom.cdf(5, n, p), 4))
print("mean, var =", binom.mean(n, p), binom.var(n, p))
Your turn
1. A biased coin lands heads with probability 0.6. Let X be 1 for heads, 0 for tails. Write the PMF and the CDF.
2. X has PMF p(1) = 0.2, p(2) = 0.5, p(3) = c. Find c, then P(X \ge 2).
3. For the two-dice sum, find P(X \text{ is even}) — and try to do it without adding up six masses.
Solutions
1. The PMF is two numbers:
p(0) = 0.4, \qquad p(1) = 0.6
The CDF must be given for all real x, which is where people usually stop too early:
F(x) = \begin{cases} 0 & x < 0 \\ 0.4 & 0 \le x < 1 \\ 1 & x \ge 1 \end{cases}
This is the Bernoulli distribution. It looks trivial, and it's the atom that Binomial, Geometric and Negative Binomial are all assembled from two lessons later.
2. Total mass is 1:
0.2 + 0.5 + c = 1 \implies c = 0.3
Then use the complement rather than adding two terms:
P(X \ge 2) = 1 - P(X = 1) = 1 - 0.2 = 0.8
3. The sum of two dice is even exactly when the dice match in parity — both odd or both even. Each die is odd with probability 1/2, even with probability 1/2, and the two are independent:
P(\text{both odd}) + P(\text{both even}) = \tfrac{1}{2}\cdot\tfrac{1}{2} + \tfrac{1}{2}\cdot\tfrac{1}{2} = \tfrac{1}{2}
Exactly one half. Adding the masses confirms it — \tfrac{1 + 3 + 5 + 5 + 3 + 1}{36} = \tfrac{18}{36} — but the parity argument never touched the PMF, and it keeps working for 100 dice where the counting would not.
Check yourself in code
Build the PMF of the sum of two dice and report the most likely value, its probability as an exact fraction, and the total mass.
Print exactly this:
most likely sum: 7
its probability: 1/6
total mass: 1
Use fractions.Fraction so the probability prints as a fraction, not a float.
from collections import Counter
from itertools import product
from fractions import Fraction
omega = list(product(range(1, 7), repeat=2))
counts = Counter(a + b for a, b in omega)
pmf = {x: Fraction(c, len(omega)) for x, c in counts.items()}
best = max(pmf, key=pmf.get)
print("most likely sum:", best)
# print its probability, then the total mass over all values
from collections import Counter
from itertools import product
from fractions import Fraction
omega = list(product(range(1, 7), repeat=2))
counts = Counter(a + b for a, b in omega)
pmf = {x: Fraction(c, len(omega)) for x, c in counts.items()}
best = max(pmf, key=pmf.get)
print("most likely sum:", best)
print("its probability:", pmf[best])
print("total mass:", sum(pmf.values()))
A random variable maps outcomes to numbers. Its PMF says how likely each number is, and must be non-negative and sum to 1. Its CDF accumulates that mass into a staircase, and turns interval questions into subtraction — as long as you get the endpoints right.
Next: what breaks when the variable can land anywhere on a continuum, where the probability of any exact value turns out to be zero.