{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Various poker odds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " \n", "\n", "This notebook is an element of the free [risk-engineering.org courseware](https://risk-engineering.org/). It can be distributed under the terms of the [Creative Commons Attribution-ShareAlike licence](https://creativecommons.org/licenses/by-sa/4.0/).\n", "\n", "Author: Eric Marsden . \n", "\n", "---\n", "\n", "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](https://sympy.org/) to analyze card playing problems analytically.\n", "\n", "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](https://risk-engineering.org/monte-carlo-methods/)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import random\n", "import collections" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Poker is played with a standard 52-card deck (a “French” deck, without the jokers). A [poker hand](https://en.wikipedia.org/wiki/List_of_poker_hands) 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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Stochastic simulation" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def random_poker_hand():\n", " deck = list()\n", " for suit in [\"♦\", \"♥\", \"♠\", \"♣\"]:\n", " for value in [\"A\", \"K\", \"Q\", \"J\", \"10\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"]:\n", " deck.append((value, suit))\n", " return random.sample(deck, 5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A trivial test that this function produces plausible output:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('A', '♣'), ('4', '♦'), ('4', '♣'), ('3', '♣'), ('9', '♥')]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "random_poker_hand()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A hand has **four of a kind** if it has four cards with the same value, such as four fives or four aces." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "scrolled": false }, "outputs": [], "source": [ "def four_of_a_kind_p(hand) -> bool:\n", " values = [value for (value, suit) in hand]\n", " counts = collections.Counter(values)\n", " return 4 in counts.values()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. " ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def full_house_p(hand) -> bool:\n", " values = [value for (value, suit) in hand]\n", " counts = collections.Counter(values)\n", " return (3 in counts.values()) and (2 in counts.values())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Poker players sometimes wonder why a four of a kind beats a full house. Let’s compare their relative probabilities using a stochastic simulation." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Estimated probability of four in a kind: 0.000243\n", "Estimated probability of a full house: 0.001463\n" ] } ], "source": [ "N = 1_000_000\n", "count_full_house = 0\n", "count_four_of_a_kind = 0\n", "for i in range(N):\n", " hand = random_poker_hand()\n", " if four_of_a_kind_p(hand):\n", " count_four_of_a_kind += 1\n", " if full_house_p(hand):\n", " count_full_house += 1\n", "print(\"Estimated probability of four in a kind: {}\".format(count_four_of_a_kind/float(N)))\n", "print(\"Estimated probability of a full house: {}\".format(count_full_house/float(N)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A **flush** is a hand whose cards all have the same suit. Let’s estimate the probability of a flush." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def flush_p(hand) -> bool:\n", " suits = [suit for (value, suit) in hand]\n", " counts = collections.Counter(suits)\n", " return 5 in counts.values()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Estimated probability of a flush: 0.002042\n" ] } ], "source": [ "N = 1_000_000\n", "count_flush = 0\n", "for i in range(N):\n", " hand = random_poker_hand()\n", " if flush_p(hand):\n", " count_flush += 1\n", "print(\"Estimated probability of a flush: {}\".format(count_flush/float(N)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A **straight flush** is a flush (all the cards are from the same suit) that contains five cards of sequential rank. " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def face_value(val) -> int:\n", " if val in [\"10\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"]:\n", " return int(val)\n", " if val == \"J\": return 11\n", " if val == \"Q\": return 12\n", " if val == \"K\": return 13\n", " if val == \"A\": return 14\n", " raise ValueError(\"Not a card value: {}\".format(val))\n", " \n", "def straight_flush_p(hand) -> bool:\n", " suits = [suit for (value, suit) in hand]\n", " counts = collections.Counter(suits)\n", " if 5 not in counts.values():\n", " return False\n", " faces = [face_value(value) for (value, suit) in hand]\n", " return (max(faces) - min(faces)) == 4" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Estimated probability of a straight flush: 1.3e-05\n" ] } ], "source": [ "N = 1_000_000\n", "count = 0\n", "for i in range(N):\n", " hand = random_poker_hand()\n", " if straight_flush_p(hand):\n", " count += 1\n", "print(\"Estimated probability of a straight flush: {}\".format(count/float(N)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A **royal flush** or **royal straight flush** is an ace-high straight flush (a straight flush in which the highest card is an ace). " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def royal_flush_p(hand) -> bool:\n", " suits = [suit for (value, suit) in hand]\n", " counts = collections.Counter(suits)\n", " if 5 not in counts.values():\n", " return False\n", " faces = [face_value(value) for (value, suit) in hand]\n", " if max(faces) != 14: return False\n", " return min(faces) == 10" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Estimated probability of a royal flush: 1.6e-06\n" ] } ], "source": [ "# this is a very rare outcome, so we need a large number of simulations to estimate its probability\n", "N = 10_000_000\n", "count = 0\n", "for i in range(N):\n", " hand = random_poker_hand()\n", " if royal_flush_p(hand):\n", " count += 1\n", "print(\"Estimated probability of a royal flush: {}\".format(count/float(N)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Symbolic calculation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "import sympy\n", "from sympy.combinatorics.subsets import ksubsets\n", "\n", "# The value is returned as a Python iterator. This can be converted into a list by calling list() on the\n", "# return value, but the list consumes large amounts of memory. It is more efficient to iterate over the \n", "# possible hands without keeping the full list in memory.\n", "def all_poker_hands():\n", " deck = list()\n", " for suit in [\"♦\", \"♥\", \"♠\", \"♣\"]:\n", " for value in [\"A\", \"K\", \"Q\", \"J\", \"10\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"]:\n", " deck.append((value, suit))\n", " # this is the set of all possible hands (with 5 cards taken from the deck)\n", " return ksubsets(deck, 5)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Exact probability of four of a kind is 0.00024009603841536616\n", "Exact probability of a full house is 0.0014405762304921968\n", "Exact probability of a flush is 0.0019807923169267707\n", "Exact probability of a straight flush is 1.3851694523963431e-05\n", "Exact probability of a royal flush is 1.5390771693292702e-06\n" ] } ], "source": [ "# here we enumerate all possible hands and count the number that are a full house \n", "# or 4-of-a-kind\n", "count_four_of_a_kind = 0\n", "count_full_house = 0\n", "count_flush = 0\n", "count_straight_flush = 0\n", "count_royal_flush = 0\n", "N = 0\n", "for hand in all_poker_hands():\n", " N += 1\n", " if four_of_a_kind_p(hand):\n", " count_four_of_a_kind += 1\n", " if full_house_p(hand):\n", " count_full_house += 1\n", " if flush_p(hand):\n", " count_flush += 1\n", " if straight_flush_p(hand):\n", " count_straight_flush += 1\n", " if royal_flush_p(hand):\n", " count_royal_flush += 1\n", "print(\"Exact probability of four of a kind is {}\".format(count_four_of_a_kind/float(N)))\n", "print(\"Exact probability of a full house is {}\".format(count_full_house/float(N)))\n", "print(\"Exact probability of a flush is {}\".format(count_flush/float(N)))\n", "print(\"Exact probability of a straight flush is {}\".format(count_straight_flush/float(N)))\n", "print(\"Exact probability of a royal flush is {}\".format(count_royal_flush/float(N)))" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.10" } }, "nbformat": 4, "nbformat_minor": 1 }