From 22c675be07c3992b6c2712b2a0f30ad04bfec871 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Tue, 1 Sep 2020 14:54:41 -0700 Subject: [PATCH] Add files via upload --- ipynb/war.ipynb | 591 +++++++++++++++++++++++++++--------------------- 1 file changed, 328 insertions(+), 263 deletions(-) diff --git a/ipynb/war.ipynb b/ipynb/war.ipynb index 2c4c623..f688571 100644 --- a/ipynb/war.ipynb +++ b/ipynb/war.ipynb @@ -6,9 +6,9 @@ "source": [ "
Peter Norvig
Aug 2020
\n", "\n", - "# WAR: [What is it Good For?](https://www.youtube.com/watch?v=bX7V6FAoTLc)\n", + "# War ([What is it Good For?](https://www.youtube.com/watch?v=bX7V6FAoTLc))\n", "\n", - "The [538 Riddler Classic for 28 August 2020](https://fivethirtyeight.com/features/can-you-cover-the-globe/) asks about the probability of winning the children's [card game **war**](https://en.wikipedia.org/wiki/War_%28card_game%29) in a **sweep**: a game where player **A** wins 26 turns in a row against player **B**. (In **war**, players are dealt 26 cards each and on each turn they flip the top card in their respective hands; higher rank card wins. Other rules are not relevant to this problem.)\n", + "The [538 Riddler Classic for 28 August 2020](https://fivethirtyeight.com/features/can-you-cover-the-globe/) asks about the chance of winning the children's [card game **War**](https://en.wikipedia.org/wiki/War_%28card_game%29) in a **sweep**: a game where player **A** wins 26 turns in a row against player **B**. (In **War**, players are dealt 26 cards each and on each turn they flip the top card in their respective hands; higher rank card wins. Other rules are not relevant to this problem.)\n", "\n", "We'll analyze this problem and come up with a program to solve it; first let's get the imports out of the way:" ] @@ -29,7 +29,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In my analysis I considered four different approaches to the problem, which I will describe in the order I considered them (although I actually implemented the final one first, and then came back to fill in the implementation of the earlier approaches).\n", + "In my analysis I considered four different approaches to the problem, which I will describe in the order I considered them (although I actually implemented the final one first, and then came back to fill in the details of the earlier approaches).\n", "\n", "# Approach 1: Simple Arithmetic?\n", "\n", @@ -44,7 +44,18 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "1.8595392516568175e-09" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "(6/13) ** 26" ] @@ -53,11 +64,38 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "or about 2 in a billion. Unfortunately, that reasoning is **wrong**, even for one turn. The probability of a tie (that is, both players flipping cards of the same rank) on the first turn is actually 3/51 (not 4/52) because after player **A** flips a card, there are 51 equiprobable cards remaining for player **B**, and 3 have the same rank. So we actually have for the first turn:\n", + "or about 2 in a billion. Unfortunately, that reasoning is **wrong**, even for one turn. The probability of a tie (that is, both players flipping cards of the same rank) on the first turn is actually 3/51 (not 1/13 or 4/52) because after player **A** flips a card, there are 51 equiprobable cards remaining for player **B**, and 3 have the same rank. So we actually have for the first turn:\n", "\n", " A wins=24/51; B wins=24/51; tie=3/51\n", - "\n", - "The probabilities on subsequent turns would be slightly different, depending on the cards picked in the previous turns. So simple arithmetic doesn't give us the answer.\n", + " \n", + "And if every subsequent turn were the same, the probability that **A** wins 26 turns in a row would be:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0808297965386556e-09" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(24/51) ** 26" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But actually the probabilities on subsequent turns would be slightly different, depending on the cards picked in the previous turns. So simple arithmetic doesn't give us the answer.\n", "\n", "# Approach 2: Brute Force Enumeration?\n", "\n", @@ -71,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -88,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -100,7 +138,7 @@ " \"\"\"Upon dealing this deck, does player A win every turn?\"\"\"\n", " return all(deck[i] > deck[i + 1] for i in range(0, len(deck), 2))\n", "\n", - "def brute_p_sweep(deck) -> Probability:\n", + "def brute_force_p_sweep(deck) -> Probability:\n", " \"\"\"The probability that A sweeps, considering every permutation of deck.\"\"\"\n", " return mean(A_sweeps(d) for d in itertools.permutations(deck))" ] @@ -114,15 +152,6 @@ "Here we run the code on a tiny deck of just 8 cards, all with different ranks:" ] }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "make_deck(8, 1)" - ] - }, { "cell_type": "code", "execution_count": 6, @@ -131,7 +160,7 @@ { "data": { "text/plain": [ - "0.0625" + "[0, 1, 2, 3, 4, 5, 6, 7]" ] }, "execution_count": 6, @@ -140,14 +169,7 @@ } ], "source": [ - "brute_p_sweep(make_deck(8, 1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There can be no ties in this deck, so this is just four turns where on each turn **A** and **B** each have an equal chance of winning, so the probability of **A** winning all 4 turns is: " + "make_deck(8, 1)" ] }, { @@ -166,6 +188,33 @@ "output_type": "execute_result" } ], + "source": [ + "brute_force_p_sweep(make_deck(8, 1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There can be no ties, so this is four turns where on each turn **A** and **B** have equal chances of winning and the probability of **A** sweeping is: " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0625" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "(1/2) ** 4" ] @@ -179,7 +228,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -188,7 +237,7 @@ "[0, 1, 2, 3, 0, 1, 2, 3]" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -199,7 +248,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -208,13 +257,13 @@ "0.03571428571428571" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "brute_p_sweep(make_deck(4, 2))" + "brute_force_p_sweep(make_deck(4, 2))" ] }, { @@ -222,7 +271,7 @@ "metadata": {}, "source": [ "What about the real deck, with 52 cards? Unfortunately, there are 52! permutations (more than $10^{67}$), and even if we were clever about the duplicated ranks and the ordering of the 26 turns, and\n", - "even if we could process a billion deals a second, it would still take [millions of years](https://www.google.com/search?q=%2852%21+%2F+4%21%5E13+%2F+26%21%29+nanoseconds+in+years&oq=%2852%21+%2F+4%21%5E13+%2F+26%21%29+nanoseconds+in+years) to complete the brute force enumeration. And 538 wanted the answer by Monday.\n", + "even if we could process a billion deals a second, it would still take [millions of years](https://www.google.com/search?q=%2852%21+%2F+4%21%5E13+%2F+26%21%29+nanoseconds+in+years&oq=%2852%21+%2F+4%21%5E13+%2F+26%21%29+nanoseconds+in+years) to complete the brute force enumeration. And 538 Riddler wanted the answer by Monday.\n", "\n" ] }, @@ -232,12 +281,12 @@ "source": [ "# Approach 3: Simulation?\n", "\n", - "It is simple to code up a simulation that **randomly samples** the space of possible permutations rather than processing **all** the permutations. Just repeatedly shuffle the cards and compute how often **A** sweeps:" + "It would take too long to look at **all** the permutations, but we can **randomly sample** the space of possible permutations; we call that a **simulation**:" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -248,13 +297,13 @@ " yield deck\n", "\n", "def simulate(deck, N=10000) -> Probability:\n", - " \"\"\"Simulate N games of war, and return the estimated probability of A sweeping.\"\"\"\n", + " \"\"\"Simulate N games of War, and return the estimated probability of A sweeping.\"\"\"\n", " return mean(A_sweeps(d) for d in deals(deck, N))" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -263,7 +312,7 @@ "0" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -276,7 +325,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Examining 10,000 deals with the full 52-card deck we got zero sweeps. Since the probability of a sweep is probably somewhere around one in a billion, we would need to look at trillions of deals to get a reliable estimate. That would require over a day of run time: much better than millions of years, but still not good enough." + "Sampling 10,000 deals with the full 52-card deck we got zero sweeps. Since the probability of a sweep is probably somewhere around one in a billion, we would need to look at trillions of deals to get a reliable estimate. That would require roughly a day of run time: much better than millions of years, but still not good enough." ] }, { @@ -287,51 +336,47 @@ "\n", "The first three approaches didn't pan out. What next? \n", "\n", - "It is feasible to consider every possible deal if we are careful. As discussed in my [How To Count Things](How%20To%20Count%20Things.ipynb) notebook, the idea is to represent a deck not as a concrete permutation of 52 ranks but rather with a representation that is:\n", - "- **Abstract**: What really matters is not the exact identity of every card in the deck, but rather whether **A**'s next card is higher, lower, or the same as **B**'s next card. For example, if there are only two cards remaining and we know they have different ranks, then the probability of **A** winning is 1/2; it doesn't matter whether the cards are [10, 8] or [2, 5] or any other combination of two distinct ranks.\n", - "- **Incremental**: First we'll consider the possibilities for the two cards in the first turn, and only if **A** wins will we then move on to consider possible cards for the next turn. If **A** loses or ties the first turn, there is no need to consider the 50! permutations of the remaining cards.\n", + "It is feasible to consider every possible deal if we are careful. As discussed in my [How To Count Things](How%20To%20Count%20Things.ipynb) notebook, the idea is to represent a deck not as a concrete permutation of 52 cards but rather with a representation that is:\n", + "- **Abstract**: What really matters is not the exact rank of every card in the deck, but rather whether **A**'s next card is higher, lower, or the same as **B**'s next card. For example, if there are only two cards remaining and we know they have different ranks, then the probability of **A** winning is 1/2; it doesn't matter whether the cards are [10, 8] or [2, 5] or any of the 52 × 51 possibilities for two cards.\n", + "- **Incremental**: First we'll consider the possibilities for the two cards in the first turn, and only if **A** wins will we then move on to consider possible cards for the next turn. For cases where **A** loses or ties the first turn, there is no need to consider the 50! permutations of the remaining cards.\n", "\n", "\n", - "The key is to focus on how many cards in the deck have other cards of the same rank. Thus, we can represent any set of remaining cards with four numbers:\n", - "- The number of ranks that have no other card of the same rank.\n", - "- The number of ranks that have exactly 2 cards of the same rank.\n", - "- The number of ranks that have exactly 3 cards of the same rank.\n", - "- The number of ranks that have exactly 4 cards of the same rank. \n", - "\n", - "An abstract deck (or `AbDeck`) is a tuple where `deck[i]` gives the number of different ranks that have `i` cards in the deck. (This representation is a bit wasteful, because `deck[0]` is always a redundant `0`, but the alternative would be to sprinkle the code with a bunch of `[i - 1]` indexes, risking an off-by-one error).\n", + "The key to the abstract representation is knowing the cards of the same rank. I will define an abstract deck (or `AbDeck`) as a tuple where `deck[i]` gives the number of different ranks that have exactly `i` cards remaining in the deck. That's all the information you need to determine the probability of winning. (This representation is a bit wasteful, because `deck[0]` is always a redundant `0`, but the alternative would be to sprinkle the code with `[i - 1]` indexes, risking an off-by-one error).\n", "\n", "Consider these sample abstract decks:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ - "AbDeck = tuple # Type: Abstract Deck, e.g. (0, 0, 0, 0, 13)\n", + "AbDeck = tuple # The Abstract Deck Type\n", "\n", - "deck = (0, 0, 0, 0, 13)\n", - "tie1 = (0, 0, 1, 0, 12)\n", - "dif1 = (0, 0, 0, 2, 11)\n", - "end4 = (0, 4, 0, 0, 0)" + "deck = AbDeck((0, 0, 0, 0, 13))\n", + "tie1 = AbDeck((0, 0, 1, 0, 12))\n", + "dif1 = AbDeck((0, 0, 0, 2, 11))\n", + "end4 = AbDeck((0, 4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- `deck` is the normal 52 card full deck with 13 ranks, each with 4 cards of that rank, so `deck[4]` is 13. (This abstract deck represents 52! concrete decks.)\n", - "- `tie1` is the deck that results when both players flip a card of the same rank on the first turn. There are 12 ranks with 4 cards each and 1 rank with 2 cards.\n", - "- `dif1` is the deck that results when the players flip cards of different ranks on the first turn. There are 11 ranks each with 4 cards and 2 ranks with 3 cards. `tie1` and `dif1` are the only two possible results for turn 1.\n", - "- `end4` is a deck you would see near the end of a game, with just 4 remaining cards, each of a different rank. (This represents 52 × 48 × 44 × 40 = 4,392,960 concrete decks.)\n", + "- `deck` is the normal 52 card full deck with 13 ranks, each with 4 cards of that rank, so `deck[4]` is 13.\n", + "- `tie1` results when both players flip a card of the same rank on the first turn.
There are 12 ranks with 4 cards each and 1 rank with 2 cards.\n", + "- `dif1` results when the players flip cards of different ranks on the first turn.
There are 11 ranks each with 4 cards and 2 ranks with 3 cards. \n", + "- `end4` is a deck you would see near the end of a game, with just 4 remaining cards, each of a different rank. \n", "\n", - "We can create abstract decks and compute the number of cards in abstract decks as follows:" + "Note that `tie1` and `dif1` are the only two possible abstract deck outcomes for turn 1. That's a lot better than the 52! possibilities that we would need to consider on turn 1 with a concrete deck. \n", + "\n", + "We can create abstract decks and count the number of cards as follows:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -346,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -355,7 +400,7 @@ "[52, 50, 50, 4]" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -368,53 +413,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Abstract Incremental Enumeration Strategy\n", + "# Probability Distributions\n", "\n", - "Now we're ready to outline a strategy to exactly and efficiently compute `p_sweep(deck)`, the probability that **A** sweeps a game of war:\n", - "\n", - "- Start with `dist` being a **probability distribution** of deck outcomes after 0 turns: `{deck: 1.0}`.\n", - "- **for** each of the 26 turns (or in general ncards(deck) / 2 turns):\n", - " - **for** each of the entries in `dist`:\n", - " - See what possible outcomes can arise from the deck\n", - " - update `dist` to include outcomes where **A** might sweep, with appropriate probabilities \n", - "- Sum the probabilities in `dist`" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "class PDist(collections.Counter): \n", - " \"\"\"A probability distribution of {deck: probability}.\"\"\"\n", - "\n", - "def p_sweep(deck: AbDeck) -> Probability:\n", - " \"\"\"The probability that player A sweeps a game of war.\"\"\"\n", - " dist = PDist({deck: 1.0})\n", - " for turn in range(ncards(deck) // 2):\n", - " dist = play_turn(dist)\n", - " return sum(dist.values())\n", - "\n", - "def play_turn(dist) -> PDist:\n", - " \"\"\"Play one turn with all possible card choices for players A and B; return\n", - " the probability distribution of outcomes where A still might sweep.\"\"\"\n", - " P = PDist()\n", - " for deck in dist:\n", - " for deck2, p_Awin in select2(deck):\n", - " P[deck2] += dist[deck] * p_Awin\n", - " return P" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the key is figuring out:\n", - "- All the possible ways to select two cards from the deck.\n", - "- The probability of that selection.\n", - "- Who won (or was it a tie) with that selection.\n", - "figuring out what the probab ility of that selection is, figuring out" + "We want to keep track of the probability of **A** winning, over a variety of possible events (flips of cards). We'll do that with the help of a class called `PDist` (or **probability distribution**), a mapping that lists all the possible abstract decks that might occur on a turn, each mapped to their probability of occurrance." ] }, { @@ -423,14 +424,74 @@ "metadata": {}, "outputs": [], "source": [ - "def select1(deck):\n", - " \"\"\"All ways to pick one card from deck, returning a list of:\n", + "class PDist(collections.Counter): \n", + " \"\"\"A probability distribution of {abstract_deck: probability}.\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Abstract Incremental Enumeration Strategy\n", + "\n", + "Now we're ready to outline a strategy to exactly and efficiently compute `p_sweep(deck)`, the probability that **A** sweeps a game of **War**:\n", + "\n", + "- Start with `P` being a **probability distribution** of deck outcomes after 0 turns: `{deck: 1.0}`.\n", + "- **for** each of the 26 turns (or in general ncards(deck) / 2 turns):\n", + " - Update `P` to be the result of playing a turn, which is given by:\n", + " - **for** each of the entries in `P`:\n", + " - See what possible outcomes can arise from selecting 2 cards from the deck\n", + " - Update the new `PDist` `P1` to include each outcome where **A** might sweep, with appropriate probability\n", + "- Sum the probabilities in `P`" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def p_sweep(deck: AbDeck) -> Probability:\n", + " \"\"\"The probability that player A sweeps a game of War.\"\"\"\n", + " P = PDist({deck: 1.0}) # The initial probability distribution\n", + " for turn in range(ncards(deck) // 2):\n", + " P = play_turn(P)\n", + " return sum(P.values())\n", + "\n", + "def play_turn(P) -> PDist:\n", + " \"\"\"Play one turn with all possible card choices for players A and B; return\n", + " the probability distribution of outcomes where A still might sweep.\"\"\"\n", + " P1 = PDist() # The probability distribution after this turn\n", + " for deck in P:\n", + " for deck2, p_Awin in select2(deck):\n", + " P1[deck2] += P[deck] * p_Awin\n", + " return P1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the key is figuring out:\n", + "- All the possible ways to select two cards from the deck.\n", + "- The probability of each selection.\n", + "- Who won (or was it a tie) with that selection." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def select1(deck) -> list:\n", + " \"\"\"All ways to pick one card from deck, returning a list of tuples:\n", " (remaining deck, probability of pick, index of pick in deck)\"\"\"\n", " return [(remove(i, deck), i * deck[i] / ncards(deck), i)\n", " for i in range(len(deck)) if deck[i]]\n", "\n", - "def select2(deck):\n", - " \"\"\"All ways to select two cards from deck on a turn, yielding tuples:\n", + "def select2(deck) -> list:\n", + " \"\"\"All ways to select two cards from deck, returning a list of tuples:\n", " (remaining deck, probability that deck occurs and player A wins).\"\"\"\n", " return [(deck2, p_i * p_j * p_Awin(deck1, i, j))\n", " for deck1, p_i, i in select1(deck)\n", @@ -439,16 +500,16 @@ " \n", "def p_Awin(deck1, i, j) -> Probability:\n", " \"\"\"Probability that A wins turn when A selects i giving deck1 and B selects j.\"\"\"\n", - " p_tie = 0 if j != i - 1 else 1 / deck1[j]\n", + " p_tie = 1 / deck1[j] if j == i - 1 else 0\n", " return (1 - p_tie) / 2\n", "\n", - "def remove(i, deck):\n", + "def remove(i, deck) -> AbDeck:\n", " \"\"\"Remove one card from deck[i].\"\"\"\n", " deck1 = list(deck)\n", " deck1[i] -= 1\n", " if i - 1 != 0:\n", " deck1[i - 1] += 1\n", - " return tuple(deck1)" + " return AbDeck(deck1)" ] }, { @@ -462,7 +523,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -471,7 +532,7 @@ "3.132436174322294e-09" ] }, - "execution_count": 17, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -484,14 +545,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The probability that **A** sweeps a game of war is a little over 3 in a billion. I have to say, I'm begining to doubt Duane’s friend’s granddaughter's claim that she won 26 in a row.(By the way, this computation took less than 1/10 second, a big improvement over millions of years.) \n", + "The probability that **A** sweeps a game of **War** is a little over 3 in a billion. \n", "\n", - "538 Riddler actually posed the question somewhat differently: \"*How many games of War would you expect to play until you had a game that lasted just 26 turns with no Wars, like Duane’s friend’s granddaughter?*\" That is, they want to know the inverse of the probability, and they are allowing for either **A** or **B** to sweep. So that would be:" + "(By the way, this computation took less than 1/10 second, a big improvement over millions of years.) \n", + "\n", + "That's the answer to *my* question about the probability of **A** sweeping, but 538 Riddler actually posed the question somewhat differently: \"*How many games of War would you expect to play until you had a game that lasted just 26 turns with no wars, like Duane’s friend’s granddaughter?*\" That is, they want to know the inverse of the probability, and they are allowing for either **A** or **B** to sweep. So that would be:" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -500,7 +563,7 @@ "159620171.7049113" ] }, - "execution_count": 18, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -513,117 +576,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - " We would expect a sweep about once every 160 million games." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Gaining Confidence in the Answer\n", - "\n", - "Above we computed that the probability of **A** winning the first turn is exactly 24/51. If every other turn were the same, the probability of a sweep would be:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "3.0808297965386556e-09" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(24/51) ** 26" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is within 2% of the answer we got, lending some credence to our answer.\n", - "\n", - "The brute force algorithm can't handle a big deck, but we can use it to verify that `p_sweep` gets the same answers as `brute_p_sweep` for small decks; in the printout below every case has a `diff` of zero):" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ranks,suits: 2,1; p: 0.50000; diff: 0.000000000\n", - "ranks,suits: 4,1; p: 0.25000; diff: 0.000000000\n", - "ranks,suits: 6,1; p: 0.12500; diff: 0.000000000\n", - "ranks,suits: 8,1; p: 0.06250; diff: 0.000000000\n", - "ranks,suits: 2,2; p: 0.16667; diff: 0.000000000\n", - "ranks,suits: 2,3; p: 0.05000; diff: 0.000000000\n", - "ranks,suits: 3,2; p: 0.06667; diff: 0.000000000\n", - "ranks,suits: 2,4; p: 0.01429; diff: 0.000000000\n", - "ranks,suits: 4,2; p: 0.03571; diff: 0.000000000\n" - ] - } - ], - "source": [ - "for r, s in [(2, 1), (4, 1), (6, 1), (8, 1),\n", - " (2, 2), (2, 3), (3, 2), (2, 4), (4, 2)]:\n", - " p = brute_p_sweep(make_deck(r, s))\n", - " diff = abs(p - p_sweep(make_abdeck(r, s)))\n", - " print(f'ranks,suits: {r},{s}; p: {p:.5f}; diff: {diff:.9f}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And we can do some unit tests on components (we should do more):" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def test():\n", - " # If there are `2N` cards in a deck, all with different ranks, \n", - " # then the probability that A sweeps is `(1/2) ** N`.\n", - " for N in range(200):\n", - " assert p_sweep((0, 2 * N)) == (1/2) ** N\n", - " # Remove a single card from abstract deck\n", - " assert remove(4, (0, 0, 0, 0, 13)) == (0, 0, 0, 1, 12)\n", - " assert remove(4, (0, 0, 0, 1, 12)) == (0, 0, 0, 2, 11)\n", - " assert remove(3, (0, 0, 0, 1, 12)) == (0, 0, 1, 0, 12)\n", - " # Count cards in abstract deck\n", - " assert [ncards(d) for d in (deck, tie1, dif1, end4)] == [52, 50, 50, 4]\n", - " # Select cards\n", - " assert select1((0, 0, 0, 0, 13)) == [((0, 0, 0, 1, 12), 1.0, 4)]\n", - " assert select2((0, 0, 0, 0, 13)) == [((0, 0, 0, 2, 11), 0.47058823529411764)]\n", - " return True\n", - " \n", - "test()" + " We would expect a sweep about once every 160 million games. I have to say, I'm begining to doubt Duane’s friend’s granddaughter's claim. " ] }, { @@ -637,7 +590,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -646,7 +599,7 @@ "(0, 0, 0, 0, 13)" ] }, - "execution_count": 22, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -657,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -666,7 +619,7 @@ "[((0, 0, 0, 1, 12), 1.0, 4)]" ] }, - "execution_count": 23, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -679,14 +632,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is saying with probability 1.0 the result of selecting one card is a deck where there are 12 ranks with four-of-a-kind and 1 rank with three-of-a-kind. The selected card came from index 4 in the deck.\n", + "This is saying with probability 1.0 the result of selecting one card is a deck where there are 12 ranks with four-of-a-kind and 1 rank with three-of-a-kind. The selected card came from index 4 in the deck (it was one of a four-of-a-kind).\n", "\n", "Now we'll try selecting two cards:" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "metadata": {}, "outputs": [ { @@ -695,7 +648,7 @@ "[((0, 0, 0, 2, 11), 0.47058823529411764)]" ] }, - "execution_count": 24, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -708,14 +661,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This says that the only result of the first turn in which **A** wins has 11 ranks with four-of-a-kind and 2 ranks with 3-of-a-kind. The probability of this outcome is 0.47058823529411764, which, as we computed earlier, is 24/51. The rest of the probability goes to an equiprobable result in which **B** wins, and to `(0, 0, 1, 0, 12)`, which indicates a tie on the first turn. Since these outcomes don't result in **A** winning, they do not appeear in the result from `select2`.\n", + "This says that the only result of the first turn in which **A** wins has 11 ranks with four-of-a-kind and 2 ranks with 3-of-a-kind. The probability of this outcome is 0.47058823529411764, which, as we calculated earlier, is 24/51. The rest of the probability goes to an equiprobable result in which **B** wins, and to `(0, 0, 1, 0, 12)`, which indicates a tie on the first turn. Since these outcomes don't result in **A** winning, they do not appear in the result from `select2`.\n", "\n", - "Now let's work through how `p_sweep` repeatedly calls `play_turn`. We start with a probability distribution where we have the initial deck with probability 1.0:" + "Now let's work through how `p_sweep` repeatedly calls `play_turn`, updating the `PDist` `P`, which is initially:" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -724,14 +677,14 @@ "PDist({(0, 0, 0, 0, 13): 1.0})" ] }, - "execution_count": 25, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dist = PDist({deck: 1.0})\n", - "dist" + "P = PDist({deck: 1.0})\n", + "P" ] }, { @@ -743,7 +696,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -752,14 +705,14 @@ "PDist({(0, 0, 0, 2, 11): 0.47058823529411764})" ] }, - "execution_count": 26, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dist = play_turn(dist)\n", - "dist" + "P = play_turn(P)\n", + "P" ] }, { @@ -771,7 +724,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -782,14 +735,14 @@ " (0, 0, 0, 4, 9): 0.16902761104441777})" ] }, - "execution_count": 27, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dist = play_turn(dist)\n", - "dist" + "P = play_turn(P)\n", + "P" ] }, { @@ -801,7 +754,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -816,14 +769,14 @@ " (0, 0, 0, 6, 7): 0.04315598579857475})" ] }, - "execution_count": 28, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dist = play_turn(dist)\n", - "dist" + "P = play_turn(P)\n", + "P" ] }, { @@ -835,7 +788,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -844,20 +797,132 @@ "0.10422926617455494" ] }, - "execution_count": 29, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sum(dist.values())" + "sum(P.values())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You have only a 10% chance of winning three turns in a row. This will steadily fall to 3-in-a-billion after 26 turns. " + "There is only a 10% chance of winning three turns in a row. This will steadily fall to 3-in-a-billion after 26 turns. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Gaining Confidence in the Answer\n", + "\n", + "The answer should be somewhere close to the arithmetic calculation of $(24/51)^{26}$; in fact we see that it differs by less than 2%:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0167508045532467" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p_sweep(deck) / ((24/51) ** 26)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although the brute force algorithm can't handle a 52-card deck, we can use it to verify that there is no difference between `brute_force_p_sweep(deck)` and `p_sweep(deck)` for small decks; this gives us more confidence that both are correct (or maybe both have similar errors):" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "deck(2, 1): p = 0.50000; diff = 0.000000000\n", + "deck(4, 1): p = 0.25000; diff = 0.000000000\n", + "deck(6, 1): p = 0.12500; diff = 0.000000000\n", + "deck(8, 1): p = 0.06250; diff = 0.000000000\n", + "deck(2, 2): p = 0.16667; diff = 0.000000000\n", + "deck(2, 3): p = 0.05000; diff = 0.000000000\n", + "deck(3, 2): p = 0.06667; diff = 0.000000000\n", + "deck(2, 4): p = 0.01429; diff = 0.000000000\n", + "deck(4, 2): p = 0.03571; diff = 0.000000000\n" + ] + } + ], + "source": [ + "for ranks, suits in [(2, 1), (4, 1), (6, 1), (8, 1),\n", + " (2, 2), (2, 3), (3, 2), (2, 4), (4, 2)]:\n", + " p1 = brute_force_p_sweep(make_deck(ranks, suits))\n", + " p2 = p_sweep(make_abdeck(ranks, suits))\n", + " print(f'deck({ranks}, {suits}): p = {p1:.5f}; diff = {abs(p1 - p2):.9f}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And we can do some unit tests on components (we should do more):" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def test() -> bool:\n", + " # If there are `2N` cards in a deck, all with different ranks, \n", + " # then the probability that A sweeps is `(1/2) ** N`.\n", + " for N in range(200):\n", + " assert p_sweep((0, 2 * N)) == (1/2) ** N\n", + " # Remove a single card from abstract deck\n", + " assert remove(4, (0, 0, 0, 0, 13)) == (0, 0, 0, 1, 12)\n", + " assert remove(4, (0, 0, 0, 1, 12)) == (0, 0, 0, 2, 11)\n", + " assert remove(3, (0, 0, 0, 1, 12)) == (0, 0, 1, 0, 12)\n", + " assert remove(1, (0, 2)) == (0, 1)\n", + " assert remove(1, (0, 1)) == (0, 0)\n", + " # Count cards in abstract deck\n", + " assert [ncards(d) for d in (deck, tie1, dif1, end4)] == [52, 50, 50, 4]\n", + " # Select cards\n", + " assert select1((0, 0, 0, 0, 13)) == [((0, 0, 0, 1, 12), 1.0, 4)]\n", + " assert select2((0, 0, 0, 0, 13)) == [((0, 0, 0, 2, 11), 0.47058823529411764)]\n", + " assert select1((0, 2)) == [((0, 1), 1.0, 1)]\n", + " assert select2((0, 2)) == [((0, 0), 0.5)]\n", + " return True\n", + " \n", + "test()" ] } ],