Suppose you're playing No Limit Hold'em, and based on how your opponent has played the hand, you believe they have pocket aces, pocket kings, or ace-king. How likely is each hand? (Hint: They're not equally likely!)

The math that determines the likelihood of each hand is called combinatorics and it's in a sweet spot of being non-obvious, yet pretty easy to understand, and highly relevant for in-game strategy. I just finished Fluent Python by Luciano Ramalho and got a refresher on the Python built-in itertools module, which provides the necessary tools to explore combinatorics. Itertools provides functions for creating iterators. Of particular interest for poker are the "combinatoric iterators," such as product and combinations, which combine elements together. These will be useful for combining ranks and suits to form cards, combining cards to form hands, and ultimately understanding the relative likelihood of different hands.

By the way, you can run this post as a colab notebook. Click the colab link under the title to open a notebook in colab where you can run the code or try your own changes.

from itertools import product, combinations, filterfalse

The first example in the book is actually a playing card deck, so we can borrow a little code to get started.

suits = 'spades hearts diamonds clubs'.split()
ranks = [str(n) for n in range(2, 11)] + list('JQKA')

To get the cards in the deck, you can use the product function from itertools. Here's the description:

"Cartesian product: yields N-tuples made by combining items from each input iterable like nested for loops could produce."

Essentially, this gets every combination of suits and ranks.

cards = product(ranks, suits)

All the itertools functions return iterators, so if you want to do things like check the length, you need to coerce the result into a list.

cards = list(cards)
len(cards)
52
cards[0], cards[-1]
(('2', 'spades'), ('A', 'clubs'))

Story checks out!

Now, combinations (or combos) are all the possible two-card starting hands. Conveniently, itertools has a combinations function. It takes two arguments: the set you'll be drawing from, and the size of the combinations. In this case you want all two-card combinations from cards. It generates combinations without replacement, i.e., you can't draw the same card twice.

combos = list(combinations(cards, 2))
len(combos)
1326

There are 1326 combinations, but these include combinations that you would typically consider the same hand.

combos[0]
(('2', 'spades'), ('2', 'hearts'))
combos[1]
(('2', 'spades'), ('2', 'diamonds'))

Even though the above hands have different cards, you'd probably think of both of them as "pocket deuces." If you play a lot of poker, you've likely seen the "Grid," which is a more intuitive way of representing hands.

In this way of representing the hands, the upper right hands are suited, the lower left are offsuit, and the pairs go diagonally across the middle. Each cell represents multiple combinations (but not always the same number of combinations per cell!). To get something more like the grid representation of hands, you can take the product of ranks with itself.

hands = list(product(ranks, ranks))
len(hands)
169
hands[0]
('2', '2')
hands[-1]
('A', 'A')

How likely a hand is depends on how many ways it can be dealt - the combinations. You now have all the information you need to answer the initial question: If you think your opponent has pocket aces, pocket kings, or ace-king, how likely is each?

aces = [('A', s) for s in suits]
kings = [('K', s) for s in suits]
AA = list(combinations(aces, 2))
KK = list(combinations(kings, 2))
AA
[(('A', 'spades'), ('A', 'hearts')),
 (('A', 'spades'), ('A', 'diamonds')),
 (('A', 'spades'), ('A', 'clubs')),
 (('A', 'hearts'), ('A', 'diamonds')),
 (('A', 'hearts'), ('A', 'clubs')),
 (('A', 'diamonds'), ('A', 'clubs'))]
len(AA)
6

There are six ways to deal pocket aces. The same applies for pocket kings, or any other pair, of course. The chances of being dealt pocket aces are six divided by the total number of combinations.

len(AA) / len(combos)
0.004524886877828055

You get dealt pocket aces about one in 200 hands. How does this compare to ace-king?

AK = list(product(aces, kings))
AK
[(('A', 'spades'), ('K', 'spades')),
 (('A', 'spades'), ('K', 'hearts')),
 (('A', 'spades'), ('K', 'diamonds')),
 (('A', 'spades'), ('K', 'clubs')),
 (('A', 'hearts'), ('K', 'spades')),
 (('A', 'hearts'), ('K', 'hearts')),
 (('A', 'hearts'), ('K', 'diamonds')),
 (('A', 'hearts'), ('K', 'clubs')),
 (('A', 'diamonds'), ('K', 'spades')),
 (('A', 'diamonds'), ('K', 'hearts')),
 (('A', 'diamonds'), ('K', 'diamonds')),
 (('A', 'diamonds'), ('K', 'clubs')),
 (('A', 'clubs'), ('K', 'spades')),
 (('A', 'clubs'), ('K', 'hearts')),
 (('A', 'clubs'), ('K', 'diamonds')),
 (('A', 'clubs'), ('K', 'clubs'))]
len(AK)
16

There are 16 combinations of AK. Of those, some are suited and some are offsuit. You can use the filter and filterfalse functions to separate them.

def suited(hand):
    return hand[0][1] == hand[1][1]
AKs = list(filter(suited, AK))
AKo = list(filterfalse(suited, AK))
len(AKs), len(AKo)
(4, 12)

There are 16 combos of ace-king and only six each of aces and kings, so ace-king is more likely than aces and kings combined. Given that pocket deuces is a slight favorite against ace-king, if you hold deuces in this situation, you are actually ahead more often than not. Unfortunately, this is a classic slightly ahead/way behind scenario: you're either a tiny favorite or a huge underdog, so you're still basically screwed. But that's a topic for another day.

Takeaways

  • Not all hands are equally likely.
  • Offsuit, unpaired hands are the most likely. Pairs are less likely. Suited hands are the rarest are all.
  • This means, if you are considering how likely it is for your opponent to have a certain hand, it's really important whether or not they would play the offsuit version of that hand.