This notebook is an element of the free risk-engineering.org courseware. It can be distributed under the terms of the Creative Commons Attribution-ShareAlike licence.

Author: Eric Marsden [email protected].

In this notebook, we illustrate the estimation of odds when working with discrete probability distributions, such as those resulting from playing poker (a card game). We also show how to use combinatrics features of the SymPy symbolic mathematics library to analyze card playing problems analytically. The notebook uses some Python 3 features.

In [1]:

```
import random
import collections
```

Poker is played with a standard 52-card deck (a “French” deck, without the jokers). A poker hand is a random subset of 5 elements from the deck of cards. The value of a hand, relative to an opponent’s hand, is determined by various “special” combinations of values or suits in the hand. Let’s estimate then calculate the odds of some of these special combinations.

In [2]:

```
def random_poker_hand():
deck = list()
for suit in ['Spades', 'Hearts', 'Diamonds', 'Clubs']:
for value in ['A', 'K', 'Q', 'J', '10', '9', '8', '7', '6', '5', '4', '3', '2']:
deck.append((value, suit))
return random.sample(deck, 5)
```

We test this function by drawing a random hand:

In [3]:

```
random_poker_hand()
```

Out[3]:

A hand has **four of a kind** if it has four cards with the same value, such as four fives or four aces.

In [4]:

```
def four_of_a_kind_p(hand) -> bool:
values = [value for (value, suit) in hand]
counts = collections.Counter(values)
return 4 in counts.values()
```

A hand is a **full house** if it has three of one value and two of a second, such as three twos and two kings.

In [5]:

```
def full_house_p(hand) -> bool:
values = [value for (value, suit) in hand]
counts = collections.Counter(values)
return (3 in counts.values()) and (2 in counts.values())
```

Poker players sometimes wonder why a four of a kind beats a full house. Let’s compare their relative probabilities using a stochastic simulation.

In [6]:

```
N = 100000
count_full_house = 0
count_four_of_a_kind = 0
for i in range(N):
hand = random_poker_hand()
if four_of_a_kind_p(hand):
count_four_of_a_kind += 1
if full_house_p(hand):
count_full_house += 1
print("Estimated probability of four in a kind: {}".format(count_four_of_a_kind/float(N)))
print("Estimated probability of a full house: {}".format(count_full_house/float(N)))
```

**Exercise**: write some code to estimate the probability of a three-of-a-kind (at least three cards in the hand have the same value). Hint: the value should be around 0.029.

A **flush** is a hand whose cards all have the same suit. Let’s estimate the probability of a flush.

In [7]:

```
def flush_p(hand) -> bool:
suits = [suit for (value, suit) in hand]
counts = collections.Counter(suits)
return 5 in counts.values()
```

In [8]:

```
N = 100000
count_flush = 0
for i in range(N):
hand = random_poker_hand()
if flush_p(hand):
count_flush += 1
print("Estimated probability of a flush: {}".format(count_flush/float(N)))
```

We can use the combinatrics support in the SymPy library to identify exhaustively the possible hands, represented as subsets of size 5 of the permutations of the deck.

In [9]:

```
import sympy
from sympy.combinatorics.subsets import ksubsets
# The value is returned as a Python iterator. This can be converted into a list by calling list() on the
# return value, but the list consumes large amounts of memory. It is more efficient to iterate over the
# possible hands without keeping the full list in memory.
def all_poker_hands():
deck = list()
for suit in ['Spades', 'Hearts', 'Diamonds', 'Clubs']:
for value in ['A', 'K', 'Q', 'J', '10', '9', '8', '7', '6', '5', '4', '3', '2']:
deck.append((value, suit))
# this is the set of all possible hands (with 5 cards taken from the deck)
return ksubsets(deck, 5)
```

In [10]:

```
# here we enumerate all possible hands and count the number that are a full house or 4-of-a-kind
count_four_of_a_kind = 0
count_full_house = 0
count_flush = 0
N = 0
for hand in all_poker_hands():
N += 1
if four_of_a_kind_p(hand):
count_four_of_a_kind += 1
if full_house_p(hand):
count_full_house += 1
if flush_p(hand):
count_flush += 1
print("Exact probability of four of a kind is {}".format(count_four_of_a_kind/float(N)))
print("Exact probability of a full house is {}".format(count_full_house/float(N)))
print("Exact probability of a flush is {}".format(count_flush/float(N)))
```

**Exercise**: a **straight flush** is a flush whose values are consecutive. Calculate the probability of obtaining a straight flush.