Dorian’s Math
Probability & statistics

Conditional probability in Python: two children, Monty Hall, and updating on evidence

The R section of Blitzstein and Hwang’s second chapter, rebuilt in Python in the style of CS61A: predicates as values, a frequency that estimates P(A|B), and a Monty Hall game split into pure logic and input.

Dorian Verlaine

0. Where this picks up

This note continues Counting and chance in Python, which rebuilt Chapter 1’s R section. Chapter 2 of Blitzstein and Hwang’s Introduction to Probability is about conditional probability, and its R section (§2.10) checks two famous results by simulation: the two-child problem and the Monty Hall problem. The same habits apply: small named functions, doctests, and functions passed around as values. The standard library is enough, and replicate from the previous note is not needed here.

1. Conditional probability as a frequency

The frequentist reading of conditional probability says: repeat an experiment nn times, count the repetitions where BB happened (nBn_B) and where both AA and BB happened (nABn_{AB}). For large nn,

P(A∣B)≈nABnB.P(A\mid B)\approx\frac{n_{AB}}{n_B}.

We only look at the runs where BB happened, and ask how often AA happened among those.

1.1. Simulating families

Let each family have two children, each a girl (1) or a boy (2) with equal probability, independently. R builds two vectors, one per child, and Python can do the same. zip then pairs them up, one tuple per family:

import random

random.seed(110)
n = 10 ** 5
child1 = random.choices([1, 2], k=n)     # elder child of each family
child2 = random.choices([1, 2], k=n)     # younger child
families = list(zip(child1, child2))

families[0] might be (1, 2): an elder girl and a younger boy. Keeping the two children of a family in one tuple means we can never accidentally pair the elder child of one family with the younger child of another.

1.2. No elementwise comparison

R’s child1 == 1 compares every entry of a vector with 1 and returns a vector of TRUE and FALSE. Python’s == compares whole values, and a list is not equal to a number.

Single choice

What would Python display? Comparing a list

What would Python display?

>>> child1 = [1, 2, 1]
>>> child1 == 1
Your answer

Choose one answer.

Hints
1

Python’s == compares the whole left value with the whole right value.

2

Is a list ever equal to a number?

Reference solution

False. Python compares the list [1, 2, 1] as a whole with the number 1, and they are not equal. Nothing is compared entry by entry, unlike R, where child1 == 1 gives TRUE FALSE TRUE. Write [c == 1 for c in child1] to get [True, False, True].

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

So Python spells out the loop with a comprehension. R’s & and | become Python’s and and or, applied inside it:

>>> pairs = [(1, 1), (1, 2), (1, 1)]
>>> [c1 == 1 and c2 == 1 for c1, c2 in pairs]
[True, False, True]
Fill in the blank

What would Python display? Counting with and

What would Python display?

>>> pairs = [(1, 1), (1, 2), (2, 1), (1, 1)]
>>> sum([c1 == 1 and c2 == 1 for c1, c2 in pairs])
Your answer

Use an integer, decimal, or fraction such as 1/2. An exact value is expected.

Hints
1

The comprehension is [True, False, False, True].

2

sum counts each True as 1.

Reference solution

2. Only the first and last pairs have both children equal to 1, so the list is [True, False, False, True], and its sum is 2. This is nABn_{AB} for AA = both girls.

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

1.3. Events are predicates

Rather than writing a new count for every question, describe each event as a predicate: a function that takes an outcome and returns True or False. Then one function estimates any conditional probability. This is the CS61A idea of passing functions as arguments.

Python

Estimate a conditional probability

Complete conditional_frequency(outcomes, a, b). a and b are predicates, functions that take one outcome and return True or False. Return nAB/nBn_{AB}/n_B: the number of outcomes where both hold, divided by the number where b holds. Return None if b never holds.

Your answer

Python function exercise. Tab moves focus; use the indent buttons for selected lines.

Public tests

1. Elder child is a girl

>>> families = [(1, 1), (1, 2), (2, 1), (2, 2)]
>>> both_girls = lambda f: f == (1, 1)
>>> conditional_frequency(families, both_girls, lambda f: f[0] == 1)
0.5

2. At least one girl

>>> families = [(1, 1), (1, 2), (2, 1), (2, 2)]
>>> both_girls = lambda f: f == (1, 1)
>>> round(conditional_frequency(families, both_girls, lambda f: 1 in f), 3)
0.333

3. B never happens

>>> families = [(1, 1), (1, 2), (2, 1), (2, 2)]
>>> both_girls = lambda f: f == (1, 1)
>>> conditional_frequency(families, both_girls, lambda f: False) is None
True

4. Other outcomes

>>> rolls = [1, 2, 3, 4, 5, 6]
>>> conditional_frequency(rolls, lambda r: r == 6, lambda r: r % 2 == 0)
0.3333333333333333
Hints
1

Count nBn_B with sum(1 for o in outcomes if b(o)).

2

Count nABn_{AB} the same way, requiring a(o) and b(o).

3

Check for nB=0n_B = 0 before dividing.

Reference solution
def conditional_frequency(outcomes, a, b):
    n_b = sum(1 for o in outcomes if b(o))
    n_ab = sum(1 for o in outcomes if a(o) and b(o))
    return None if n_b == 0 else n_ab / n_b

The function knows nothing about children or dice. It only calls the predicates it is given, which is what lets one definition answer every question in the note.

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

With conditional_frequency written, the book’s two questions are two lines:

both_girls = lambda f: f == (1, 1)
elder_girl = lambda f: f[0] == 1
at_least_one_girl = lambda f: 1 in f

conditional_frequency(families, both_girls, elder_girl)
conditional_frequency(families, both_girls, at_least_one_girl)

Example 1.1 · What one run gave

With the seed and 10510^5 families above, the first estimate was 0.50050.5005 and the second 0.33120.3312. The exact answers from Example 2.2.5 in the book are 1/21/2 and 1/31/3. Knowing that the elder child is a girl and knowing that at least one child is a girl are different pieces of information, and they lead to different answers.

2. The Monty Hall problem

A car is behind one of three doors, goats behind the other two. You pick a door. Monty, who knows where the car is, opens one of the other doors to show a goat, and offers to let you switch. Should you?

2.1. Never switching

Assume you always pick door 1. Then the never-switch strategy wins exactly when the car is behind door 1, and we do not even need to simulate Monty:

random.seed(110)
cardoor = random.choices([1, 2, 3], k=n)
sum(car == 1 for car in cardoor) / n

One run with seed 110 gave 0.33130.3313, close to 1/31/3.

2.2. Modeling Monty

To simulate switching we need Monty’s choice. When your door hides a goat, Monty has exactly one door he can open. When your door hides the car, he has two, and in the game he picks at random. For code that is easy to test, the next exercise makes a fixed choice instead, always the lowest-numbered door allowed. Randomness can be added back around it later.

Python

Which door does Monty open?

Complete monty_opens(chosen, car) for doors 1, 2, and 3. Monty never opens the player’s door or the car’s door. When two doors are allowed (the player picked the car), return the lower-numbered one, so the result is always predictable.

Your answer

Python function exercise. Tab moves focus; use the indent buttons for selected lines.

Public tests

1. Only one door allowed

>>> monty_opens(1, 2)
3
>>> monty_opens(2, 1)
3
>>> monty_opens(3, 1)
2

2. Player picked the car

>>> monty_opens(1, 1)
2
>>> monty_opens(2, 2)
1
>>> monty_opens(3, 3)
1
Hints
1

Consider each door in (1, 2, 3) and keep the ones that are neither chosen nor car.

2

min of the allowed doors gives the lower-numbered one.

Reference solution
def monty_opens(chosen, car):
    return min(door for door in (1, 2, 3) if door != chosen and door != car)

When chosen != car, exactly one door is allowed; when they are equal, two are, and min picks the lower. In the real game Monty chooses at random in that case, which the simulation in the note adds back.

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

Once Monty has opened a door, switching means taking the one door that is neither yours nor his. Doors 1, 2, and 3 add up to 6, so that door is 6 - chosen - opened.

Fill in the blank

The chance of winning by switching

You pick door 1, Monty opens a door with a goat behind it, and you always switch. What is the probability that you win the car? Answer as a fraction.

Your answer

Use an integer, decimal, or fraction such as 1/2. An exact value is expected.

Hints
1

Switching wins exactly when your first pick was wrong.

2

Your first pick is wrong with probability 2/32/3.

Reference solution

2/32/3. If your first pick is the car (probability 1/31/3), switching loses. If your first pick is a goat (probability 2/32/3), Monty must open the other goat door, so the door you switch to has the car. The simulation in the note gives 0.66900.6690.

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

To check that answer, simulate the whole game, with Monty choosing at random when he has a choice:

def play_switching():
    """Pick door 1, watch Monty open a goat door, switch; return True on a win."""
    car = random.randint(1, 3)
    chosen = 1
    opened = random.choice([d for d in (1, 2, 3) if d != chosen and d != car])
    return 6 - chosen - opened == car

random.seed(110)
sum(play_switching() for _ in range(n)) / n

Example 2.1 · What one run gave

With seed 110 and 10510^5 games, switching won 0.66900.6690 of the time, close to 2/32/3. Never switching wins only when the first pick was right, so switching wins exactly when it was wrong.

2.3. Playing the game

The book ends with an interactive monty() function. In Python, input plays the role of R’s scan: it prints a prompt and returns what the player types, always as a string, so int(...) converts the door number. An f-string replaces R’s paste, and startswith replaces substr(reply, 1, 1).

import random

def switch_door(chosen, opened):
    """The one door that is neither CHOSEN nor OPENED."""
    return 6 - chosen - opened

def monty():
    """Play one round of the Monty Hall game at the terminal."""
    doors = [1, 2, 3]
    car = random.choice(doors)                       # where the car is
    print("Monty Hall says 'Pick a door, any door!'")
    chosen = int(input('Your door (1, 2, or 3): '))
    # Monty opens a door that is neither yours nor the car's.
    opened = random.choice([d for d in doors if d != chosen and d != car])
    print(f'Monty opens door {opened}!')
    reply = input('Would you like to switch (y/n)? ')
    if reply.strip().lower().startswith('y'):        # "yes", "yep", "Yeah!"
        chosen = switch_door(chosen, opened)
    print('You won!' if chosen == car else 'You lost!')

Save this as monty.py and run python3 -i monty.py, then call monty() as often as you like. The exercises on this page cannot run it, because they have no keyboard input.

Notice the shape of the program. Every decision that can be tested without a person at the keyboard (switch_door, and monty_opens in the exercise) is a pure function. Only the outer monty talks to the player. CS61A calls this separating computation from input and output, and it is why the earlier exercises can check the logic with doctests.

3. Posteriors become priors

The end of Chapter 2 (Figure 2.7) describes how evidence arrives in steps. Before any evidence, P(A)P(A) is the prior. After learning BB, the posterior P(A∣B)P(A\mid B) comes from Bayes’ rule. That posterior is the prior for the next step, and learning CC gives P(A∣B,C)P(A\mid B,C).

Theorem 3.1 · Bayes’ rule with a complement

If 0<P(A)<10<P(A)<1 and P(E)>0P(E)>0, then

P(A∣E)=P(E∣A) P(A)P(E∣A) P(A)+P(E∣Ac) P(Ac).P(A\mid E)=\frac{P(E\mid A)\,P(A)}{P(E\mid A)\,P(A)+P(E\mid A^c)\,P(A^c)}.

The denominator is P(E)P(E), expanded by the law of total probability.

Python

Update a probability on evidence

Complete bayes_update(prior, p_evidence_if_a, p_evidence_if_not_a). Given P(A)P(A), P(E∣A)P(E\mid A), and P(E∣Ac)P(E\mid A^c), return P(A∣E)P(A\mid E) using Bayes’ rule with the law of total probability. The tests pass Fraction values so the answers are exact. Assume P(E)>0P(E)>0.

Your answer

Python function exercise. Tab moves focus; use the indent buttons for selected lines.

Public tests

1. Monday: at least one girl

>>> bayes_update(Fraction(1, 4), 1, Fraction(2, 3))
Fraction(1, 3)

2. Tuesday: the elder is a girl

>>> bayes_update(Fraction(1, 3), 1, Fraction(1, 2))
Fraction(1, 2)

3. Evidence that rules out not-A

>>> bayes_update(Fraction(1, 100), 1, 0)
Fraction(1, 1)

4. Evidence that says nothing

>>> bayes_update(Fraction(1, 5), Fraction(1, 2), Fraction(1, 2))
Fraction(1, 5)
Hints
1

The numerator is P(E∣A) P(A)P(E\mid A)\,P(A).

2

The denominator adds P(E∣Ac) (1−P(A))P(E\mid A^c)\,(1-P(A)) to the numerator.

Reference solution
def bayes_update(prior, p_evidence_if_a, p_evidence_if_not_a):
    numerator = p_evidence_if_a * prior
    return numerator / (numerator + p_evidence_if_not_a * (1 - prior))

Because the result is again a probability for AA, it can be passed straight back in as the next prior: that is sequential updating. Evidence equally likely under AA and not AA leaves the prior unchanged.

Source: Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python · Original exercise; the book's R examples are paraphrased, not copied

Updating one piece of evidence at a time is then a chain of calls, each output feeding the next input. Take the two-child family again, with AA = both children are girls, so P(A)=1/4P(A)=1/4.

Example 3.2 · Monday, then Tuesday

Monday: we learn BB = at least one child is a girl. If AA holds, BB certainly does; if not, BB holds in two of the three remaining equally likely families, so P(B∣Ac)=2/3P(B\mid A^c)=2/3.

>>> monday = bayes_update(Fraction(1, 4), 1, Fraction(2, 3))
>>> monday
Fraction(1, 3)

Tuesday: we also learn CC = the elder child is a girl. Given BB and not AA, the family is girl–boy or boy–girl, so P(C∣Ac,B)=1/2P(C\mid A^c,B)=1/2.

>>> bayes_update(monday, 1, Fraction(1, 2))
Fraction(1, 2)

Both answers match the simulations in Section 1. Learning BB and then CC is the same as learning “the elder child is a girl” directly, which is a useful check on the arithmetic.

fractions.Fraction keeps the arithmetic exact, so 1/31/3 stays 1/31/3 rather than becoming 0.333…0.333\ldots with a rounding error.

References

  • Joseph K. Blitzstein and Jessica Hwang, Introduction to Probability, Chapter 2: Example 2.2.5 (two children), the Monty Hall problem, Figure 2.7 (sequential updating), and §2.10 (R). Course materials: stat110.net.
  • CS61A and John DeNero’s Composing Programs, for higher-order functions, doctests, and separating logic from input and output.
  • Python documentation: random, fractions, and input.
Read as Markdown ↗All notes