Skip to content
The Lumenary

Math laboratory

Conditional probability by simulation: dice, doors and cards

Rolls two simulated dice 60,000 times (seeded) and estimates P(A), P(A | B) and P(A and B) for an independent pair, a dependent pair and a mutually exclusive pair, next to the exact values; plays 30,000 Monty Hall games (switching wins about 2/3 of the time) and draws two cards 60,000 times (P(second ace | first ace) = 3/51). Statistical evidence from one run, checked against exact probabilities.

Back to the labStatistical evidenceExact computation
Conditional probability by simulation: dice, doors and cardsPythonStatistical evidenceExact computationComputationally tested
"""Conditional probability as a relative frequency: simulated dice, doors, cards.

A conditional probability P(A | B) can be estimated by looking only at the
trials in which B happened and asking how often A happened too. A seeded
pseudo-random generator rolls two dice 60,000 times: "first die even" is
independent of "total 7", but "first die 6" is not independent of "total at
least 10", and two mutually exclusive events are as dependent as can be. The
Monty Hall game and drawing two cards without replacement show conditioning
changing a probability. Every estimate is printed next to its exact value.
"""

import random
from fractions import Fraction

def roll_pairs(rng, n):
    """n rolls of two fair dice, as (first, second) faces 1..6."""
    return [(rng.randint(1, 6), rng.randint(1, 6)) for _ in range(n)]

def estimate(trials, event, given=None):
    """Relative frequency of `event` among the trials where `given` holds."""
    pool = [t for t in trials if given is None or given(t)]
    return sum(1 for t in pool if event(t)) / len(pool)

def exact(event, given=None):
    """The exact probability over the 36 equally likely outcomes."""
    pool = [(a, b) for a in range(1, 7) for b in range(1, 7)
            if given is None or given((a, b))]
    return Fraction(sum(1 for t in pool if event(t)), len(pool))

def monty_hall(rng, games):
    """Win rates (stick, switch) when the host always opens a goat door."""
    stick = switch = 0
    for _ in range(games):
        car, pick = rng.randrange(3), rng.randrange(3)
        opened = rng.choice([d for d in range(3) if d not in (car, pick)])
        other = next(d for d in range(3) if d not in (pick, opened))
        stick += pick == car
        switch += other == car
    return stick / games, switch / games

def second_ace_given_first(rng, draws):
    """Estimate P(second card is an ace | first is an ace), no replacement."""
    deck = ["A"] * 4 + ["x"] * 48
    both = first = 0
    for _ in range(draws):
        c1, c2 = rng.sample(deck, 2)
        first += c1 == "A"
        both += c1 == "A" and c2 == "A"
    return both / first

def main():
    rng = random.Random(1654)
    rolls = roll_pairs(rng, 60000)
    pairs = [
        ("first even | total 7", lambda t: t[0] % 2 == 0,
         lambda t: t[0] + t[1] == 7),
        ("first is 6 | total >= 10", lambda t: t[0] == 6,
         lambda t: t[0] + t[1] >= 10),
        ("first is 1 | first is 6", lambda t: t[0] == 1, lambda t: t[0] == 6),
    ]
    print("60,000 rolls of two dice      P(A)            P(A | B)")
    print("                              sim    exact    sim    exact")
    for name, a, b in pairs:
        line = (f"  {name:26} {estimate(rolls, a):.3f}  {str(exact(a)):5}    "
                f"{estimate(rolls, a, b):.3f}  {exact(a, b)}")
        print(line)
    print("Independent: P(A | B) = P(A). Not independent: knowing B")
    print("changes the chance of A (to 1/2 in row 2, to 0 in row 3).")
    print()
    stick, switch = monty_hall(rng, 30000)
    print(f"Monty Hall, 30,000 games: stick wins {stick:.3f} (exact 1/3), "
          f"switch {switch:.3f} (2/3)")
    est = second_ace_given_first(rng, 60000)
    print(f"Two cards, 60,000 draws: P(2nd ace | 1st ace) = {est:.3f}")
    print(f"  (exact 3/51 = {3 / 51:.3f}, while P(ace) = 4/52 = "
          f"{4 / 52:.3f})")
    print()
    print("Epistemic status: statistical evidence from one seeded run of a")
    print("pseudo-random generator; another seed gives slightly different")
    print("estimates. The exact values come from counting equally likely")
    print("outcomes, and the lesson proves the rules that connect them.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in