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

## 0. Where this picks up

This note continues [Counting and chance in Python](/en/notes/simulation-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 $n$ times, count the repetitions where $B$ happened ($n_B$) and
where both $A$ and $B$ happened ($n_{AB}$). For large $n$,

$$
P(A\mid B)\approx\frac{n_{AB}}{n_B}.
$$

We only look at the runs where $B$ happened, and ask how often $A$ 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:

```python
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.

### What would Python display? Comparing a list

*Single choice · en*

What would Python display?

```python
>>> child1 = [1, 2, 1]
>>> child1 == 1
```

- \[ ] elementwise: `[True, False, True]`
- \[ ] false: `False`
- \[ ] error: A `TypeError`

Your answer: false

#### 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](http://stat110.net) · 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:

```python
>>> pairs = [(1, 1), (1, 2), (1, 1)]
>>> [c1 == 1 and c2 == 1 for c1, c2 in pairs]
[True, False, True]
```

### What would Python display? Counting with and

*Fill in the blank · en*

What would Python display?

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

Your answer: 2 (Accepted absolute error: 0)

#### 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 $n_{AB}$ for $A$ = both girls.

Source: [Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python](http://stat110.net) · 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.

### Estimate a conditional probability

*Python · en*

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

```python
def conditional_frequency(outcomes, a, b):
    """Estimate P(A | B) as n_AB / n_B over the list OUTCOMES.

    A and B are predicates: functions from an outcome to True or False.
    Return None when B never occurs.

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

#### Public tests

Elder child is a girl

```pycon
>>> 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
```

At least one girl

```pycon
>>> 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
```

B never happens

```pycon
>>> 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
```

Other outcomes

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

#### Hints

1. Count $n_B$ with `sum(1 for o in outcomes if b(o))`.

2. Count $n_{AB}$ the same way, requiring `a(o) and b(o)`.

3. Check for $n_B = 0$ before dividing.

#### Reference solution

```python
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](http://stat110.net) · Original exercise; the book's R examples are paraphrased, not copied

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

```python
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 $10^5$ families above, the first estimate was $0.5005$
> and the second $0.3312$. The exact answers from Example 2.2.5 in the book
> are $1/2$ and $1/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:

```python
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.3313$, close to $1/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.

### Which door does Monty open?

*Python · en*

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.

```python
def monty_opens(chosen, car):
    """Return the door Monty opens among doors 1, 2, and 3.

    It is neither CHOSEN nor CAR. When two doors are allowed,
    return the lower-numbered one.

    >>> monty_opens(1, 2)
    3
    """
    pass
```

#### Public tests

Only one door allowed

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

Player picked the car

```pycon
>>> 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

```python
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](http://stat110.net) · 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`.

### The chance of winning by switching

*Fill in the blank · en*

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: 2/3 (Accepted absolute error: 0)

#### Hints

1. Switching wins exactly when your first pick was wrong.

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

#### Reference solution

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

Source: [Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python](http://stat110.net) · 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:

```python
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 $10^5$ games, switching won $0.6690$ of the time, close
> to $2/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)`.

```python
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)$ is the prior. After learning $B$, the posterior
$P(A\mid B)$ comes from Bayes’ rule. That posterior is the prior for the
next step, and learning $C$ gives $P(A\mid B,C)$.

> **Theorem · 3.1 · Bayes’ rule with a complement**
>
> If $0<P(A)<1$ and $P(E)>0$, then
>
> $$
> 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)$, expanded by the law of total probability.

### Update a probability on evidence

*Python · en*

Complete `bayes_update(prior, p_evidence_if_a, p_evidence_if_not_a)`. Given $P(A)$, $P(E\mid A)$, and $P(E\mid A^c)$, return $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)>0$.

```python
from fractions import Fraction


def bayes_update(prior, p_evidence_if_a, p_evidence_if_not_a):
    """Return P(A | E) from the prior P(A), P(E | A), and P(E | not A).

    >>> bayes_update(Fraction(1, 4), 1, Fraction(2, 3))
    Fraction(1, 3)
    """
    pass
```

#### Public tests

Monday: at least one girl

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

Tuesday: the elder is a girl

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

Evidence that rules out not-A

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

Evidence that says nothing

```pycon
>>> bayes_update(Fraction(1, 5), Fraction(1, 2), Fraction(1, 2))
Fraction(1, 5)
```

#### Hints

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

2. The denominator adds $P(E\mid A^c)\,(1-P(A))$ to the numerator.

#### Reference solution

```python
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 $A$, it can be passed straight back in as the next prior: that is sequential updating. Evidence equally likely under $A$ and not $A$ leaves the prior unchanged.

Source: [Adapted from Blitzstein & Hwang, Introduction to Probability, Chapter 2 and §2.10 (R), rewritten in Python](http://stat110.net) · 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
$A$ = both children are girls, so $P(A)=1/4$.

> **Example · 3.2 · Monday, then Tuesday**
>
> **Monday:** we learn $B$ = at least one child is a girl. If $A$ holds, $B$
> certainly does; if not, $B$ holds in two of the three remaining equally
> likely families, so $P(B\mid A^c)=2/3$.
>
> ```python
> >>> monday = bayes_update(Fraction(1, 4), 1, Fraction(2, 3))
> >>> monday
> Fraction(1, 3)
> ```
>
> **Tuesday:** we also learn $C$ = the elder child is a girl. Given $B$ and
> not $A$, the family is girl–boy or boy–girl, so $P(C\mid A^c,B)=1/2$.
>
> ```python
> >>> bayes_update(monday, 1, Fraction(1, 2))
> Fraction(1, 2)
> ```
>
> Both answers match the simulations in Section 1. Learning $B$ and then $C$
> 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/3$ stays $1/3$ rather
than becoming $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](http://stat110.net).
- [CS61A](https://cs61a.org/) and John DeNero’s
  [*Composing Programs*](https://www.composingprograms.com/), for
  higher-order functions, doctests, and separating logic from input and output.
- Python documentation: [`random`](https://docs.python.org/3/library/random.html),
  [`fractions`](https://docs.python.org/3/library/fractions.html), and
  [`input`](https://docs.python.org/3/library/functions.html#input).
