Home Course Concepts About

Various poker odds

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 eric.marsden@risk-engineering.org.


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 generate approximate estimations of the odds using stochastic simulation (Monte Carlo) methods. We also show how to use combinatrics features of the SymPy symbolic mathematics library to analyze card playing problems analytically.

For background information on stochastic simulation, and to download this content as a Jupyter/Python notebook, see our online course materials on Monte Carlo methods.

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.

Stochastic simulation

In [2]:
def random_poker_hand():
    deck = list()
    for suit in ["♦", "♥", "♠", "♣"]:
        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)

A trivial test that this function produces plausible output:

In [3]:
random_poker_hand()
Out[3]:
[('7', '♣'), ('A', '♦'), ('5', '♠'), ('Q', '♠'), ('10', '♥')]

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 = 1_000_000
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)))
Estimated probability of four in a kind: 0.000235
Estimated probability of a full house: 0.001483

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 = 1_000_000
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)))
Estimated probability of a flush: 0.002061

A straight flush is a flush (all the cards are from the same suit) that contains five cards of sequential rank.

In [9]:
def face_value(val) -> int:
    if val in ["10", "9", "8", "7", "6", "5", "4", "3", "2"]:
        return int(val)
    if val == "J": return 11
    if val == "Q": return 12
    if val == "K": return 13
    if val == "A": return 14
    raise ValueError("Not a card value: {}".format(val))
    
def straight_flush_p(hand) -> bool:
    suits = [suit for (value, suit) in hand]
    counts = collections.Counter(suits)
    if 5 not in counts.values():
        return False
    faces = [face_value(value) for (value, suit) in hand]
    return (max(faces) - min(faces)) == 4
In [10]:
N = 1_000_000
count = 0
for i in range(N):
    hand = random_poker_hand()
    if straight_flush_p(hand):
        count += 1
print("Estimated probability of a straight flush: {}".format(count/float(N)))
Estimated probability of a straight flush: 8e-06

A royal flush or royal straight flush is an ace-high straight flush (a straight flush in which the highest card is an ace).

In [11]:
def royal_flush_p(hand) -> bool:
    suits = [suit for (value, suit) in hand]
    counts = collections.Counter(suits)
    if 5 not in counts.values():
        return False
    faces = [face_value(value) for (value, suit) in hand]
    if max(faces) != 14: return False
    return min(faces) == 10
In [12]:
# this is a very rare outcome, so we need a large number of simulations to estimate its probability
N = 10_000_000
count = 0
for i in range(N):
    hand = random_poker_hand()
    if royal_flush_p(hand):
        count += 1
print("Estimated probability of a royal flush: {}".format(count/float(N)))
Estimated probability of a royal flush: 1e-06

Symbolic calculation

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 [13]:
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 ["♦", "♥", "♠", "♣"]:
        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 [14]:
# 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
count_straight_flush = 0
count_royal_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
    if straight_flush_p(hand):
        count_straight_flush += 1
    if royal_flush_p(hand):
        count_royal_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)))
print("Exact probability of a straight flush is {}".format(count_straight_flush/float(N)))
print("Exact probability of a royal flush is {}".format(count_royal_flush/float(N)))
Exact probability of four of a kind is 0.00024009603841536616
Exact probability of a full house is 0.0014405762304921968
Exact probability of a flush is 0.0019807923169267707
Exact probability of a straight flush is 1.3851694523963431e-05
Exact probability of a royal flush is 1.5390771693292702e-06