learn-statistics-with-phoebe / Builder session 2 of 10
Learn Statistics with Phoebe · Builder session 2 of 10

Probability and simulation, when the formula is easier to run than to solve

Probability has a reputation for being fiddly algebra. It does not have to be. In this session you learn the four rules that cover almost everything - addition, multiplication, conditional probability, and Bayes - and then you check every one of them by simulating it in numpy. When a Bayes problem makes your head hurt, you will draw 200,000 fake Lumen sessions and just count. Same answer, no anxiety.

🟢 Start-adjacent Builders · DA / DE / DS Python · numpy / scipy 45 min
0-3 · Setup 3-16 · The four rules 16-40 · Simulate it in Python 40-45 · Wrap
Part 0

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.

One Lumen session has exactly two outcomes Convert 3.2% No convert 96.8% A Lumen session converts 3.2% of the time and does not 96.8% of the time - the whole sample space.
🔍 Click to zoom - conversion is the rare outcome, at 3.2% of sessions
Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today The four probability rules in plain language and in code, the difference between joint / marginal / conditional probability, a working definition of independence you can test with one line of numpy, and Bayes theorem done the painless way - with natural frequencies instead of fractions. You will also see the law of large numbers converge in front of you and understand base-rate neglect well enough to never fall for it.
Part 1 · the vocabulary in code

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.

Which probability rule do you reach for yes no yes no Do you want P(A or B)? Are A and B independent? Addition rule P(A) + P(B) - overlap Multiply P(A) x P(B) for independent events Conditional rule multiply by P(B given A) Four rules collapse to one question: are you combining events with OR, AND, or given information?
🔍 Click to zoom - OR adds, independent AND multiplies, dependent AND conditions
10,000 orders the base population 400 high-value 4% base rate 9,600 ordinary 96% 360 get flagged 90% sensitivity 768 get flagged 8% false-positive rate Flagged = 360 + 768 = 1,128 P(high-value | flag) = 360 / 1,128 = 32% - not 90% a 90%-sensitive flag on a rare event is mostly false alarms
🔍 Click to zoom - Bayes by counting people, not juggling 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.
Independence has a precise test A and B are independent exactly when P(A and B) = P(A) x P(B). Not "they feel unrelated" - the multiplication has to hold. In the demo you will check two Lumen columns against this equation directly, no hand-waving.
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."

★ everyone builds · simulate the rules

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).

simulate_convert.py
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.

law_of_large_numbers.py
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%.

bayes_natural_freq.py
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
Real world

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).

independence_check.py
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))
Before Session 3

This week ◐ 40 min total

Check yourself

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.

Source material

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.

✓Sample spaces, addition / multiplication / conditional rules, independence in numpyPart 1 + demo
✓Bayes with natural frequencies; base-rate neglect; law of large numbers by simulationdemo
◐OpenIntro Statistics ch. 3 - Probabilitytheory spine; free PDF
◐Google "The Power of Statistics" - module 2, probabilitymodule 2
◐StatQuest - Bayes theorem, clearly explainedvideo, natural-frequency intuition

Builder Session 2 cheat sheet · pin this

Simulate = estimate(rng.random(n) < p).mean() approximates P. Big n, close estimate. That is the law of large numbers.
AdditionP(A or B) = P(A) + P(B) - P(A and B). Subtract the overlap.
MultiplicationP(A and B) = P(A) x P(B | A). Independent: = P(A) x P(B).
ConditionalP(A | B) = P(A and B) / P(B). Restrict to the world where B is true.
BayesCount people, not fractions. Natural frequencies turn a scary flip into arithmetic.
Base-rate neglectA 90%-sensitive flag on a rare event is mostly false alarms. Raise the base rate before you trust a flag.