A probability is a long-run frequency you can fake in code
"Lumen converts at 3.2%" is a probability statement. It does not mean every 100th session buys - it means that over a very large number of sessions, about 3.2 in 100 do. That reframing is the whole trick: any probability you can write down, you can also estimate by generating a mountain of random outcomes and taking the average. This session teaches the rules and the simulation habit side by side, so you never again get stuck on a probability word problem.
Sample spaces, the four rules, and Bayes 7 min live
Everything starts with a sample space - the set of things that can happen. A Lumen session either converts or it does not: the sample space is {convert, no convert} with probabilities {0.032, 0.968}. From there, four rules do the heavy lifting, and Bayes is the one people fear. The picture below shows why the fear is misplaced: count people, not fractions.
LiveThe four rules, in one card4 min▶
- Addition (OR): P(A or B) = P(A) + P(B) - P(A and B). Subtract the overlap so you do not double-count. If A and B are mutually exclusive, the overlap is 0.
- Multiplication (AND): P(A and B) = P(A) x P(B | A). If A and B are independent, this collapses to P(A) x P(B).
- Conditional: P(A | B) = P(A and B) / P(B). "Given that B happened, how likely is A." Restricting to the world where B is true.
- Bayes: P(A | B) = P(B | A) x P(A) / P(B). It flips a conditional you know into the one you want. The natural-frequency box above is Bayes, just counted out.
SelfWhy base rates dominate a rare event3 min▶
The flag above is genuinely good at its job - it catches 90% of high-value orders. Yet when it fires, the order is high-value only about a third of the time. The reason is arithmetic, not a broken flag: there are so many more ordinary orders (9,600 vs 400) that even a small 8% false-positive rate produces more false alarms (768) than the flag has true hits (360). When the thing you are hunting is rare, the base rate wins. This is the same math behind "a 99%-accurate test for a 1-in-1000 disease is wrong most times it fires."
Estimate probabilities by counting fake outcomes ★ 22 min · everyone builds
Four short scripts. Each takes a rule you just read and confirms it by generating random outcomes and averaging. If the simulation matches the formula, you understood the formula - and if you ever forget the formula, the simulation still works.
Simulate Lumen conversions. A boolean array is a probability estimator: its mean is P(true).
import numpy as np
rng = np.random.default_rng(42)
conv = rng.random(200_000) < 0.032 # True with probability 0.032
print("draws :", conv.size)
print("P(convert) est:", round(conv.mean(), 4)) # ~ 0.0319, close to 0.032
print("count of True :", int(conv.sum())) # ~ 6,380 conversions
Watch the law of large numbers converge. The running estimate settles onto 0.032 as sessions pile up.
import numpy as np, matplotlib.pyplot as plt
running = np.cumsum(conv) / np.arange(1, conv.size + 1)
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(running, color="#0E7C90", lw=1)
ax.axhline(0.032, color="#F97362", ls="--", label="true p = 0.032")
ax.set_xscale("log")
ax.set_xlabel("sessions seen"); ax.set_ylabel("running P(convert)")
ax.set_title("Law of large numbers - noisy early, locked on 0.032 late")
ax.legend(); plt.tight_layout(); plt.show()
# early on the estimate swings wildly; by ~50,000 sessions it barely moves
Do Bayes with natural frequencies. The rare "high-value" flag from the diagram, in code - and its answer is far below 90%.
N = 10_000
base = 0.04 # 4% of orders are genuinely high-value
sens = 0.90 # the flag catches 90% of high-value orders
fpr = 0.08 # the flag also fires on 8% of ordinary orders
high = N * base # 400 high-value orders
ordinary = N * (1 - base) # 9,600 ordinary orders
flag_high = high * sens # 360 true positives
flag_ord = ordinary * fpr # 768 false positives
p_hv_given_flag = flag_high / (flag_high + flag_ord)
print("P(high-value | flag):", round(p_hv_given_flag, 3)) # 0.319
# same thing straight from Bayes theorem
p_flag = base * sens + (1 - base) * fpr
print("via Bayes formula :", round(base * sens / p_flag, 3)) # 0.319
A Lumen growth PM wanted to auto-email every "high-value" flagged customer a VIP coupon. The flag was pitched as "90% accurate." The natural-frequency box says that of every 1,128 emails, only 360 land on a real high-value customer - two thirds are wasted on ordinary orders. Not because the flag is broken, but because high-value orders are rare. The fix is to raise the base rate first (segment before you flag), not to trust the 90%.
Test independence on two real columns. Is converting independent of buying a serum? Check P(A and B) against P(A) x P(B).
import numpy as np, pandas as pd
df = pd.read_parquet("lumen_orders.parquet")
rng = np.random.default_rng(42)
df["converted"] = rng.random(len(df)) < 0.032 # the b1 homework column
A = df["converted"]
B = df["product_category"].eq("serum")
p_A, p_B = A.mean(), B.mean()
print("P(A) P(B) :", round(p_A * p_B, 5))
print("P(A and B) :", round((A & B).mean(), 5)) # ~ equal => independent
# conditional sanity check: P(convert | serum) should ~ P(convert)
print("P(convert | serum):", round(A[B].mean(), 4), "vs", round(p_A, 4))
This week ◐ 40 min total
- Gambler's fallacy, simulated. Draw 1,000 sessions. After a run of 30 non-conversions, is the next session any more likely to convert? Show with a fresh
rng.random() < 0.032that the streak carries no memory. - Move the base rate. Re-run the Bayes script with
base = 0.20instead of 0.04, keeping sensitivity and false-positive rate fixed. Watch P(high-value | flag) jump. Write one sentence on why segmenting first helps. - Addition rule by hand. Estimate P(order is a "set" OR order value > $150) two ways: with the formula P(A) + P(B) - P(A and B), and by direct counting on the dataframe. Confirm they match.
- Optional: simulate the law of large numbers for the AOV mean instead of a proportion - draw order values and plot the running mean converging on ~$74.
Three questions before you go 🎯 ◐ 90 seconds
1 · Lumen converts at 3.2%. A session has just failed to convert 30 times in a row. What is P(convert) on the next session?
Independent trials carry no memory of past outcomes. The gambler's fallacy is believing a run changes the next probability. Each session is a fresh 3.2% draw.
2 · A flag is 90% sensitive on a 4% base rate, with an 8% false-positive rate. When it fires, how often is the order truly high-value?
Of 10,000 orders, 360 true positives and 768 false positives get flagged. 360 / 1,128 is about 32%. When the target is rare, base rate dominates - this is base-rate neglect.
3 · How do you test whether events A and B are independent in numpy?
Independence is defined by P(A and B) = P(A) x P(B). Option C describes mutually exclusive events, which is a different thing. Equal probabilities (B) is unrelated.
What this session covers
This builder session covers ~80% of the introductory-probability content in the sources below, but done as simulation on the Lumen dataset. Distributions as named objects come next, in b4.
Builder Session 2 cheat sheet · pin this
(rng.random(n) < p).mean() approximates P. Big n, close estimate. That is the law of large numbers.